Skip to content

feat(erli): reuse an existing Allegro connection's credentials for category access (#1387) - #1405

Merged
piotrswierzy merged 1 commit into
epic/1381-erli-allegro-category-catalogfrom
1387-erli-reuse-allegro-connection-credentials
Jul 8, 2026
Merged

feat(erli): reuse an existing Allegro connection's credentials for category access (#1387)#1405
piotrswierzy merged 1 commit into
epic/1381-erli-allegro-category-catalogfrom
1387-erli-reuse-allegro-connection-credentials

Conversation

@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator

Closes #1387. Part of epic #1381 (final sub-task).

Summary

Extends the #1384 Erli credentials panel (PR #1401) with a reuse-vs-manual choice for the Allegro app credentials used for category-catalog access: when the operator already has an Allegro connection configured, its clientId/clientSecret can be reused instead of registering a second Allegro app. Matches the approved mockup (both states).

Security: the raw Allegro clientSecret is resolved and copied entirely server-side — it is never serialized into any HTTP response body, and the frontend never sees it.

Architecture note

The initial implementation embedded the reuse-resolution logic (with 'allegro'/allegroClientId/allegroClientSecret literals) directly inside the generic, cross-platform ConnectionService.updateCredentials. That was caught in review as a "platform name leaks into core/host layer" violation and refactored before this PR was opened.

The shipped design instead introduces a new, fully platform-neutral ConnectionCredentialsRewriterPort + ConnectionCredentialsRewriterRegistryService in libs/core/src/integrations/, mirroring the existing ConnectionConfigShapeValidatorPort/ConnectionCredentialsShapeValidatorPort pair. ConnectionService.updateCredentials now just looks up a rewriter by adapterKey and no-ops when none is registered — it has zero knowledge of Allegro or Erli.

All Erli/Allegro-specific logic (resolve reuseAllegroConnectionId → read the source connection's credentials → validate it's an Allegro connection with client credentials configured) lives in ErliAllegroCredentialsRewriterAdapter, registered from a new companion ErliCredentialsRewriterModule (mirrors the existing ErliWebhookProvisioningModule shape) since the adapter needs a NestJS-injected ConnectionPort that's intentionally outside the framework-neutral HostServices bag.

Backend

  • libs/core/src/integrations/domain/ports/connection-credentials-rewriter.port.ts — new port
  • libs/core/src/integrations/domain/exceptions/connection-credentials-rewrite.exception.ts — new domain exception
  • libs/core/src/integrations/infrastructure/adapters/connection-credentials-rewriter-registry.service.ts — new registry
  • apps/api/.../connection.service.tsupdateCredentials now dispatches through the registry (generic, no platform knowledge)
  • libs/integrations/erli/src/infrastructure/adapters/erli-allegro-credentials-rewriter.adapter.ts — the actual reuse logic
  • libs/integrations/erli/src/erli-credentials-rewriter.module.ts — registers the adapter
  • HostServices.connectionCredentialsRewriterRegistry threaded through libs/plugin-sdk and all three host-plugin modules (Allegro, PrestaShop, Erli) that build the bag manually

Tenant/ownership note: this codebase has no tenant/organization model — there's no tenantId/organizationId anywhere on Connection. @Roles('admin') on the credentials endpoint is the only access boundary that exists today, applied uniformly across the single-deployment instance. The closest enforceable check is that the reuse-source id must resolve to a real, existing platformType: 'allegro' connection — anything else is rejected. Documented inline in the adapter's JSDoc.

Frontend

ErliCredentialsPanel (apps/web/src/plugins/erli/components/erli-credentials-panel.tsx): when ≥1 active Allegro connection exists, shows a "Reuse credentials from an existing Allegro connection" / "Enter Allegro app credentials manually" radio choice with a connection picker, per the approved mockup. Zero Allegro connections falls back to manual-only entry (today's #1384 behavior) with a notice. The reuse path sends { reuseAllegroConnectionId } and follows the same sequenced, fail-safe-ordered write pattern established in #1401 (credentials first, then the allegroCategoryAccessEnabled config patch).

Tests

  • connection.service.spec.ts — generic passthrough/delegation/error-mapping tests against a stub rewriter (no Allegro-specific cases; those moved out)
  • erli-allegro-credentials-rewriter.adapter.spec.ts — new, 7 cases (successful reuse, not-found, wrong platform, missing client credentials, blank id, passthrough when absent)
  • erli-credentials-panel.test.tsx — zero-connections notice, reuse radio + picker, successful reuse submit (asserts payload never contains the raw secret), manual override still works, reuse-without-selection validation

Quality gate

Full-repo pnpm lint, pnpm check:invariants, pnpm type-check, and pnpm test all green (confirmed independently in an isolated worktree before this PR was opened).

…tegory access (#1387)

Extends the #1384 Erli credentials panel with a reuse-vs-manual choice: when
the operator already has an Allegro connection, its clientId/clientSecret can
be copied server-side into the Erli connection instead of registering a
second Allegro app. The raw clientSecret never round-trips through the
browser.

The credential-copy logic is dispatched through a new, platform-neutral
ConnectionCredentialsRewriterPort + registry (mirroring the existing
ConnectionConfigShapeValidatorPort/ConnectionCredentialsShapeValidatorPort
pair) so the generic ConnectionService.updateCredentials stays unaware that
Allegro or Erli exist. The Erli-specific resolution lives entirely in
ErliAllegroCredentialsRewriterAdapter, registered from a companion
ErliCredentialsRewriterModule (mirrors ErliWebhookProvisioningModule) since
it needs ConnectionPort, which is intentionally outside the plugin-neutral
HostServices bag.

Closes #1387

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
@norbert-kulus-blockydevs

Copy link
Copy Markdown
Collaborator Author

Review

Overall this is a clean, well-precedented change: pulling the reuse-resolution logic out of ConnectionService into a new ConnectionCredentialsRewriterPort/registry (mirroring the existing shape-validator pair) correctly keeps CORE/host platform-agnostic, and the raw Allegro secret never round-trips through the browser (confirmed by the existing FE test asserting allegroClientSecret never appears in the outbound payload).

IMPORTANT

  1. ConnectionCredentialsRewriterPort's own contract is only half-wired. libs/core/src/integrations/domain/ports/connection-credentials-rewriter.port.ts documents the rewrite as happening "on connection create / credential rotation," but ConnectionService.create() (apps/api/src/integrations/application/services/connection.service.ts, ~line 170) validates the raw payload directly and never calls rewriteCredentials - only updateCredentials() does. If reuseAllegroConnectionId were ever submitted through POST /connections (create), it would be persisted verbatim as an unresolved literal instead of being resolved, silently breaking the feature. Not reachable via the shipped FE today (the panel only uses the rotate endpoint), but worth either wiring create() too or narrowing the port's docstring to "credential rotation only."

  2. No status check on the reuse-source connection. ErliAllegroCredentialsRewriterAdapter.rewrite() (libs/integrations/erli/src/infrastructure/adapters/erli-allegro-credentials-rewriter.adapter.ts:41-49) checks platformType === 'allegro' and that client credentials exist, but never checks status. The FE picker filters to status: 'active' connections, but that's UI-only, not a backend boundary - a direct API call could reuse credentials from a disabled/error Allegro connection. Worth an explicit status check or a documented rationale for omitting it.

SUGGESTION

  1. apps/web/src/plugins/erli/components/erli-credentials-panel.tsx calls useConnectionsQuery({ platformType: 'allegro', status: 'active' }) unconditionally on every render, even when the rotate panel (showRotate) is collapsed. Consider gating with enabled: showRotate so the query only fires when the operator actually opens "Rotate API key."

  2. The panel continues (and expands) the file's existing inline-style pattern (style={{ display: 'flex', ... }}) rather than shared classes per docs/frontend-ui-style-guide.md. Not introduced by this PR, but grown by it - worth a follow-up cleanup pass.

Security

No security findings. The design intentionally keeps the raw secret server-side (never echoed in responses, error messages only leak the already-known connection ID/platformType), the authorization boundary (@Roles('admin') on updateCredentials) is unchanged, and the cross-connection credential read isn't a new privilege boundary given OpenLinker's current single-tenant, admin-only access model.

Verdict: approve with changes - resolve or explicitly document #1 and #2 before merge; #3/#4 are optional follow-ups. Naming, layering, DI tokens, and test coverage all follow the documented conventions.

@piotrswierzy piotrswierzy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/pr-review — feat(erli): reuse an existing Allegro connection's credentials for category access (#1387)

Summary

Adds a "reuse an existing Allegro connection's app credentials" path to the Erli credentials panel, resolving the raw clientSecret entirely server-side so it never reaches the browser or any response body. The standout is the architecture: an initial version that embedded 'allegro'/allegroClient* literals in the generic ConnectionService.updateCredentials was caught in review and refactored into a fully platform-neutral ConnectionCredentialsRewriterPort + registry — exactly mirroring the #586/#587 shape-validator pattern. Security-conscious, correctly layered, well-tested. Approve.

Architecture — the platform-leak fix is textbook ✅

  • ConnectionService.updateCredentials now dispatches through ConnectionCredentialsRewriterRegistryService.get(adapterKey)?.rewrite() and no-ops when no rewriter is registered — the only allegro/erli mentions in the file are explanatory comments; the code carries zero platform knowledge. This is precisely the CORE↔Integration boundary the docs demand ("no platform name leaks into core/host").
  • New surface is correctly placed: port in domain/ports/, exception in domain/exceptions/ (extends Error, sets .name, captureStackTrace — standards-compliant), registry in infrastructure/adapters/, token in integrations.tokens.ts, threaded through HostServices + plugin-sdk + all three host modules. The ErliCredentialsRewriterModule mirrors the existing ErliWebhookProvisioningModule shape (needs a Nest-injected ConnectionPort outside the framework-neutral HostServices bag — the right call).
  • All Allegro/Erli logic lives in ErliAllegroCredentialsRewriterAdapter, and it's properly defensive: passthrough when reuseAllegroConnectionId is absent, and it throws ConnectionCredentialsRewriteException on blank id / not-found / non-Allegro platformType / missing source client creds.

Security — verified end-to-end ✅

  • Secret never hits a response. @Roles('admin') @Put(':id/credentials')HttpStatus.NO_CONTENT (empty body); updateCredentials(): Promise<void> resolves the pair, merges it into stored credentials (credentials.update(ref, { credentialsJson })), and returns nothing. The raw clientSecret is read server-side → persisted → never serialized. Matches the architecture-overview baseline ("never return secrets/credentials in API responses").
  • reuseAllegroConnectionId is stripped by the rewriter and not persisted — only the resolved concrete allegroClientId/allegroClientSecret land in storage; the log line names the source connection but not the secret.
  • FE sends only { reuseAllegroConnectionId } on the reuse path (secret only on the manual path), and the panel test asserts the payload never contains the raw secret. No any anywhere in the diff.

🟢 One forward note (not a blocker) — the reuse surface is bounded only by admin + single-tenant

reuseAllegroConnectionId lets an admin copy any existing Allegro connection's secret into an Erli connection. The PR is honest about this: there's no tenant/org model, so @Roles('admin') + "must resolve to a real platformType:'allegro' connection" is the whole boundary. That's fine today — an admin already has full credential-management authority over every connection in a single-deployment instance, so this is not privilege escalation, and the existence + platform guard blocks garbage ids. The forward risk: when a tenant/organization model eventually lands, this rewriter needs an ownership check (source connection must belong to the caller's tenant), otherwise it becomes a cross-tenant credential-read vector. Worth a one-line TODO/ADR note in the adapter so it's not missed at that point — the JSDoc already flags the absence of tenancy, which is most of the way there.

Tests — strong ✅

Generic service delegation/passthrough/error-mapping (Allegro cases correctly moved out to the adapter spec); adapter spec's 7 cases cover success + every rejection branch (not-found, wrong-platform, missing creds, blank id, absent-passthrough); FE panel covers the zero-connections fallback, reuse radio + picker, the secret-free reuse payload, manual override, and reuse-without-selection validation.

Verdict

Approve — clean platform-neutral refactor of a real boundary violation, server-side secret handling with the correct HTTP contract (admin-only, 204, void), and thorough tests. mergeable_state: clean onto the epic branch. This closes out the #1381 Erli-Allegro epic (plan → client → wiring → FE → credential reuse). Only follow-up is the tenant-ownership check whenever multi-tenancy arrives.

@piotrswierzy
piotrswierzy merged commit 1b3f45c into epic/1381-erli-allegro-category-catalog Jul 8, 2026
1 check passed
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 8, 2026
…tegory access (#1387) (#1405)

Extends the #1384 Erli credentials panel with a reuse-vs-manual choice: when
the operator already has an Allegro connection, its clientId/clientSecret can
be copied server-side into the Erli connection instead of registering a
second Allegro app. The raw clientSecret never round-trips through the
browser.

The credential-copy logic is dispatched through a new, platform-neutral
ConnectionCredentialsRewriterPort + registry (mirroring the existing
ConnectionConfigShapeValidatorPort/ConnectionCredentialsShapeValidatorPort
pair) so the generic ConnectionService.updateCredentials stays unaware that
Allegro or Erli exist. The Erli-specific resolution lives entirely in
ErliAllegroCredentialsRewriterAdapter, registered from a companion
ErliCredentialsRewriterModule (mirrors ErliWebhookProvisioningModule) since
it needs ConnectionPort, which is intentionally outside the plugin-neutral
HostServices bag.

Closes #1387

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
piotrswierzy pushed a commit that referenced this pull request Jul 8, 2026
…t a required Allegro connection (#1407)

* feat(erli): Erli-owned Allegro client_credentials category-catalog client (#1388)

* feat(erli): add Erli-owned Allegro client_credentials category-catalog client

Adds AllegroCategoryCatalogClient, a self-contained HTTP client that lets
an Erli connection browse Allegro's public /sale/categories and
/sale/categories/{id}/parameters catalog via an Allegro app's
grant_type=client_credentials token, with no dependency on
@openlinker/integrations-allegro (per ADR-030) and no requirement for the
operator to own a real Allegro seller connection.

Extends ErliConnectionConfig with an optional allegroEnvironment field and
ErliCredentials with optional allegroClientId/allegroClientSecret fields
to carry the Allegro app credentials alongside Erli's own apiKey.

Part of #1381, sub-task 1 of 3 (see #1383, #1384).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpsVFTpKZ1HoowEUKzbnWu
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(erli): address PR #1388 review findings on AllegroCategoryCatalogClient

- Extract inline wire-shape types to allegro-category-catalog-client.types.ts,
  matching the erli-http-client.ts / erli-http-client.types.ts split.
- Add a single-flight guard around token acquisition so two concurrent
  fetchCategories/fetchCategoryParameters calls on a cold cache issue one
  grant_type=client_credentials request, not two.
- Treat a token response missing expires_in as already-expired instead of
  caching it indefinitely.
- Add tests for the 401/403 branch on a data call (categories/parameters),
  distinct from the token-endpoint rejection branch already covered.
- Add a cross-plugin mapper parity test suite that runs Allegro's real
  sandbox category-parameters fixture through this client's copy of
  toNeutralCategoryParameter, mirroring assertions from
  allegro-category-parameter.mapper.spec.ts, with cross-referencing comments
  in both spec files so a future mapper change prompts updating both.
- Replace the inline 'sandbox' | 'production' union with an as const +
  runtime-array AllegroCatalogEnvironment type in erli-connection.types.ts,
  matching AllegroConnectionConfig's own convention.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* feat(erli): wire per-connection Allegro category-catalog capability + credential/config validation (#1399)

* feat(erli): wire per-connection Allegro category-catalog capability + credential/config validation (#1383)

ErliOfferManagerAdapter now wires fetchCategories/fetchCategoryParameters as
optional instance properties (never a static implements clause) so
isCategoryBrowser/isCategoryParametersReader reflect whether a specific Erli
connection has configured a valid Allegro app credential pair, per ADR-031.
ErliAdapterFactory constructs the shared AllegroCategoryCatalogClient only
when both allegroClientId and allegroClientSecret resolve to non-empty
strings, resolving config.allegroEnvironment (default 'production').

The credentials shape validator now enforces "both or neither" for the
Allegro credential pair, and the config shape validator restricts
allegroEnvironment to 'sandbox' | 'production'.

Verified CategoriesCacheService and ListingsController need no changes: both
already resolve capabilities generically via getCapabilityAdapter + is* type
guards, so a misconfigured Erli connection behaves like today's
"adapter doesn't implement this capability" case.

Added an integration test exercising the real ErliAdapterFactory +
ErliOfferManagerAdapter + AllegroCategoryCatalogClient (fetch stubbed) through
the production adapter-resolution seam and the real HTTP endpoint, covering
configured / unconfigured / partially-configured connections. Confirmed the
existing #1367 bulk-wizard capability-gate test passes unmodified.

Part of #1381, sub-task 2 of 4 (depends on #1382, merged; see also #1384, #1387).

Signed-off-by: Norbert Kulus <42zeroo@gmail.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(erli): add allegroCategoryAccessEnabled config flag as the FE-visible signal

connection.supportedCapabilities turned out to be a static, per-adapterKey
manifest value rather than computed per-connection-instance, so it can't
distinguish an Erli connection with configured Allegro app credentials from
one without. Add a non-secret ErliConnectionConfig.allegroCategoryAccessEnabled
boolean for the frontend to read instead (see ADR-031 "Correction"); the
write path that sets allegroClientId/allegroClientSecret must set/clear this
flag atomically (tracked in #1384's scope).

Also fixes stale ADR-030 references (renumbered to ADR-031 to avoid a
collision with #1307) in allegro-category-catalog-client.ts and
erli-connection.types.ts.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(erli): hoist Allegro category-token cache to host.cache + dedupe credential resolve (#1399 review)

AllegroCategoryCatalogClient built a fresh in-memory token cache per adapter
instance, but ErliAdapterFactory builds a fresh instance per
getCapabilityAdapter call — so fetchCategoryParameters paid a full
client_credentials OAuth round-trip on every request. The client now
persists the acquired token in the optional CachePort (host.cache), keyed by
clientId, with a TTL matching the token's remaining lifetime, so it survives
across per-request instances and processes.

Also collapses ErliAdapterFactory.createAdapters's two credentialsResolver.get
calls (one for apiKey, one for the Allegro pair) into one shared resolve.

Addresses both IMPORTANT findings from the PR #1399 review round (Piotr's
token-cache note and the self-review's double-resolve finding).

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

---------

Signed-off-by: Norbert Kulus <42zeroo@gmail.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* feat(erli,web): Allegro credentials checkbox + offer wizard category/parameters steps (#1401)

* feat(erli,web): checkbox-reveal Allegro credentials panel + offer wizard category/parameters steps

Adds the operator-facing half of the Erli-owned Allegro category-catalog
feature (#1384, part of epic #1381, depends on #1382/#1383):

- ErliCredentialsPanel gains a "Browse Allegro categories when creating
  Erli offers" checkbox that reveals Client ID / Client Secret fields
  (masked, show/hide toggle). Saving sequences two existing mutations from
  one click: useUpdateConnectionCredentialsMutation (credentials pair,
  merged with apiKey) then useUpdateConnectionMutation (config patch for
  allegroCategoryAccessEnabled) — credentials first so a failure never
  leaves the flag advertising access the backend can't serve.

- ErliCreateOfferWizard renders the reused Allegro CategoryPicker +
  CategoryParametersStep (fed by the existing useCategoryParametersQuery)
  as dedicated Category / Category-parameters steps when
  connection.config.allegroCategoryAccessEnabled is true — per ADR-031's
  correction, this per-connection-instance config flag is the FE gating
  signal, not the static, per-adapterKey connection.supportedCapabilities.
  Falls back to today's plain-text field + a link to the connection's edit
  page otherwise. Stepper labels and needsProductParameters become
  capability-conditional; erliCreateOfferSchema gains a parameters slice
  serialized into overrides.parameters on submit (no BE change needed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpsVFTpKZ1HoowEUKzbnWu
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(erli,web): address PR #1401 review findings

- Gate `canSubmit` in ErliCredentialsPanel on `allegroEnabled` so
  unchecking the Allegro-access box after typing (but not saving)
  Client ID/Secret disables Save instead of showing a false-positive
  "Credentials saved" toast while discarding the typed secret.
- Drop the checkbox's invalid inline `accentColor: var(--accent)`
  (token doesn't exist) — `index.css` already applies
  `accent-color: var(--accent-primary)` globally.
- Restore category-parameter values on Erli offer retry by reusing
  the Allegro retry mapper's `readParameters` heuristic (exported)
  instead of always prefilling an empty `parameters` object — both
  platforms persist the same neutral `overrides.parameters` shape.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(erli,connections): merge credential rotation + enforce category pick (#1401 second review)

- ConnectionService.updateCredentials now merges the submitted payload onto
  the existing stored credentialsJson instead of replacing it wholesale, so
  rotating the plain Erli apiKey no longer silently wipes a previously
  configured allegroClientId/allegroClientSecret pair (and enabling Allegro
  access without touching apiKey no longer fails shape validation).
- ErliCreateOfferWizard now blocks Next on the Category step until a
  category is actually selected, mirroring AllegroCreateOfferWizard's
  required categoryId field, instead of silently falling through to
  Category-parameters/Review with an empty category.
- Carried over the approved mockup's helper copy under the Allegro Client ID
  / Client Secret fields and restored the checkbox's original hint text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* test(erli,docs): cover mutation-sequencing failure paths + close ADR-031 documentation gaps (#1401 third review)

- erli-credentials-panel.test.tsx: add regression tests asserting the config
  patch never fires when the credentials write rejects, and that a config-patch
  rejection after a successful credentials write leaves the flag/fields intact
  with a retryable inline error - the two claims the PR description makes about
  "the trickiest part of this issue" were previously unverified by the suite.
- ADR-031: add a second Correction noting the sequencing is two FE-orchestrated
  mutations, not a single backend write, since no endpoint accepts both the
  credential pair and the config flag together; also record the wizard's
  3-step-vs-5-step topology and the deferred credential-verification indicator
  as explicit, confirmed scope cuts rather than implicit deviations from the
  approved mockup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* feat(erli): reuse an existing Allegro connection's credentials for category access (#1387) (#1405)

Extends the #1384 Erli credentials panel with a reuse-vs-manual choice: when
the operator already has an Allegro connection, its clientId/clientSecret can
be copied server-side into the Erli connection instead of registering a
second Allegro app. The raw clientSecret never round-trips through the
browser.

The credential-copy logic is dispatched through a new, platform-neutral
ConnectionCredentialsRewriterPort + registry (mirroring the existing
ConnectionConfigShapeValidatorPort/ConnectionCredentialsShapeValidatorPort
pair) so the generic ConnectionService.updateCredentials stays unaware that
Allegro or Erli exist. The Erli-specific resolution lives entirely in
ErliAllegroCredentialsRewriterAdapter, registered from a companion
ErliCredentialsRewriterModule (mirrors ErliWebhookProvisioningModule) since
it needs ConnectionPort, which is intentionally outside the plugin-neutral
HostServices bag.

Closes #1387

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Signed-off-by: Norbert Kulus <42zeroo@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
norbert-kulus-blockydevs added a commit that referenced this pull request Jul 22, 2026
…t a required Allegro connection (#1407)

* feat(erli): Erli-owned Allegro client_credentials category-catalog client (#1388)

* feat(erli): add Erli-owned Allegro client_credentials category-catalog client

Adds AllegroCategoryCatalogClient, a self-contained HTTP client that lets
an Erli connection browse Allegro's public /sale/categories and
/sale/categories/{id}/parameters catalog via an Allegro app's
grant_type=client_credentials token, with no dependency on
@openlinker/integrations-allegro (per ADR-030) and no requirement for the
operator to own a real Allegro seller connection.

Extends ErliConnectionConfig with an optional allegroEnvironment field and
ErliCredentials with optional allegroClientId/allegroClientSecret fields
to carry the Allegro app credentials alongside Erli's own apiKey.

Part of #1381, sub-task 1 of 3 (see #1383, #1384).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpsVFTpKZ1HoowEUKzbnWu
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(erli): address PR #1388 review findings on AllegroCategoryCatalogClient

- Extract inline wire-shape types to allegro-category-catalog-client.types.ts,
  matching the erli-http-client.ts / erli-http-client.types.ts split.
- Add a single-flight guard around token acquisition so two concurrent
  fetchCategories/fetchCategoryParameters calls on a cold cache issue one
  grant_type=client_credentials request, not two.
- Treat a token response missing expires_in as already-expired instead of
  caching it indefinitely.
- Add tests for the 401/403 branch on a data call (categories/parameters),
  distinct from the token-endpoint rejection branch already covered.
- Add a cross-plugin mapper parity test suite that runs Allegro's real
  sandbox category-parameters fixture through this client's copy of
  toNeutralCategoryParameter, mirroring assertions from
  allegro-category-parameter.mapper.spec.ts, with cross-referencing comments
  in both spec files so a future mapper change prompts updating both.
- Replace the inline 'sandbox' | 'production' union with an as const +
  runtime-array AllegroCatalogEnvironment type in erli-connection.types.ts,
  matching AllegroConnectionConfig's own convention.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* feat(erli): wire per-connection Allegro category-catalog capability + credential/config validation (#1399)

* feat(erli): wire per-connection Allegro category-catalog capability + credential/config validation (#1383)

ErliOfferManagerAdapter now wires fetchCategories/fetchCategoryParameters as
optional instance properties (never a static implements clause) so
isCategoryBrowser/isCategoryParametersReader reflect whether a specific Erli
connection has configured a valid Allegro app credential pair, per ADR-031.
ErliAdapterFactory constructs the shared AllegroCategoryCatalogClient only
when both allegroClientId and allegroClientSecret resolve to non-empty
strings, resolving config.allegroEnvironment (default 'production').

The credentials shape validator now enforces "both or neither" for the
Allegro credential pair, and the config shape validator restricts
allegroEnvironment to 'sandbox' | 'production'.

Verified CategoriesCacheService and ListingsController need no changes: both
already resolve capabilities generically via getCapabilityAdapter + is* type
guards, so a misconfigured Erli connection behaves like today's
"adapter doesn't implement this capability" case.

Added an integration test exercising the real ErliAdapterFactory +
ErliOfferManagerAdapter + AllegroCategoryCatalogClient (fetch stubbed) through
the production adapter-resolution seam and the real HTTP endpoint, covering
configured / unconfigured / partially-configured connections. Confirmed the
existing #1367 bulk-wizard capability-gate test passes unmodified.

Part of #1381, sub-task 2 of 4 (depends on #1382, merged; see also #1384, #1387).

Signed-off-by: Norbert Kulus <42zeroo@gmail.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(erli): add allegroCategoryAccessEnabled config flag as the FE-visible signal

connection.supportedCapabilities turned out to be a static, per-adapterKey
manifest value rather than computed per-connection-instance, so it can't
distinguish an Erli connection with configured Allegro app credentials from
one without. Add a non-secret ErliConnectionConfig.allegroCategoryAccessEnabled
boolean for the frontend to read instead (see ADR-031 "Correction"); the
write path that sets allegroClientId/allegroClientSecret must set/clear this
flag atomically (tracked in #1384's scope).

Also fixes stale ADR-030 references (renumbered to ADR-031 to avoid a
collision with #1307) in allegro-category-catalog-client.ts and
erli-connection.types.ts.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(erli): hoist Allegro category-token cache to host.cache + dedupe credential resolve (#1399 review)

AllegroCategoryCatalogClient built a fresh in-memory token cache per adapter
instance, but ErliAdapterFactory builds a fresh instance per
getCapabilityAdapter call — so fetchCategoryParameters paid a full
client_credentials OAuth round-trip on every request. The client now
persists the acquired token in the optional CachePort (host.cache), keyed by
clientId, with a TTL matching the token's remaining lifetime, so it survives
across per-request instances and processes.

Also collapses ErliAdapterFactory.createAdapters's two credentialsResolver.get
calls (one for apiKey, one for the Allegro pair) into one shared resolve.

Addresses both IMPORTANT findings from the PR #1399 review round (Piotr's
token-cache note and the self-review's double-resolve finding).

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

---------

Signed-off-by: Norbert Kulus <42zeroo@gmail.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* feat(erli,web): Allegro credentials checkbox + offer wizard category/parameters steps (#1401)

* feat(erli,web): checkbox-reveal Allegro credentials panel + offer wizard category/parameters steps

Adds the operator-facing half of the Erli-owned Allegro category-catalog
feature (#1384, part of epic #1381, depends on #1382/#1383):

- ErliCredentialsPanel gains a "Browse Allegro categories when creating
  Erli offers" checkbox that reveals Client ID / Client Secret fields
  (masked, show/hide toggle). Saving sequences two existing mutations from
  one click: useUpdateConnectionCredentialsMutation (credentials pair,
  merged with apiKey) then useUpdateConnectionMutation (config patch for
  allegroCategoryAccessEnabled) — credentials first so a failure never
  leaves the flag advertising access the backend can't serve.

- ErliCreateOfferWizard renders the reused Allegro CategoryPicker +
  CategoryParametersStep (fed by the existing useCategoryParametersQuery)
  as dedicated Category / Category-parameters steps when
  connection.config.allegroCategoryAccessEnabled is true — per ADR-031's
  correction, this per-connection-instance config flag is the FE gating
  signal, not the static, per-adapterKey connection.supportedCapabilities.
  Falls back to today's plain-text field + a link to the connection's edit
  page otherwise. Stepper labels and needsProductParameters become
  capability-conditional; erliCreateOfferSchema gains a parameters slice
  serialized into overrides.parameters on submit (no BE change needed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpsVFTpKZ1HoowEUKzbnWu
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(erli,web): address PR #1401 review findings

- Gate `canSubmit` in ErliCredentialsPanel on `allegroEnabled` so
  unchecking the Allegro-access box after typing (but not saving)
  Client ID/Secret disables Save instead of showing a false-positive
  "Credentials saved" toast while discarding the typed secret.
- Drop the checkbox's invalid inline `accentColor: var(--accent)`
  (token doesn't exist) — `index.css` already applies
  `accent-color: var(--accent-primary)` globally.
- Restore category-parameter values on Erli offer retry by reusing
  the Allegro retry mapper's `readParameters` heuristic (exported)
  instead of always prefilling an empty `parameters` object — both
  platforms persist the same neutral `overrides.parameters` shape.

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* fix(erli,connections): merge credential rotation + enforce category pick (#1401 second review)

- ConnectionService.updateCredentials now merges the submitted payload onto
  the existing stored credentialsJson instead of replacing it wholesale, so
  rotating the plain Erli apiKey no longer silently wipes a previously
  configured allegroClientId/allegroClientSecret pair (and enabling Allegro
  access without touching apiKey no longer fails shape validation).
- ErliCreateOfferWizard now blocks Next on the Category step until a
  category is actually selected, mirroring AllegroCreateOfferWizard's
  required categoryId field, instead of silently falling through to
  Category-parameters/Review with an empty category.
- Carried over the approved mockup's helper copy under the Allegro Client ID
  / Client Secret fields and restored the checkbox's original hint text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

* test(erli,docs): cover mutation-sequencing failure paths + close ADR-031 documentation gaps (#1401 third review)

- erli-credentials-panel.test.tsx: add regression tests asserting the config
  patch never fires when the credentials write rejects, and that a config-patch
  rejection after a successful credentials write leaves the flag/fields intact
  with a retryable inline error - the two claims the PR description makes about
  "the trickiest part of this issue" were previously unverified by the suite.
- ADR-031: add a second Correction noting the sequencing is two FE-orchestrated
  mutations, not a single backend write, since no endpoint accepts both the
  credential pair and the config flag together; also record the wizard's
  3-step-vs-5-step topology and the deferred credential-verification indicator
  as explicit, confirmed scope cuts rather than implicit deviations from the
  approved mockup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* feat(erli): reuse an existing Allegro connection's credentials for category access (#1387) (#1405)

Extends the #1384 Erli credentials panel with a reuse-vs-manual choice: when
the operator already has an Allegro connection, its clientId/clientSecret can
be copied server-side into the Erli connection instead of registering a
second Allegro app. The raw clientSecret never round-trips through the
browser.

The credential-copy logic is dispatched through a new, platform-neutral
ConnectionCredentialsRewriterPort + registry (mirroring the existing
ConnectionConfigShapeValidatorPort/ConnectionCredentialsShapeValidatorPort
pair) so the generic ConnectionService.updateCredentials stays unaware that
Allegro or Erli exist. The Erli-specific resolution lives entirely in
ErliAllegroCredentialsRewriterAdapter, registered from a companion
ErliCredentialsRewriterModule (mirrors ErliWebhookProvisioningModule) since
it needs ConnectionPort, which is intentionally outside the plugin-neutral
HostServices bag.

Closes #1387

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>

---------

Signed-off-by: norbert-kulus-blockydevs <norbert.kulus@blockydevs.com>
Signed-off-by: Norbert Kulus <42zeroo@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants