@fabricorg/platform-host 7.1.2 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +9 -0
- package/dist/index.cjs +196 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -1
- package/dist/index.d.ts +63 -1
- package/dist/index.js +195 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @fabricorg/platform-host
|
|
2
2
|
|
|
3
|
+
## 7.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 715e920: `tenantIsolationChecks()` and `runTenantIsolationChecks(subject)`: six checks proving that scope is a key on every read, write, claim, and completion. An invocation cannot be read, executed, or completed by callback under another tenant's scope; the same idempotency key in two tenants is two invocations; a tenant's event stream carries no other tenant's events; a tenant-scoped worker claims nothing of another's; and, when the subject lends a governed read, a viewer in the other tenant sees none of ours. Each check carries a positive control, so a store or host that does nothing fails it, and each has a teeth test against the flaw it names. The claim check uses a probe space of its own and retires its probe row, so a backlog cannot hide a tenant-ignoring claim and a live worker never runs the probe. Rows a lent read returns must carry their `tenantId`.
|
|
8
|
+
|
|
3
9
|
## 7.1.2
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -300,6 +300,15 @@ migration 3 adds the `pending_completion`, `external_completion`, and `completio
|
|
|
300
300
|
a partial index on the due instant the overdue sweep uses; the due instant is a real column because a
|
|
301
301
|
cast in an index expression is not immutable and PostgreSQL refuses it.
|
|
302
302
|
|
|
303
|
+
## Tenant isolation
|
|
304
|
+
|
|
305
|
+
`tenantIsolationChecks()` and `runTenantIsolationChecks(subject)` prove that scope is a key on every
|
|
306
|
+
read, write, claim, and completion: an invocation is invisible, inert, and uncompletable under another
|
|
307
|
+
tenant's scope; the same idempotency key in two tenants is two invocations; event streams carry only
|
|
308
|
+
their own tenant's events; a tenant-scoped worker claims nothing of another's; and a governed read
|
|
309
|
+
the subject lends shows the other tenant none of ours. The subject supplies a host, a recoverable
|
|
310
|
+
store, a registered action, and two scopes in different tenants.
|
|
311
|
+
|
|
303
312
|
## Runtime observability and health
|
|
304
313
|
|
|
305
314
|
Platform Host exposes vendor-neutral lifecycle telemetry through the optional `telemetry` sink on
|
package/dist/index.cjs
CHANGED
|
@@ -715,24 +715,24 @@ function createGovernedActionHost(options) {
|
|
|
715
715
|
}
|
|
716
716
|
const action = actionResolver(invocation.actionId);
|
|
717
717
|
if (!action) {
|
|
718
|
-
return
|
|
718
|
+
return fail2(invocation, "failed", `Unknown action: ${invocation.actionId}`);
|
|
719
719
|
}
|
|
720
720
|
if (action.kind === "saga") {
|
|
721
|
-
return
|
|
721
|
+
return fail2(
|
|
722
722
|
invocation,
|
|
723
723
|
"failed",
|
|
724
724
|
`Saga action ${invocation.actionId} requires a durable PlatformActionDispatcher and cannot execute as an atomic Host action.`
|
|
725
725
|
);
|
|
726
726
|
}
|
|
727
727
|
if (resumingRunningInvocation && invocation.attemptCount === 0 && !invocation.leaseOwner && !action.idempotent) {
|
|
728
|
-
return
|
|
728
|
+
return fail2(
|
|
729
729
|
invocation,
|
|
730
730
|
"failed",
|
|
731
731
|
`Interrupted inline action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`
|
|
732
732
|
);
|
|
733
733
|
}
|
|
734
734
|
if (invocation.attemptCount > 1 && !action.idempotent) {
|
|
735
|
-
return
|
|
735
|
+
return fail2(
|
|
736
736
|
invocation,
|
|
737
737
|
"failed",
|
|
738
738
|
`Interrupted action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`
|
|
@@ -756,12 +756,12 @@ function createGovernedActionHost(options) {
|
|
|
756
756
|
const parsed = action.schema.safeParse(invocation.parameters);
|
|
757
757
|
if (!parsed.success) {
|
|
758
758
|
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
759
|
-
return
|
|
759
|
+
return fail2(invocation, "validation_failed", message);
|
|
760
760
|
}
|
|
761
761
|
if (options.resolveMutationGovernance && !invocation.mutationFootprint) {
|
|
762
762
|
const governanceStore2 = asGovernanceStore(options.store);
|
|
763
763
|
if (!governanceStore2) {
|
|
764
|
-
return
|
|
764
|
+
return fail2(invocation, "failed", "The configured mutation-governance resolver requires a governance-capable store.");
|
|
765
765
|
}
|
|
766
766
|
const context = await options.resolveMutationGovernance({
|
|
767
767
|
actionInvocationId,
|
|
@@ -779,7 +779,7 @@ function createGovernedActionHost(options) {
|
|
|
779
779
|
const hitlParameters = isRecord(parsed.data) ? parsed.data : invocation.parameters;
|
|
780
780
|
const approvalStore = asApprovalStore(options.store);
|
|
781
781
|
if (!approvalStore) {
|
|
782
|
-
return
|
|
782
|
+
return fail2(
|
|
783
783
|
invocation,
|
|
784
784
|
"failed",
|
|
785
785
|
"The configured HITL evaluator requires a store with approval persistence support."
|
|
@@ -810,7 +810,7 @@ function createGovernedActionHost(options) {
|
|
|
810
810
|
);
|
|
811
811
|
}
|
|
812
812
|
if (invocation.hitlRoute === "rejected") {
|
|
813
|
-
return
|
|
813
|
+
return fail2(
|
|
814
814
|
invocation,
|
|
815
815
|
"failed",
|
|
816
816
|
`HITL rejected: ${invocation.hitlReason ?? "no reason provided"}`
|
|
@@ -829,7 +829,7 @@ function createGovernedActionHost(options) {
|
|
|
829
829
|
const authorizationInput = toAuthorizationInput(action, invocation);
|
|
830
830
|
const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : invocation.executionReason ?? "initial");
|
|
831
831
|
if (!await options.authorization.checkEntitlement(authorizationInput)) {
|
|
832
|
-
return
|
|
832
|
+
return fail2(
|
|
833
833
|
invocation,
|
|
834
834
|
"failed",
|
|
835
835
|
`Module "${action.namespace}" is no longer enabled for tenant ${tenantId}`
|
|
@@ -869,7 +869,7 @@ function createGovernedActionHost(options) {
|
|
|
869
869
|
}) : await options.authorization.authorize(authorizationInput);
|
|
870
870
|
if (!executionAuthorized) {
|
|
871
871
|
if (!action.execution) {
|
|
872
|
-
return
|
|
872
|
+
return fail2(invocation, "failed", `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`);
|
|
873
873
|
}
|
|
874
874
|
const reconciliation = {
|
|
875
875
|
kind: "authorization_denied",
|
|
@@ -919,12 +919,12 @@ function createGovernedActionHost(options) {
|
|
|
919
919
|
subjectId: actionInvocationId,
|
|
920
920
|
payload: { actionId: action.actionId, policyId: aggregate.policyId, reason }
|
|
921
921
|
}, `compliance:${aggregate.policyId}`);
|
|
922
|
-
return
|
|
922
|
+
return fail2(invocation, "blocked_by_policy", reason);
|
|
923
923
|
}
|
|
924
924
|
if (options.resolvePolicyObligations) {
|
|
925
925
|
const governanceStore2 = asGovernanceStore(options.store);
|
|
926
926
|
if (!governanceStore2) {
|
|
927
|
-
return
|
|
927
|
+
return fail2(invocation, "failed", "Policy obligations require a governance-capable store.");
|
|
928
928
|
}
|
|
929
929
|
const existing = await governanceStore2.listPolicyObligations(actionInvocationId, tenantId, spaceId);
|
|
930
930
|
if (existing.length === 0) {
|
|
@@ -1039,7 +1039,7 @@ function createGovernedActionHost(options) {
|
|
|
1039
1039
|
data = executed.data;
|
|
1040
1040
|
domainEvents = executed.domainEvents;
|
|
1041
1041
|
} catch (error) {
|
|
1042
|
-
return
|
|
1042
|
+
return fail2(invocation, "failed", errorMessage(error));
|
|
1043
1043
|
}
|
|
1044
1044
|
let pendingHandoff = invocation.pendingCompletion;
|
|
1045
1045
|
for (const [stepIndex, step] of (action.adapterSteps ?? []).entries()) {
|
|
@@ -1219,7 +1219,7 @@ function createGovernedActionHost(options) {
|
|
|
1219
1219
|
subjectId: adapterEventSubject.subjectId,
|
|
1220
1220
|
payload: { adapterType: step.adapterType, operation: step.operation }
|
|
1221
1221
|
}, `adapter:${stepIndex}:failed`);
|
|
1222
|
-
return
|
|
1222
|
+
return fail2(invocation, "failed", result2.error ?? "Adapter failed");
|
|
1223
1223
|
}
|
|
1224
1224
|
const resultRecord = result2;
|
|
1225
1225
|
const output = isRecord(resultRecord.output) ? resultRecord.output : Object.fromEntries(
|
|
@@ -1323,7 +1323,7 @@ function createGovernedActionHost(options) {
|
|
|
1323
1323
|
error: message,
|
|
1324
1324
|
updatedAt: now()
|
|
1325
1325
|
});
|
|
1326
|
-
return
|
|
1326
|
+
return fail2(invocation, "failed", message);
|
|
1327
1327
|
} finally {
|
|
1328
1328
|
if (adapterDeadlineTimer) clearTimeout(adapterDeadlineTimer);
|
|
1329
1329
|
}
|
|
@@ -1345,7 +1345,7 @@ function createGovernedActionHost(options) {
|
|
|
1345
1345
|
}
|
|
1346
1346
|
return withConsistency({ actionInvocationId, status: "reconciliation_required", error, pendingCompletion: pendingHandoff }, action.actionId);
|
|
1347
1347
|
}
|
|
1348
|
-
return
|
|
1348
|
+
return fail2(invocation, "failed", message);
|
|
1349
1349
|
}
|
|
1350
1350
|
}
|
|
1351
1351
|
const result = withoutPrivateHostFields(data, eventResultFields);
|
|
@@ -1418,7 +1418,7 @@ function createGovernedActionHost(options) {
|
|
|
1418
1418
|
};
|
|
1419
1419
|
} catch (error) {
|
|
1420
1420
|
if (error instanceof RecoverableFinalizationError) throw error;
|
|
1421
|
-
return
|
|
1421
|
+
return fail2(invocation, "failed", errorMessage(error));
|
|
1422
1422
|
}
|
|
1423
1423
|
}
|
|
1424
1424
|
async function resumeApprovedInvocation(actionInvocationId, tenantId, spaceId, decision) {
|
|
@@ -1437,7 +1437,7 @@ function createGovernedActionHost(options) {
|
|
|
1437
1437
|
};
|
|
1438
1438
|
}
|
|
1439
1439
|
const action = actionResolver(invocation.actionId);
|
|
1440
|
-
if (!action) return
|
|
1440
|
+
if (!action) return fail2(invocation, "failed", `Unknown action: ${invocation.actionId}`);
|
|
1441
1441
|
const approvalAuthorizationInput = toAuthorizationInput(action, {
|
|
1442
1442
|
tenantId,
|
|
1443
1443
|
spaceId,
|
|
@@ -1590,7 +1590,7 @@ function createGovernedActionHost(options) {
|
|
|
1590
1590
|
await (transaction ?? options.store).appendEvent(envelope);
|
|
1591
1591
|
}
|
|
1592
1592
|
}
|
|
1593
|
-
async function
|
|
1593
|
+
async function fail2(invocation, status, error) {
|
|
1594
1594
|
await persistInvocation(invocation, { status, error });
|
|
1595
1595
|
return {
|
|
1596
1596
|
actionInvocationId: invocation.id,
|
|
@@ -3564,6 +3564,181 @@ function createDurableSagaParentLifecycle(options) {
|
|
|
3564
3564
|
};
|
|
3565
3565
|
}
|
|
3566
3566
|
|
|
3567
|
+
// src/tenant-isolation.ts
|
|
3568
|
+
function fail(message) {
|
|
3569
|
+
throw new Error(message);
|
|
3570
|
+
}
|
|
3571
|
+
function submitInput(subject, scope, idempotencyKey) {
|
|
3572
|
+
return {
|
|
3573
|
+
actionId: subject.actionId,
|
|
3574
|
+
parameters: subject.parameters ?? {},
|
|
3575
|
+
tenantId: scope.tenantId,
|
|
3576
|
+
spaceId: scope.spaceId,
|
|
3577
|
+
actorId: scope.actorId,
|
|
3578
|
+
actorType: scope.actorType,
|
|
3579
|
+
idempotencyKey
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
function tenantIsolationChecks() {
|
|
3583
|
+
return [
|
|
3584
|
+
{
|
|
3585
|
+
id: "fabric.tenancy.invocation-scoped-read.v1",
|
|
3586
|
+
async run(subject) {
|
|
3587
|
+
await subject.reset?.();
|
|
3588
|
+
if (subject.ours.tenantId === subject.theirs.tenantId) fail("the two scopes must be in different tenants");
|
|
3589
|
+
const ours = await subject.host.submitAction(submitInput(subject, subject.ours, "iso:read"));
|
|
3590
|
+
const crossRead = await subject.store.getActionInvocation(ours.actionInvocationId, subject.theirs.tenantId, subject.theirs.spaceId);
|
|
3591
|
+
if (crossRead !== void 0) fail("an invocation was readable under another tenant's scope");
|
|
3592
|
+
const shape = (record) => JSON.stringify({ status: record?.status, result: record?.result, error: record?.error, attemptCount: record?.attemptCount, updatedAt: record?.updatedAt });
|
|
3593
|
+
const before = await subject.store.getActionInvocation(ours.actionInvocationId, subject.ours.tenantId, subject.ours.spaceId);
|
|
3594
|
+
if (!before) fail("our own invocation was not readable under our scope, so the check proves nothing");
|
|
3595
|
+
const untouched = shape(before);
|
|
3596
|
+
let crossExecute;
|
|
3597
|
+
try {
|
|
3598
|
+
crossExecute = await subject.host.executeInvocation(ours.actionInvocationId, subject.theirs.tenantId, subject.theirs.spaceId);
|
|
3599
|
+
} catch {
|
|
3600
|
+
crossExecute = void 0;
|
|
3601
|
+
}
|
|
3602
|
+
if (crossExecute !== void 0) fail("an invocation was executable under another tenant's scope");
|
|
3603
|
+
const after = await subject.store.getActionInvocation(ours.actionInvocationId, subject.ours.tenantId, subject.ours.spaceId);
|
|
3604
|
+
if (untouched !== shape(after)) fail("an execution attempted under another tenant's scope changed our invocation");
|
|
3605
|
+
return ["invocation readable under our scope, invisible and untouched under the other tenant's"];
|
|
3606
|
+
}
|
|
3607
|
+
},
|
|
3608
|
+
{
|
|
3609
|
+
id: "fabric.tenancy.idempotency-key-scoped.v1",
|
|
3610
|
+
async run(subject) {
|
|
3611
|
+
await subject.reset?.();
|
|
3612
|
+
const ours = await subject.host.submitAction(submitInput(subject, subject.ours, "iso:shared-key"));
|
|
3613
|
+
const again = await subject.host.submitAction(submitInput(subject, subject.ours, "iso:shared-key"));
|
|
3614
|
+
if (ours.actionInvocationId !== again.actionInvocationId) fail("the same idempotency key in one tenant produced two invocations, so the key is not doing anything and the check proves nothing");
|
|
3615
|
+
const theirs = await subject.host.submitAction(submitInput(subject, subject.theirs, "iso:shared-key"));
|
|
3616
|
+
if (ours.actionInvocationId === theirs.actionInvocationId) fail("the same idempotency key in two tenants resolved to one invocation");
|
|
3617
|
+
return ["one idempotency key: one invocation within a tenant, two across tenants"];
|
|
3618
|
+
}
|
|
3619
|
+
},
|
|
3620
|
+
{
|
|
3621
|
+
id: "fabric.tenancy.events-scoped.v1",
|
|
3622
|
+
async run(subject) {
|
|
3623
|
+
await subject.reset?.();
|
|
3624
|
+
const ours = await subject.host.submitAction(submitInput(subject, subject.ours, "iso:events"));
|
|
3625
|
+
const theirEvents = await subject.store.listEvents(subject.theirs.tenantId, subject.theirs.spaceId);
|
|
3626
|
+
const leaked = theirEvents.filter((event) => event.tenantId === subject.ours.tenantId || event.actionInvocationId === ours.actionInvocationId);
|
|
3627
|
+
if (leaked.length > 0) fail(`${leaked.length} of our events appeared in the other tenant's stream`);
|
|
3628
|
+
const ourEvents = await subject.store.listEvents(subject.ours.tenantId, subject.ours.spaceId);
|
|
3629
|
+
if (!ourEvents.some((event) => event.actionInvocationId === ours.actionInvocationId)) fail("our own stream does not carry the event we just caused, so the check proves nothing");
|
|
3630
|
+
if (ourEvents.some((event) => event.tenantId !== subject.ours.tenantId)) fail("another tenant's event appeared in our stream");
|
|
3631
|
+
return ["event streams carry their own tenant's events and only those"];
|
|
3632
|
+
}
|
|
3633
|
+
},
|
|
3634
|
+
{
|
|
3635
|
+
id: "fabric.tenancy.worker-claim-scoped.v1",
|
|
3636
|
+
async run(subject) {
|
|
3637
|
+
await subject.reset?.();
|
|
3638
|
+
const probeSpace = `iso-probe-${Date.now()}`;
|
|
3639
|
+
const created = await subject.store.createActionInvocation({
|
|
3640
|
+
id: `iso_pending_${Date.now()}`,
|
|
3641
|
+
tenantId: subject.ours.tenantId,
|
|
3642
|
+
spaceId: probeSpace,
|
|
3643
|
+
actionId: subject.actionId,
|
|
3644
|
+
actionVersion: 1,
|
|
3645
|
+
actorId: subject.ours.actorId,
|
|
3646
|
+
actorType: subject.ours.actorType,
|
|
3647
|
+
status: "pending",
|
|
3648
|
+
parameters: subject.parameters ?? {},
|
|
3649
|
+
result: {},
|
|
3650
|
+
correlationId: `iso-corr-${Date.now()}`
|
|
3651
|
+
});
|
|
3652
|
+
try {
|
|
3653
|
+
const theirs = await subject.store.claimActionInvocations({
|
|
3654
|
+
workerId: "iso-worker",
|
|
3655
|
+
leaseDurationMs: 1e3,
|
|
3656
|
+
limit: 1e3,
|
|
3657
|
+
tenantId: subject.theirs.tenantId,
|
|
3658
|
+
spaceId: probeSpace
|
|
3659
|
+
});
|
|
3660
|
+
if (theirs.some((row) => row.id === created.id)) fail("a worker scoped to another tenant claimed our pending invocation");
|
|
3661
|
+
const ours = await subject.store.claimActionInvocations({
|
|
3662
|
+
workerId: "iso-worker",
|
|
3663
|
+
leaseDurationMs: 1e3,
|
|
3664
|
+
limit: 1e3,
|
|
3665
|
+
tenantId: subject.ours.tenantId,
|
|
3666
|
+
spaceId: probeSpace
|
|
3667
|
+
});
|
|
3668
|
+
if (!ours.some((row) => row.id === created.id)) fail("a worker scoped to our own tenant did not claim our pending invocation, so the check proves nothing");
|
|
3669
|
+
} finally {
|
|
3670
|
+
await subject.store.updateActionInvocation(created.id, subject.ours.tenantId, probeSpace, {
|
|
3671
|
+
status: "failed",
|
|
3672
|
+
error: "tenant isolation probe; never executed"
|
|
3673
|
+
});
|
|
3674
|
+
}
|
|
3675
|
+
return ["tenant-scoped claim takes our work and leaves other tenants' alone"];
|
|
3676
|
+
}
|
|
3677
|
+
},
|
|
3678
|
+
{
|
|
3679
|
+
id: "fabric.tenancy.completion-scoped.v1",
|
|
3680
|
+
async run(subject) {
|
|
3681
|
+
await subject.reset?.();
|
|
3682
|
+
const ours = await subject.host.submitAction(submitInput(subject, subject.ours, "iso:complete"));
|
|
3683
|
+
let outcome;
|
|
3684
|
+
try {
|
|
3685
|
+
await subject.host.completeExternalInvocation(ours.actionInvocationId, subject.theirs.tenantId, subject.theirs.spaceId, {
|
|
3686
|
+
externalReference: "iso-ref",
|
|
3687
|
+
outcome: "completed",
|
|
3688
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3689
|
+
});
|
|
3690
|
+
outcome = "accepted";
|
|
3691
|
+
} catch (error) {
|
|
3692
|
+
const refusal = error.refusal;
|
|
3693
|
+
outcome = refusal ?? "threw";
|
|
3694
|
+
}
|
|
3695
|
+
if (outcome !== "not_found") fail(`a completion under another tenant's scope was ${outcome}; it must be refused as not_found`);
|
|
3696
|
+
let mine;
|
|
3697
|
+
try {
|
|
3698
|
+
await subject.host.completeExternalInvocation(ours.actionInvocationId, subject.ours.tenantId, subject.ours.spaceId, {
|
|
3699
|
+
externalReference: "iso-ref",
|
|
3700
|
+
outcome: "completed",
|
|
3701
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3702
|
+
});
|
|
3703
|
+
mine = "accepted";
|
|
3704
|
+
} catch (error) {
|
|
3705
|
+
mine = error.refusal ?? "threw";
|
|
3706
|
+
}
|
|
3707
|
+
if (mine === "not_found") fail("a completion under our own scope was refused as not_found, so the check proves nothing");
|
|
3708
|
+
return ["completion under the other tenant's scope refused as not_found; found under ours"];
|
|
3709
|
+
}
|
|
3710
|
+
},
|
|
3711
|
+
{
|
|
3712
|
+
id: "fabric.tenancy.projection-scoped.v1",
|
|
3713
|
+
async run(subject) {
|
|
3714
|
+
if (!subject.read) return ["skipped: subject lends no read"];
|
|
3715
|
+
await subject.reset?.();
|
|
3716
|
+
await subject.host.submitAction(submitInput(subject, subject.ours, "iso:projection"));
|
|
3717
|
+
const ours = await subject.read(subject.ours);
|
|
3718
|
+
const theirs = await subject.read(subject.theirs);
|
|
3719
|
+
if (!ours.allowed) fail("our own viewer was denied the read");
|
|
3720
|
+
if (ours.rows.length === 0) fail("our own viewer saw nothing, so the check proves nothing");
|
|
3721
|
+
if (ours.rows.some((row) => row?.tenantId !== subject.ours.tenantId)) fail("our own viewer saw rows that are not ours, or rows carrying no tenant");
|
|
3722
|
+
const leaked = theirs.allowed ? theirs.rows.filter((row) => row?.tenantId === subject.ours.tenantId) : [];
|
|
3723
|
+
if (leaked.length > 0) fail(`a viewer in the other tenant saw ${leaked.length} of our rows`);
|
|
3724
|
+
return [theirs.allowed ? "other tenant's viewer sees none of our rows" : "other tenant's viewer is denied"];
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
];
|
|
3728
|
+
}
|
|
3729
|
+
async function runTenantIsolationChecks(subject) {
|
|
3730
|
+
const checks = [];
|
|
3731
|
+
for (const check of tenantIsolationChecks()) {
|
|
3732
|
+
try {
|
|
3733
|
+
const evidence = await check.run(subject);
|
|
3734
|
+
checks.push({ id: check.id, status: evidence[0]?.startsWith("skipped") ? "skipped" : "passed", evidence });
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
checks.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
return { passed: checks.every((check) => check.status !== "failed"), checks };
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3567
3742
|
exports.ExternalCompletionError = ExternalCompletionError;
|
|
3568
3743
|
exports.IdempotencyConflictError = IdempotencyConflictError;
|
|
3569
3744
|
exports.MemoryPlatformHostStore = MemoryPlatformHostStore;
|
|
@@ -3584,6 +3759,8 @@ exports.getPlatformHostHealthSnapshot = getPlatformHostHealthSnapshot;
|
|
|
3584
3759
|
exports.runOutboxRelayCycle = runOutboxRelayCycle;
|
|
3585
3760
|
exports.runPlatformActionWorker = runPlatformActionWorker;
|
|
3586
3761
|
exports.runPlatformActionWorkerCycle = runPlatformActionWorkerCycle;
|
|
3762
|
+
exports.runTenantIsolationChecks = runTenantIsolationChecks;
|
|
3763
|
+
exports.tenantIsolationChecks = tenantIsolationChecks;
|
|
3587
3764
|
exports.toEnterpriseEventEnvelope = toEnterpriseEventEnvelope;
|
|
3588
3765
|
//# sourceMappingURL=index.cjs.map
|
|
3589
3766
|
//# sourceMappingURL=index.cjs.map
|