Skip to content

feat: SCAPI migration for jobs, code, bm, sites, site-archive, and CAP#413

Draft
clavery wants to merge 22 commits into
mainfrom
feature/scapi-migration
Draft

feat: SCAPI migration for jobs, code, bm, sites, site-archive, and CAP#413
clavery wants to merge 22 commits into
mainfrom
feature/scapi-migration

Conversation

@clavery

@clavery clavery commented May 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds SCAPI Admin API support across the job, code, bm users, bm roles, and sites command families, plus site-archive import/export and CAP install/uninstall — each with automatic fallback to OCAPI for environments where SCAPI scopes aren't provisioned. SCAPI is now the primary path; OCAPI remains as a transitional, deprecated fallback.

In auto mode (default), the CLI prefers SCAPI when shortCode and tenantId are configured and the auth flow can request scopes. On invalid_scope from Account Manager (or a rejected SCAPI request), it silently falls back to OCAPI. On instances where OCAPI itself is disabled, commands surface an actionable error naming the exact SCAPI scope required.

Behavioral guarantee

No CLI-visible changes for any existing setup:

  • OCAPI-only configurations (no shortCode/tenantId): identical behavior.
  • --api-backend ocapi: OCAPI runs unconditionally.
  • --api-backend scapi: SCAPI runs; clear error if scopes/coordinates are missing or the auth flow can't request scopes.
  • auto with full SCAPI scopes: SCAPI runs and stays.
  • auto with no SCAPI scopes: cached OCAPI fallback. Output, JSON shape, and exit codes match prior behavior.

What's covered by SCAPI (with OCAPI fallback)

  • job run / execution search / wait / log, and system-job triggers for site-import / site-export and cap install / cap uninstall (archive transfer still uses WebDAV).
  • code list/activate/delete/reload (--reload toggles activation via whichever backend is selected) plus code deploy code-version management and all VS Code extension code-version actions.
  • bm users and bm roles CRUD (bm users update --disabled transparently uses OCAPI in auto mode — SCAPI Users PATCH lacks the flag).
  • sites list and sites cartridges list reads over the SCAPI site/sites API. Cartridge-path writes (add/remove/set) have no SCAPI equivalent and remain OCAPI / site-archive import.

What's new

  • b2c job execution delete — SCAPI-only (no OCAPI equivalent).
  • --api-backend flag on instance commands (auto | scapi | ocapi), also honored from apiBackend in dw.json and SFCC_API_BACKEND (CLI and SDK consumers, e.g. the VS Code extension).
  • Required SCAPI scopes (rw + ro tier; auth tries rw first, falls to ro for reads): sfcc.jobs(.rw), sfcc.scripts(.rw), sfcc.users(.rw), sfcc.roles(.rw), sfcc.sites(.rw).
  • OCAPI deprecation detectionOcapiDeprecatedException (403) is caught and turned into an actionable message naming the SCAPI scope the operation needs.

Architecture highlights

  • B2CInstance is the single source of SCAPI client config. A B2CInstance.scapiClientConfig getter returns {shortCode, tenantId, auth} (or undefined when the instance can't reach SCAPI), and B2CInstance.apiBackend exposes the preference. The dual-backend factories now take just {instance}. This is the forward-looking seam for the OCAPI→SCAPI transition: SCAPI operations need nothing beyond a configured instance. Only stateless, scope-flexible OAuth (client-credentials or JWT Bearer) qualifies for SCAPI — implicit/stateful flows hold a fixed-scope token and use OCAPI (this holds for explicit --api-backend scapi too, which fails with a clear error rather than an under-scoped token).
  • Scope cascadeAuthStrategy.getAccessTokenForCascade(candidates) + createScapiAuthMiddleware pick the rw/ro tier per operation and cache the surviving token.
  • runSystemJob — shared dual-backend runner for site-archive/CAP system jobs. Preserves the raw OCAPI JobExecution result/error contract across backends (via mapCanonicalToOcapiExecution), and only falls back to OCAPI when the SCAPI start provably created no job — an ambiguous failure (network drop, timeout, 5xx) propagates rather than risk re-running a mutating job.
  • BackendDispatcher (src/compat/dispatcher.ts) — transitional CLI-side router; deleted once OCAPI is removed.

Correctness fixes from review

An adversarial review of the migration surfaced five confirmed issues, all fixed with tests:

  • Double-execution guard: runSystemJob no longer re-runs a mutating system job over OCAPI after an ambiguous SCAPI failure.
  • Cascade token invalidation: a 401 retry now re-requests from AM instead of reusing the rejected merged-scope token.
  • Sites pagination: sites list now pages through all sites (was capped at the first 25) with bounded-concurrency enrichment.
  • Explicit-SCAPI error clarity and SFCC_API_BACKEND wiring for SDK consumers.
  • SCAPI-first documentation and skills; OCAPI presented as the deprecated fallback.

Test plan

  • pnpm run typecheck:agent clean across packages
  • pnpm run lint:agent clean across packages
  • pnpm --filter @salesforce/b2c-tooling-sdk run test:agent (1889 passing)
  • pnpm --filter @salesforce/b2c-cli run test:agent (1354 passing)
  • pnpm run docs:build succeeds
  • Manual: b2c job run my-job --wait against instances with and without SCAPI scopes (verify silent OCAPI fallback)
  • Manual: b2c site-import / b2c site-export and b2c cap install / b2c cap uninstall over SCAPI (system-job trigger not yet live-verified end-to-end)
  • Manual: b2c sites list and b2c sites cartridges list against a multi-site instance (>25 sites)
  • Manual: b2c code list/activate --reload, b2c bm users list, b2c bm roles list against both kinds of instances
  • Manual: explicit --api-backend scapi and --api-backend ocapi; OCAPI-disabled instance surfaces the scope-named deprecation error

Follow-ups (not in this PR)

  • Live end-to-end verification that SCAPI accepts triggering the site-archive and CAP system jobs by ID.
  • Migrate code, bm users, bm roles from the legacy ScopeTierManager pattern onto the dispatcher + cascade pattern; then delete ScopeTierManager.
  • Upgrade the OCAPI-fallback log line from debug to warn to nudge SCAPI provisioning.

clavery added 22 commits May 8, 2026 12:22
Introduces a JobsBackend interface so job commands can transparently
use either OCAPI or SCAPI. Auto mode prefers SCAPI when shortCode and
tenantId are configured, falling back to OCAPI on invalid_scope errors.

- New SCAPI Jobs client (operation/jobs/v1) with optimistic sfcc.jobs.rw
  scope and read-only downgrade for read operations
- Canonical JobExecutionResult type bridges OCAPI snake_case and SCAPI
  camelCase response shapes
- --api-backend flag and apiBackend dw.json field for explicit control
- New job execution delete command (SCAPI only)
- job:run, job:search, job:wait, job:log migrated to backend abstraction
- job:import and job:export remain OCAPI-only for now
# Conflicts:
#	packages/b2c-cli/src/commands/job/run.ts
#	packages/b2c-cli/src/commands/job/search.ts
#	packages/b2c-tooling-sdk/src/config/dw-json.ts
#	packages/b2c-tooling-sdk/src/config/mapping.ts
Pulls three domain-agnostic utilities out of jobs into shared modules
ahead of applying the same pattern to scripts, users, and roles:

- isInvalidScopeError, ApiBackendPreference, resolveScapiOrOcapi —
  scope-error detection and preference resolution
- ScapiFallbackBackend<T> — generic fallback wrapper that tries SCAPI
  first and falls back to OCAPI on invalid_scope
- ScopeTierManager<C> — manages dual rw/read-only client tiers with
  optimistic rw + downgrade on scope error

Refactors ScapiJobsBackend and FallbackJobsBackend to use the new
utilities without behavior change. Removes the unused
downgradeToReadOnly() method and confusing tier-resolution logic.
Migrates code list/activate/delete commands to use the dual-backend
pattern. New CodeCommand base class exposes createScriptsBackend(),
which selects between OCAPI and SCAPI based on --api-backend.

- New SCAPI Scripts client (dx/scripts/v1) reusing the shared
  ScopeTierManager and ScapiFallbackBackend utilities
- ScriptsBackend interface with canonical CodeVersionInfo shape
  (camelCase, _raw escape hatch)
- reloadCodeVersion remains OCAPI-only — SCAPI backend throws,
  auto mode falls back to OCAPI on the first reload call
Migrates bm users list/get/update/delete commands to the dual-backend
pattern. New BmCommand base class exposes createUsersBackend(),
which selects between OCAPI and SCAPI based on --api-backend.

- New SCAPI Merchant Users client (merchant/users/v1)
- UsersBackend interface with canonical UserInfo (camelCase)
- bm users search, bm whoami, bm access-key * stay OCAPI-only —
  no SCAPI equivalents
- SCAPI updateUser does not support `disabled`; the SCAPI backend
  throws when --disabled is passed, prompting the user to use OCAPI
Migrates bm roles list/get/create/delete/grant/revoke and
bm roles permissions get/set commands to the dual-backend pattern.
BmCommand now exposes createRolesBackend() alongside createUsersBackend().

- New SCAPI Merchant Roles client (merchant/roles/v1)
- RolesBackend with canonical RoleInfo and RolePermissionsInfo
  (camelCase; OCAPI mapping converts snake_case fields like
  locale_id → localeId)
- Permissions display updated to use canonical camelCase fields
- Bm roles get --expand users continues to work via the _raw
  escape hatch (handles both OCAPI snake_case and SCAPI camelCase
  user fields)
- Configuration guide: clarify api-backend applies to job, code,
  bm users, and bm roles commands
- code.md: new "API Backend" section with SCAPI scopes, fallback
  behavior, and notes on reload/deploy/download/watch staying OCAPI/WebDAV
- bm.md: new "API Backend" section with per-command compatibility
  table (users search, whoami, access-key remain OCAPI-only)
- b2c-code skill: backend selection examples
- b2c-bm-users-roles skill: backend selection notes including the
  --disabled fallback caveat
- Single changeset replaces the jobs-only one
Three correctness fixes plus four DRY extractions across the SCAPI
migration. Tests stay green (1722 SDK + 1219 CLI) and the API surface
is unchanged for consumers.

Bugs fixed:

- code activate --reload now works in auto mode. The reload toggle
  (list + activate(alt) + activate(target)) is implementable on any
  backend, so reloadCodeVersion is a backend-agnostic free function
  that takes a ScriptsBackend. The OCAPI-only stub in
  ScapiScriptsBackend that previously broke fallback is gone.

- job run --body is no longer subject to a special-case backend switch.
  SCAPI accepts raw bodies for system jobs (just with a slightly
  different payload shape) so we pass --body through to whichever
  backend the user picked. Removes a no-op resolveBackend helper that
  called createJobsBackend twice.

- Scope merging now works for any AuthStrategy. The instanceof
  OAuthStrategy check silently dropped scopes for ImplicitOAuthStrategy
  and StatefulOAuthStrategy. AuthStrategy gains an optional
  withAdditionalScopes; a new withScopes() helper centralizes the
  method-presence check across all SCAPI client factories.

DRY extractions:

- Fallback*Backend subclasses (jobs, scripts, users, roles) replaced
  with a single Proxy-based createFallbackBackend<T>(). Each domain
  shed ~25 lines of mechanical method delegation. The proxy traps
  reads of `name` and routes method calls through the same
  withFallback logic.

- create*Backend factory functions (jobs, scripts, users, roles)
  collapsed into createDualBackend<T>() that takes constructors. Each
  domain backend.ts shrunk from ~50 lines to ~10.

- createScapi*Client factories collapsed into buildScapiClient<P>()
  that takes a path segment, scope set, and middleware key. Each
  domain client.ts shrunk from ~60 lines to ~30.

- InstanceCommand gained createBackend<T>() so per-domain command
  base classes (JobCommand/CodeCommand/BmCommand) shrink to one-line
  wrappers.

Other:

- Renamed JobExecutionResult to JobExecutionInfo for consistency with
  CodeVersionInfo, UserInfo, RoleInfo across the canonical types.
Adds 8 unit tests for createFallbackBackend covering:
- happy path (SCAPI works, choice is cached)
- fallback path (invalid_scope triggers OCAPI, choice is cached)
- name reflects the resolved backend
- non-fallback errors are rethrown without falling back
- multi-arg method dispatch
- non-method property access (documented as SCAPI-target-only)

Tightens the JSDoc on createFallbackBackend to spell out the contract
explicitly: both backends must implement T (TypeScript enforces this
at the call site), only methods are routed through fallback, non-method
properties stay on the SCAPI target, and concurrent first-calls are
benign since they only retry SCAPI redundantly.
The hostile review surfaced two real type-safety gaps in the dual-backend
pattern. Both are fixed by making interface-level capability explicit
rather than relying on runtime throws.

1. RoleInfo.permissions silently dropped after fallback. The OCAPI role
   mapper omitted permissions; SCAPI included them. Both satisfied the
   optional `permissions?` field, so TypeScript and the Proxy were happy
   while data quietly disappeared on the fallback path. Fix: map OCAPI's
   permissions through the existing snake_case → camelCase converter so
   getRole returns the same shape from both backends.

2. JobsBackend.deleteJobExecution was a runtime-throwing stub on OCAPI.
   Auto-mode behavior was unstable: it worked on a SCAPI-resolved
   backend, threw on an OCAPI-resolved one. Fix: split capability into
   DeletableJobsBackend (extends JobsBackend), which only ScapiJobsBackend
   implements. A supportsDeleteJobExecution() type guard lets callers
   narrow before calling. The Fallback Proxy detects SCAPI-only methods
   (those missing on OCAPI) and routes them directly to SCAPI without
   attempting fallback — invalid_scope errors propagate to the caller
   instead of trying an OCAPI that can't handle the operation.

The job execution delete command now uses the type guard and gives a
clear error message when the active backend can't delete.

Adds 2 fallback-backend tests covering SCAPI-only method dispatch.
1732 SDK + 1219 CLI tests passing.
Replaces the layered backend abstraction (interface + adapter classes +
Proxy fallback wrapper + ScopeTierManager) for the jobs domain with a
single CLI-side dispatcher and free-function SCAPI ops. The dispatcher's
sole purpose is caching the resolved backend across multi-call
operations in apiBackend=auto mode, so a polling command (job run --wait)
doesn't re-probe SCAPI on every iteration when the user has no SCAPI
scopes provisioned.

Auth changes:
- AuthStrategy gains optional getAccessTokenForCascade(candidates).
  OAuthStrategy and JwtOAuthStrategy walk candidates in order; first
  that AM accepts wins, cached per requested scope set.
- findCachedTokenSatisfying scans the cache for a non-expired token
  whose scopes are a superset of a required set, so a previously
  granted broader-scope token can serve a narrower request without
  another AM round trip.
- 5 new unit tests exercise cascade resolution, cache reuse, and error
  paths.

Client/middleware changes:
- buildScapiClient gains scopeCascade option and a new
  createScapiAuthMiddleware that reads the per-request
  x-b2c-scope-mode header and asks the strategy to resolve the chosen
  cascade tier (read or write). Legacy defaultScopes still works for
  scripts/users/roles until they migrate.
- SCAPI Jobs declares its cascade once at client construction:
  read = [['sfcc.jobs.rw'], ['sfcc.jobs']], write = [['sfcc.jobs.rw']].

Jobs domain:
- ScapiJobsBackend / OcapiJobsBackend / FallbackJobsBackend / DeletableJobsBackend
  all deleted. ScopeTierManager dependency removed from jobs.
- New scapi-ops.ts exports free functions (scapiExecuteJob, scapiGetJobExecution,
  scapiSearchJobExecutions, scapiDeleteJobExecution, scapiGetJobLog) that
  take a ScapiJobsClient and declare scope mode via headers.
- mapOcapiExecution / mapOcapiSearchResult exposed as transitional helpers
  for the OCAPI dispatcher branches.
- waitForJobExecution rewritten to take a getter callback over the
  canonical JobExecutionInfo shape.
- New CanonicalJobExecutionError carries JobExecutionInfo (was raw OCAPI),
  fixing a regression where SCAPI --wait failures reported 'ERROR'
  instead of the real exit code.

CLI:
- BackendDispatcher moved to src/compat/dispatcher.ts behind a new
  ./compat package export. Re-exported from ./cli for ergonomics.
  runScapiOnly removed; SCAPI-only commands branch on apiBackendPreference
  + buildScapiJobsClient directly.
- JobCommand exposes createJobsDispatcher, buildScapiJobsClient, and
  showJobLog (canonical-first, raw-OCAPI accepted for legacy callers).
- All five job commands rewritten to dispatcher.run({scapi, ocapi})
  with the scapi branch receiving a typed ScapiJobsClient.

Behavioral guarantee: no CLI-visible changes. OCAPI-only setups, explicit
--api-backend ocapi/scapi, and apiBackend=auto with full SCAPI scopes
all behave identically to the prior implementation. The auto + missing
SCAPI scopes path now caches the OCAPI choice across polls instead of
re-probing AM on every call.

13 dispatcher unit tests + 5 cascade unit tests + updated CLI command
tests. 1746 SDK + 1220 CLI tests passing.
# Conflicts:
#	packages/b2c-cli/test/commands/job/run.test.ts
#	packages/b2c-tooling-sdk/package.json
reloadCodeVersion now takes a ScriptsBackend (was B2CInstance). Wrap the
extension's instance in OcapiScriptsBackend at the two call sites so the
extension keeps its OCAPI-only behavior while satisfying the new type.

Mirrors how b2c-cli/src/commands/code/deploy.ts:207 already handles this.
# Conflicts:
#	packages/b2c-tooling-sdk/src/cli/job-command.ts
#	packages/b2c-tooling-sdk/src/operations/jobs/index.ts
#	packages/b2c-vs-extension/src/code-sync/deploy-command.ts
#	packages/b2c-vs-extension/src/code-sync/index.ts
OCAPI is deprecated and disabled on newer instances; the CLI previously
surfaced its 403 OcapiDeprecatedException as an opaque 'Failed to ...'
error with no guidance. Add a central detector + actionable error in
error-utils (isOcapiDeprecatedFault, OcapiDeprecatedError,
throwOcapiError) and route every OCAPI-terminal site through it (code
versions, jobs run/site-archive, cap install/uninstall/list, scaffold,
sites list).

Also fixes a credential leak: B2C embeds the bearer JWT in some auth
faults (InvalidAccessTokenException); getApiErrorMessage now redacts
tokens from every user-facing message.

Rewrite code/job/bm docs and agent skills SCAPI-first, presenting OCAPI
as the deprecated fallback rather than the assumed baseline.

Also fixes a pre-existing prettier error in b2c-dx-mcp diagnostics.
… redaction

Remove redactTokens: the leaked token is our own, echoed by the remote
OCAPI server only on the deprecated path, so scrubbing it isn't worth
the machinery. getApiErrorMessage is back to a pure extractor.

The OCAPI-deprecation message now names the exact SCAPI scope the failed
operation needs (e.g. 'sfcc.scripts' or 'sfcc.scripts.rw'), via an
optional requiredScopes arg threaded through throwOcapiError /
OcapiDeprecatedError. Scope constants are sourced from each domain's
canonical definitions (derived from the jobs cascade where applicable)
so they can't drift. Operations with no SCAPI equivalent (sites,
bm whoami/search/access-key, cap) keep the generic 'sfcc.* scopes'
wording. Routed bm users/roles and the cartridge-path read through the
shared helper so the dual-backend fallbacks surface the guidance too.
Migrate `sites list` and `sites cartridges list` (reads) to a dual backend
that prefers the SCAPI site/sites API when shortCode/tenantId/sfcc.sites scopes
are configured, falling back to the deprecated OCAPI Data API otherwise.

- New SCAPI sites client (site/sites/v1) with read/write scope cascade
- ScapiSitesBackend enriches the id-only list response via per-site getSite
- OcapiSitesBackend preserves the legacy /sites path and surfaces an
  OcapiDeprecatedError naming the sfcc.sites scopes on deprecated instances
- Cartridge-path writes have no SCAPI equivalent and stay on OCAPI / archive
- Docs and the b2c-sites skill are now SCAPI-first
B2CInstance now encodes the SCAPI connection coordinates and backend
preference, so SCAPI operations need nothing beyond a configured instance.
This is the forward-looking seam for the OCAPI -> SCAPI transition: when
OCAPI is eventually removed, the OCAPI accessors disappear and the SCAPI
plumbing stands on its own.

- InstanceConfig gains shortCode/tenantId/apiBackend; populated in
  createInstanceFromConfig from resolved config.
- AuthConfig.oauth gains jwtCertPath/jwtKeyPath/jwtPassphrase so the instance
  can build a JWT Bearer strategy for SCAPI, not just client-credentials.
- New B2CInstance.scapiClientConfig getter returns {shortCode, tenantId, auth}
  or undefined, encapsulating the "only stateless scope-flexible OAuth
  qualifies for auto-SCAPI" eligibility rule in one place. New apiBackend
  getter exposes the preference.
- createDualBackend / DualBackendConfig drop the threaded
  shortCode/tenantId/auth fields and source them from instance.scapiClientConfig;
  preference defaults to instance.apiBackend.
- InstanceCommand.createBackend + hasScapiConfig, JobCommand.buildScapiJobsClient,
  the sites CLI commands, and the VS Code scripts-backend builder all simplify
  to pass just the instance. This also fixes a latent issue where the CLI auth
  path could hand a fixed-scope stateful-session token to the SCAPI leg.
- Adds B2CInstance.scapiClientConfig unit coverage.
…fallback

Site archive import/export and CAP install/uninstall were OCAPI-only because
they POSTed directly to /jobs/{id}/executions. With B2CInstance now carrying
SCAPI config, route them through a shared dual-backend runner.

- New operations/jobs/run-system-job.ts: runSystemJob(instance, spec) starts a
  named system job, waits, and returns the raw OCAPI JobExecution. In auto mode
  it tries SCAPI (instance.scapiClientConfig) and falls back to OCAPI only if
  the *start* is rejected — once a job has started it never falls back, so a
  write is never re-run. Honors apiBackend ocapi/scapi/auto.
- The SCAPI JobExecutionRequest accepts the same {parameters:[{name,value}]}
  shape the OCAPI internal-user retry already used, so each operation declares
  one parameters array reused by both backends.
- New mapCanonicalToOcapiExecution reverse mapper keeps the public result/error
  contract identical across backends: every consumer reads raw snake_case off
  result.execution, and a SCAPI job failure is re-thrown as the raw
  JobExecutionError so existing log-fetch handling works unchanged.
- site-archive import/export and cap install/uninstall rewired to runSystemJob;
  log-fetch-on-failure factored into a shared helper. OCAPI behavior (shorthand
  body + UnknownPropertyException params retry) preserved exactly.
- Adds runSystemJob tests (SCAPI path, raw-shape contract, no-fallback-after-
  start, auto fallback, OCAPI default, params retry, deprecation, explicit
  preference). Existing site-archive + CAP tests pass unchanged.

Not yet live-verified end-to-end (deferred): auto mode keeps working via OCAPI
fallback if SCAPI rejects a system-job start.
Five confirmed findings from an adversarial review of the migration work,
verified against the code before fixing.

F1 (data integrity): runSystemJob no longer re-runs a mutating system job over
OCAPI after an ambiguous SCAPI failure. executeJob now throws a typed
ScapiJobStartError carrying the HTTP status; auto-mode fallback is gated on
isSafeStartFallback — invalid_scope, ScapiCapabilityUnsupportedError, or a
client-side rejection status (400/401/403/404/405/406/415). Network/timeout and
5xx/429 propagate without a re-run (job may have started).

F3 (auth): OAuthStrategy/JwtOAuthStrategy invalidateToken() now clears ALL
cached tokens for the client/method/AM-host identity, not just the base-scope
key. A cascade 401 retry previously reused the rejected token cached under the
merged-scope key; it now re-requests from AM.

F2 (correctness): ScapiSitesBackend.listSites paginates server-side (limit 50,
offset) honoring start/count, instead of reading only the first 25-item page;
per-site enrichment now uses bounded concurrency (5) and skips already-rich
items rather than an unbounded Promise.all.

F4 (clarity): explicit --api-backend scapi with implicit/stateful auth now
fails with a message naming the real requirement (a stateless client-credentials
or JWT flow), via shared scapiUnavailableMessage; scapiClientConfig JSDoc and
the changeset no longer imply explicit opt-in works for those flows.

F5 (docs/config): SFCC_API_BACKEND now maps in EnvSource (with value
validation) so the VS Code extension honors it; corrected stale OCAPI-only
claims in jobs.md, code.md (--reload is backend-agnostic), auth.md (Sites reads
use SCAPI), the b2c-job/b2c-code skills, and the authentication guide.

Removes now-dead invalidateCachedOAuthToken. Adds tests for all fixes.
@clavery clavery changed the title feat: SCAPI migration for jobs, code, and bm users/roles commands feat: SCAPI migration for jobs, code, bm, sites, site-archive, and CAP Jul 15, 2026
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.

1 participant