first commit
This commit is contained in:
84
docs/api-stack/api/api-saga/src/tenant/cross-tenant.md
Normal file
84
docs/api-stack/api/api-saga/src/tenant/cross-tenant.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# `tenant/cross-tenant`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Design doc §9.1 (final paragraph) requires that a saga step whose
|
||||
target tenant differs from the saga initiator's tenant be gated by
|
||||
the `saga.cross-tenant` capability on the owner plugin. Without the
|
||||
capability the orchestrator MUST refuse dispatch with
|
||||
`SAGA.CROSS_TENANT_DENIED` — quietly running the step is a tenant
|
||||
isolation violation.
|
||||
|
||||
This module exports the canonical capability id and two helpers the
|
||||
orchestrator (and any saga aware middleware) funnels every dispatch
|
||||
through. The check is pure and side-effect-free so it can be applied
|
||||
at registry register time, at orchestrator start time, and at every
|
||||
step dispatch — three checkpoints that catch wiring drift at three
|
||||
levels of granularity.
|
||||
|
||||
## Features
|
||||
|
||||
- `CROSS_TENANT_CAPABILITY` — the literal `"saga.cross-tenant"`
|
||||
capability id, pinned in the `@suaveplan/api-types` capabilities
|
||||
table.
|
||||
- `hasCrossTenantCapability(owner)` — predicate for non-throwing
|
||||
branches (e.g. when computing UI affordances).
|
||||
- `assertCrossTenantAllowed(owner, initiator, target)` — throws
|
||||
`SagaError(SAGA.CROSS_TENANT_DENIED)` when the gate refuses; no-op
|
||||
otherwise.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
assertCrossTenantAllowed,
|
||||
CROSS_TENANT_CAPABILITY,
|
||||
} from "@suaveplan/api-saga/tenant/cross-tenant";
|
||||
|
||||
assertCrossTenantAllowed(owner, "acme", "acme"); // same-tenant — ok
|
||||
assertCrossTenantAllowed(owner, "acme", "globex"); // throws unless owner declares capability
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Compose the gate with the cross-cutting capability checker used
|
||||
elsewhere in the host — `hasCrossTenantCapability` is non-throwing so
|
||||
it composes inside conditional pipelines:
|
||||
|
||||
```ts
|
||||
import { hasCrossTenantCapability } from "@suaveplan/api-saga";
|
||||
|
||||
const canDispatch =
|
||||
initiatorTenantId === targetTenantId ||
|
||||
hasCrossTenantCapability(owner);
|
||||
|
||||
if (!canDispatch) {
|
||||
metrics.crossTenantDeniedTotal.add(1, {
|
||||
saga: owner.saga.name,
|
||||
owner: owner.pluginName,
|
||||
});
|
||||
throw new SagaError(SAGA_CODES.CROSS_TENANT_DENIED, "denied");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CROSS_TENANT_CAPABILITY: "saga.cross-tenant"` — literal constant.
|
||||
- `hasCrossTenantCapability(owner: RegisteredSagaOwner): boolean`.
|
||||
- `assertCrossTenantAllowed(owner, initiatorTenantId,
|
||||
targetTenantId): void` — no-op on accept, throws
|
||||
`SAGA.CROSS_TENANT_DENIED` on refuse.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The capability is owner-scoped rather than step-scoped because the
|
||||
owner declares both. Per-step opt-outs would require manifest changes
|
||||
to `sagas.owns[].steps[]` and threading a per-step capability list
|
||||
through the orchestrator — a strictly larger surface than the design
|
||||
doc calls for. If a future change needs finer granularity, the
|
||||
registry's `RegisteredSagaOwner.saga.capabilities` shape is the
|
||||
forward-compatible extension point.
|
||||
Reference in New Issue
Block a user