@fabricorg/platform-host 7.1.1 → 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/dist/index.cjs CHANGED
@@ -284,6 +284,221 @@ function isPromiseLike(value) {
284
284
  );
285
285
  }
286
286
 
287
+ // src/host-internals.ts
288
+ function asAtomicMutationStore(store) {
289
+ const candidate = store;
290
+ return typeof candidate.transactionWithEvents === "function" ? store : void 0;
291
+ }
292
+ function asGovernanceStore(store) {
293
+ const candidate = store;
294
+ return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
295
+ }
296
+ function withoutPrivateHostFields(data, eventResultFields) {
297
+ return Object.fromEntries(
298
+ Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
299
+ );
300
+ }
301
+ function errorMessage(error) {
302
+ return error instanceof Error ? error.message : String(error);
303
+ }
304
+ function lifecycleId(prefix, invocationId, key) {
305
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
306
+ return `${prefix}_${invocationId}_${safeKey}`;
307
+ }
308
+ function emitInvocationStatusTelemetry(invocation, status, telemetry, occurredAt, previousStatus) {
309
+ if (!status) return;
310
+ const event = status === "completed" ? { name: "invocation.completed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationCompleted } : status === "blocked_by_policy" ? { name: "invocation.policy_blocked", metricName: PLATFORM_HOST_METRIC_NAMES.invocationPolicyBlocked } : status === "validation_failed" ? { name: "invocation.validation_failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationValidationFailed } : status === "waiting_for_approval" ? { name: "invocation.approval_waited", metricName: PLATFORM_HOST_METRIC_NAMES.invocationApprovalWaited } : status === "reconciliation_required" ? { name: "invocation.reconciliation_required", metricName: PLATFORM_HOST_METRIC_NAMES.invocationReconciliationRequired } : status === "failed" ? { name: "invocation.failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationFailed } : void 0;
311
+ if (!event) return;
312
+ emitPlatformHostTelemetry(telemetry, {
313
+ kind: "event",
314
+ name: event.name,
315
+ metricName: event.metricName,
316
+ occurredAt,
317
+ tenantId: invocation.tenantId,
318
+ spaceId: invocation.spaceId,
319
+ attributes: {
320
+ ...invocation.actionId ? { actionId: invocation.actionId } : {},
321
+ ...invocation.actionVersion !== void 0 ? { actionVersion: invocation.actionVersion } : {},
322
+ ...previousStatus ? { fromStatus: previousStatus } : {}
323
+ }
324
+ });
325
+ }
326
+
327
+ // src/external-completion.ts
328
+ function createExternalCompletionLifecycle(deps) {
329
+ const { now, actionResolver, appendEvent, persistInvocation, actionResult: actionResult2, withConsistency, eventResultFields } = deps;
330
+ async function completeExternalInvocation(actionInvocationId, tenantId, spaceId, completion) {
331
+ if (typeof completion.externalReference !== "string" || completion.externalReference.trim() === "") {
332
+ throw new ExternalCompletionError("invalid_completion", "External completion must name the external reference it completes.");
333
+ }
334
+ if (completion.outcome !== "completed" && completion.outcome !== "failed") {
335
+ throw new ExternalCompletionError("invalid_completion", `External completion outcome must be "completed" or "failed", received "${String(completion.outcome)}".`);
336
+ }
337
+ const atomicStore = asAtomicMutationStore(deps.store);
338
+ if (atomicStore) {
339
+ return atomicStore.transactionWithEvents(async (transaction) => {
340
+ const read2 = transaction.getActionInvocationForUpdate ? await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId) : await deps.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
341
+ return settleExternalCompletion(read2, actionInvocationId, completion, {
342
+ update: (patch) => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch),
343
+ transaction
344
+ });
345
+ });
346
+ }
347
+ const read = await deps.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
348
+ return settleExternalCompletion(read, actionInvocationId, completion, {
349
+ update: (patch) => deps.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch)
350
+ });
351
+ }
352
+ async function settleExternalCompletion(invocation, actionInvocationId, completion, writer) {
353
+ if (!invocation) throw new ExternalCompletionError("not_found", `ActionInvocation not found: ${actionInvocationId}`);
354
+ const recorded = invocation.externalCompletion;
355
+ const stillExecuting = !recorded && (invocation.status === "pending" || invocation.status === "running" && (invocation.leaseOwner !== void 0 || invocation.pendingCompletion?.parkedAt === void 0));
356
+ if (stillExecuting) {
357
+ throw new ExternalCompletionError("still_executing", `ActionInvocation ${actionInvocationId} is still executing; retry the completion after it parks.`);
358
+ }
359
+ if (recorded) {
360
+ if (recorded.externalReference !== completion.externalReference) {
361
+ throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.externalReference}", not "${completion.externalReference}".`);
362
+ }
363
+ if (completion.provider !== void 0 && recorded.provider !== void 0 && completion.provider !== recorded.provider) {
364
+ throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.provider}", not "${completion.provider}".`);
365
+ }
366
+ const incomingDigest = completionDigest(completion, recorded.provider ?? completion.provider);
367
+ if (recorded.digest === incomingDigest) {
368
+ return withConsistency(actionResult2(invocation), invocation.actionId);
369
+ }
370
+ const message = recorded.outcome === completion.outcome ? `External operation "${completion.externalReference}" reported ${completion.outcome} again with different evidence.` : `External operation "${completion.externalReference}" reported ${completion.outcome} after reporting ${recorded.outcome}.`;
371
+ await recordCompletionReconciliation(invocation, { ...completion, kind: `contradicted:${incomingDigest.slice(0, 16)}` }, message);
372
+ await appendEvent(invocation, {
373
+ eventType: "ExternalOperationContradicted",
374
+ subjectType: "AdapterInvocation",
375
+ subjectId: invocation.pendingCompletion?.adapterInvocationId ?? actionInvocationId,
376
+ payload: { externalReference: completion.externalReference, recorded: recorded.outcome, reported: completion.outcome }
377
+ }, `external:${completion.externalReference}:contradicted:${completion.outcome}`, 1, writer.transaction);
378
+ await writer.update({ status: "reconciliation_required", error: message });
379
+ emitInvocationStatusTelemetry(invocation, "reconciliation_required", deps.telemetry, now(), invocation.status);
380
+ return withConsistency(actionResult2({ ...invocation, status: "reconciliation_required", error: message }), invocation.actionId);
381
+ }
382
+ const pending = invocation.pendingCompletion;
383
+ if (!pending) {
384
+ const action = actionResolver(invocation.actionId);
385
+ if ((action?.execution?.completion ?? "immediate") === "immediate") {
386
+ throw new ExternalCompletionError("immediate_contract", `Action ${invocation.actionId} declares immediate completion; nothing external completes it.`);
387
+ }
388
+ throw new ExternalCompletionError("not_awaiting", `ActionInvocation ${actionInvocationId} is not awaiting an external completion.`);
389
+ }
390
+ if (pending.externalReference !== completion.externalReference) {
391
+ throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} awaits "${pending.externalReference}", not "${completion.externalReference}".`);
392
+ }
393
+ if (completion.provider !== void 0 && completion.provider !== pending.provider) {
394
+ throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} awaits completion from "${pending.provider}", not "${completion.provider}".`);
395
+ }
396
+ const recordedAt = now();
397
+ const externalCompletion = {
398
+ ...completion,
399
+ provider: completion.provider ?? pending.provider,
400
+ observedAt: completion.observedAt instanceof Date ? completion.observedAt.toISOString() : completion.observedAt,
401
+ recordedAt,
402
+ digest: completionDigest(completion, completion.provider ?? pending.provider)
403
+ };
404
+ const subject = { subjectType: "AdapterInvocation", subjectId: pending.adapterInvocationId };
405
+ const settles = invocation.status === "running" && pending.parkedAt !== void 0 || invocation.status === "reconciliation_required" && invocation.error?.includes("did not complete by") === true;
406
+ if (completion.outcome === "completed") {
407
+ const action = actionResolver(invocation.actionId);
408
+ const merged = withoutPrivateHostFields({ ...invocation.result, ...completion.result ?? {} }, eventResultFields);
409
+ if (action?.resultSchema) {
410
+ const parsedResult = action.resultSchema.safeParse(merged);
411
+ if (!parsedResult.success) {
412
+ throw new ExternalCompletionError("result_invalid", `External completion result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
413
+ }
414
+ }
415
+ await deps.store.updateAdapterInvocation(pending.adapterInvocationId, {
416
+ ...completion.result ? { output: withoutPrivateHostFields(completion.result, eventResultFields) } : {},
417
+ updatedAt: recordedAt
418
+ });
419
+ await appendEvent(invocation, {
420
+ eventType: "ExternalOperationCompleted",
421
+ ...subject,
422
+ payload: { externalReference: pending.externalReference, provider: pending.provider, ...completion.result ? { result: withoutPrivateHostFields(completion.result, eventResultFields) } : {} }
423
+ }, `external:${pending.adapterInvocationId}:completed`, 1, writer.transaction);
424
+ const patch2 = settles ? { status: "completed", result: merged, externalCompletion, pendingCompletion: void 0, error: void 0 } : { externalCompletion, pendingCompletion: void 0 };
425
+ await writer.update(patch2);
426
+ if (settles) emitInvocationStatusTelemetry(invocation, "completed", deps.telemetry, now(), invocation.status);
427
+ return withConsistency(actionResult2({ ...invocation, ...patch2, result: settles ? merged : invocation.result, ...settles ? { error: void 0 } : {} }), invocation.actionId);
428
+ }
429
+ const error = completion.error ?? `External operation "${pending.externalReference}" failed.`;
430
+ await deps.store.updateAdapterInvocation(pending.adapterInvocationId, { status: "failed", error, updatedAt: recordedAt });
431
+ await appendEvent(invocation, {
432
+ eventType: "ExternalOperationFailed",
433
+ ...subject,
434
+ payload: { externalReference: pending.externalReference, provider: pending.provider, error }
435
+ }, `external:${pending.adapterInvocationId}:failed`, 1, writer.transaction);
436
+ const patch = settles ? { status: "failed", error, externalCompletion, pendingCompletion: void 0 } : { externalCompletion, pendingCompletion: void 0 };
437
+ await writer.update(patch);
438
+ if (settles) emitInvocationStatusTelemetry(invocation, "failed", deps.telemetry, now(), invocation.status);
439
+ return withConsistency(actionResult2({ ...invocation, ...patch }), invocation.actionId);
440
+ }
441
+ function completionDigest(completion, provider) {
442
+ return crypto.createHash("sha256").update(canonicalJson({
443
+ provider: provider ?? null,
444
+ externalReference: completion.externalReference,
445
+ outcome: completion.outcome,
446
+ result: completion.result ?? null,
447
+ error: completion.error ?? null,
448
+ evidenceReferences: completion.evidenceReferences ?? null
449
+ })).digest("hex");
450
+ }
451
+ async function recordCompletionReconciliation(invocation, completion, reason) {
452
+ const governanceStore = asGovernanceStore(deps.store);
453
+ if (!governanceStore) throw new Error("External completion reconciliation requires a governance-capable store.");
454
+ const provider = completion.provider ?? invocation.pendingCompletion?.provider ?? invocation.externalCompletion?.provider ?? "external";
455
+ const seed = crypto.createHash("sha256").update(`${provider}\0${completion.externalReference}\0${completion.kind ?? "refused"}`).digest("hex").slice(0, 32);
456
+ await governanceStore.appendExternalReconciliation({
457
+ id: lifecycleId("rec", invocation.id, `ext:${seed}`),
458
+ actionInvocationId: invocation.id,
459
+ tenantId: invocation.tenantId,
460
+ spaceId: invocation.spaceId,
461
+ status: "pending",
462
+ provider,
463
+ externalOperationId: completion.externalReference,
464
+ attempt: 1,
465
+ reason,
466
+ ...completion.evidenceReferences ? { evidenceReferences: completion.evidenceReferences } : {},
467
+ observedAt: now()
468
+ });
469
+ }
470
+ async function reconcileOverdueCompletions(input = {}) {
471
+ const recoverable = deps.store;
472
+ if (!recoverable.listActionInvocations) {
473
+ throw new Error("Overdue completion reconciliation requires a store that can list invocations.");
474
+ }
475
+ if (!asGovernanceStore(deps.store)) {
476
+ throw new Error("Overdue completion reconciliation requires a governance-capable store to record its findings.");
477
+ }
478
+ const current = input.now ?? now();
479
+ const candidates = await recoverable.listActionInvocations({
480
+ statuses: ["running"],
481
+ completionDueBefore: current,
482
+ unleased: true,
483
+ ...input.tenantId ? { tenantId: input.tenantId } : {},
484
+ ...input.spaceId ? { spaceId: input.spaceId } : {},
485
+ limit: Math.max(1, Math.min(input.limit ?? 100, 1e3))
486
+ });
487
+ const overdue = [];
488
+ for (const invocation of candidates) {
489
+ const pending = invocation.pendingCompletion;
490
+ if (!pending?.dueAt || pending.dueAt.getTime() > current.getTime()) continue;
491
+ if (invocation.leaseOwner) continue;
492
+ const message = `External operation "${pending.externalReference}" from ${pending.provider} did not complete by ${pending.dueAt.toISOString()}.`;
493
+ await recordCompletionReconciliation(invocation, { externalReference: pending.externalReference, provider: pending.provider, kind: "overdue" }, message);
494
+ await persistInvocation(invocation, { status: "reconciliation_required", error: message });
495
+ overdue.push(invocation.id);
496
+ }
497
+ return { overdue };
498
+ }
499
+ return { completeExternalInvocation, reconcileOverdueCompletions, recordCompletionReconciliation };
500
+ }
501
+
287
502
  // src/host.ts
288
503
  var DEFAULT_EXTRACT_EVENTS = (data) => {
289
504
  const value = data._events;
@@ -500,24 +715,24 @@ function createGovernedActionHost(options) {
500
715
  }
501
716
  const action = actionResolver(invocation.actionId);
502
717
  if (!action) {
503
- return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
718
+ return fail2(invocation, "failed", `Unknown action: ${invocation.actionId}`);
504
719
  }
505
720
  if (action.kind === "saga") {
506
- return fail(
721
+ return fail2(
507
722
  invocation,
508
723
  "failed",
509
724
  `Saga action ${invocation.actionId} requires a durable PlatformActionDispatcher and cannot execute as an atomic Host action.`
510
725
  );
511
726
  }
512
727
  if (resumingRunningInvocation && invocation.attemptCount === 0 && !invocation.leaseOwner && !action.idempotent) {
513
- return fail(
728
+ return fail2(
514
729
  invocation,
515
730
  "failed",
516
731
  `Interrupted inline action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`
517
732
  );
518
733
  }
519
734
  if (invocation.attemptCount > 1 && !action.idempotent) {
520
- return fail(
735
+ return fail2(
521
736
  invocation,
522
737
  "failed",
523
738
  `Interrupted action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`
@@ -541,12 +756,12 @@ function createGovernedActionHost(options) {
541
756
  const parsed = action.schema.safeParse(invocation.parameters);
542
757
  if (!parsed.success) {
543
758
  const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
544
- return fail(invocation, "validation_failed", message);
759
+ return fail2(invocation, "validation_failed", message);
545
760
  }
546
761
  if (options.resolveMutationGovernance && !invocation.mutationFootprint) {
547
762
  const governanceStore2 = asGovernanceStore(options.store);
548
763
  if (!governanceStore2) {
549
- return fail(invocation, "failed", "The configured mutation-governance resolver requires a governance-capable store.");
764
+ return fail2(invocation, "failed", "The configured mutation-governance resolver requires a governance-capable store.");
550
765
  }
551
766
  const context = await options.resolveMutationGovernance({
552
767
  actionInvocationId,
@@ -564,7 +779,7 @@ function createGovernedActionHost(options) {
564
779
  const hitlParameters = isRecord(parsed.data) ? parsed.data : invocation.parameters;
565
780
  const approvalStore = asApprovalStore(options.store);
566
781
  if (!approvalStore) {
567
- return fail(
782
+ return fail2(
568
783
  invocation,
569
784
  "failed",
570
785
  "The configured HITL evaluator requires a store with approval persistence support."
@@ -595,7 +810,7 @@ function createGovernedActionHost(options) {
595
810
  );
596
811
  }
597
812
  if (invocation.hitlRoute === "rejected") {
598
- return fail(
813
+ return fail2(
599
814
  invocation,
600
815
  "failed",
601
816
  `HITL rejected: ${invocation.hitlReason ?? "no reason provided"}`
@@ -614,7 +829,7 @@ function createGovernedActionHost(options) {
614
829
  const authorizationInput = toAuthorizationInput(action, invocation);
615
830
  const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : invocation.executionReason ?? "initial");
616
831
  if (!await options.authorization.checkEntitlement(authorizationInput)) {
617
- return fail(
832
+ return fail2(
618
833
  invocation,
619
834
  "failed",
620
835
  `Module "${action.namespace}" is no longer enabled for tenant ${tenantId}`
@@ -654,7 +869,7 @@ function createGovernedActionHost(options) {
654
869
  }) : await options.authorization.authorize(authorizationInput);
655
870
  if (!executionAuthorized) {
656
871
  if (!action.execution) {
657
- return fail(invocation, "failed", `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`);
872
+ return fail2(invocation, "failed", `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`);
658
873
  }
659
874
  const reconciliation = {
660
875
  kind: "authorization_denied",
@@ -704,12 +919,12 @@ function createGovernedActionHost(options) {
704
919
  subjectId: actionInvocationId,
705
920
  payload: { actionId: action.actionId, policyId: aggregate.policyId, reason }
706
921
  }, `compliance:${aggregate.policyId}`);
707
- return fail(invocation, "blocked_by_policy", reason);
922
+ return fail2(invocation, "blocked_by_policy", reason);
708
923
  }
709
924
  if (options.resolvePolicyObligations) {
710
925
  const governanceStore2 = asGovernanceStore(options.store);
711
926
  if (!governanceStore2) {
712
- return fail(invocation, "failed", "Policy obligations require a governance-capable store.");
927
+ return fail2(invocation, "failed", "Policy obligations require a governance-capable store.");
713
928
  }
714
929
  const existing = await governanceStore2.listPolicyObligations(actionInvocationId, tenantId, spaceId);
715
930
  if (existing.length === 0) {
@@ -824,7 +1039,7 @@ function createGovernedActionHost(options) {
824
1039
  data = executed.data;
825
1040
  domainEvents = executed.domainEvents;
826
1041
  } catch (error) {
827
- return fail(invocation, "failed", errorMessage(error));
1042
+ return fail2(invocation, "failed", errorMessage(error));
828
1043
  }
829
1044
  let pendingHandoff = invocation.pendingCompletion;
830
1045
  for (const [stepIndex, step] of (action.adapterSteps ?? []).entries()) {
@@ -1004,7 +1219,7 @@ function createGovernedActionHost(options) {
1004
1219
  subjectId: adapterEventSubject.subjectId,
1005
1220
  payload: { adapterType: step.adapterType, operation: step.operation }
1006
1221
  }, `adapter:${stepIndex}:failed`);
1007
- return fail(invocation, "failed", result2.error ?? "Adapter failed");
1222
+ return fail2(invocation, "failed", result2.error ?? "Adapter failed");
1008
1223
  }
1009
1224
  const resultRecord = result2;
1010
1225
  const output = isRecord(resultRecord.output) ? resultRecord.output : Object.fromEntries(
@@ -1042,7 +1257,7 @@ function createGovernedActionHost(options) {
1042
1257
  });
1043
1258
  let error = refuse;
1044
1259
  try {
1045
- await recordCompletionReconciliation(invocation, { externalReference: reference, provider: adapter.vendor }, refuse);
1260
+ await externalCompletion.recordCompletionReconciliation(invocation, { externalReference: reference, provider: adapter.vendor }, refuse);
1046
1261
  } catch (logError) {
1047
1262
  error = `${refuse} Reconciliation log unavailable: ${errorMessage(logError)}`;
1048
1263
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { error });
@@ -1108,7 +1323,7 @@ function createGovernedActionHost(options) {
1108
1323
  error: message,
1109
1324
  updatedAt: now()
1110
1325
  });
1111
- return fail(invocation, "failed", message);
1326
+ return fail2(invocation, "failed", message);
1112
1327
  } finally {
1113
1328
  if (adapterDeadlineTimer) clearTimeout(adapterDeadlineTimer);
1114
1329
  }
@@ -1123,14 +1338,14 @@ function createGovernedActionHost(options) {
1123
1338
  await persistInvocation(invocation, { status: "reconciliation_required", error: message });
1124
1339
  let error = message;
1125
1340
  try {
1126
- await recordCompletionReconciliation(invocation, { externalReference: pendingHandoff.externalReference, provider: pendingHandoff.provider, kind: "obligations" }, message);
1341
+ await externalCompletion.recordCompletionReconciliation(invocation, { externalReference: pendingHandoff.externalReference, provider: pendingHandoff.provider, kind: "obligations" }, message);
1127
1342
  } catch (logError) {
1128
1343
  error = `${message} Reconciliation log unavailable: ${errorMessage(logError)}`;
1129
1344
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { error });
1130
1345
  }
1131
1346
  return withConsistency({ actionInvocationId, status: "reconciliation_required", error, pendingCompletion: pendingHandoff }, action.actionId);
1132
1347
  }
1133
- return fail(invocation, "failed", message);
1348
+ return fail2(invocation, "failed", message);
1134
1349
  }
1135
1350
  }
1136
1351
  const result = withoutPrivateHostFields(data, eventResultFields);
@@ -1203,7 +1418,7 @@ function createGovernedActionHost(options) {
1203
1418
  };
1204
1419
  } catch (error) {
1205
1420
  if (error instanceof RecoverableFinalizationError) throw error;
1206
- return fail(invocation, "failed", errorMessage(error));
1421
+ return fail2(invocation, "failed", errorMessage(error));
1207
1422
  }
1208
1423
  }
1209
1424
  async function resumeApprovedInvocation(actionInvocationId, tenantId, spaceId, decision) {
@@ -1222,7 +1437,7 @@ function createGovernedActionHost(options) {
1222
1437
  };
1223
1438
  }
1224
1439
  const action = actionResolver(invocation.actionId);
1225
- if (!action) return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
1440
+ if (!action) return fail2(invocation, "failed", `Unknown action: ${invocation.actionId}`);
1226
1441
  const approvalAuthorizationInput = toAuthorizationInput(action, {
1227
1442
  tenantId,
1228
1443
  spaceId,
@@ -1316,175 +1531,6 @@ function createGovernedActionHost(options) {
1316
1531
  spaceId
1317
1532
  });
1318
1533
  }
1319
- async function completeExternalInvocation(actionInvocationId, tenantId, spaceId, completion) {
1320
- if (typeof completion.externalReference !== "string" || completion.externalReference.trim() === "") {
1321
- throw new ExternalCompletionError("invalid_completion", "External completion must name the external reference it completes.");
1322
- }
1323
- if (completion.outcome !== "completed" && completion.outcome !== "failed") {
1324
- throw new ExternalCompletionError("invalid_completion", `External completion outcome must be "completed" or "failed", received "${String(completion.outcome)}".`);
1325
- }
1326
- const atomicStore = asAtomicMutationStore(options.store);
1327
- if (atomicStore) {
1328
- return atomicStore.transactionWithEvents(async (transaction) => {
1329
- const read2 = transaction.getActionInvocationForUpdate ? await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId) : await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
1330
- return settleExternalCompletion(read2, actionInvocationId, completion, {
1331
- update: (patch) => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch),
1332
- transaction
1333
- });
1334
- });
1335
- }
1336
- const read = await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
1337
- return settleExternalCompletion(read, actionInvocationId, completion, {
1338
- update: (patch) => options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch)
1339
- });
1340
- }
1341
- async function settleExternalCompletion(invocation, actionInvocationId, completion, writer) {
1342
- if (!invocation) throw new ExternalCompletionError("not_found", `ActionInvocation not found: ${actionInvocationId}`);
1343
- const recorded = invocation.externalCompletion;
1344
- const stillExecuting = !recorded && (invocation.status === "pending" || invocation.status === "running" && (invocation.leaseOwner !== void 0 || invocation.pendingCompletion?.parkedAt === void 0));
1345
- if (stillExecuting) {
1346
- throw new ExternalCompletionError("still_executing", `ActionInvocation ${actionInvocationId} is still executing; retry the completion after it parks.`);
1347
- }
1348
- if (recorded) {
1349
- if (recorded.externalReference !== completion.externalReference) {
1350
- throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.externalReference}", not "${completion.externalReference}".`);
1351
- }
1352
- if (completion.provider !== void 0 && recorded.provider !== void 0 && completion.provider !== recorded.provider) {
1353
- throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.provider}", not "${completion.provider}".`);
1354
- }
1355
- const incomingDigest = completionDigest(completion, recorded.provider ?? completion.provider);
1356
- if (recorded.digest === incomingDigest) {
1357
- return withConsistency(actionResult(invocation), invocation.actionId);
1358
- }
1359
- const message = recorded.outcome === completion.outcome ? `External operation "${completion.externalReference}" reported ${completion.outcome} again with different evidence.` : `External operation "${completion.externalReference}" reported ${completion.outcome} after reporting ${recorded.outcome}.`;
1360
- await recordCompletionReconciliation(invocation, { ...completion, kind: `contradicted:${incomingDigest.slice(0, 16)}` }, message);
1361
- await appendEvent(invocation, {
1362
- eventType: "ExternalOperationContradicted",
1363
- subjectType: "AdapterInvocation",
1364
- subjectId: invocation.pendingCompletion?.adapterInvocationId ?? actionInvocationId,
1365
- payload: { externalReference: completion.externalReference, recorded: recorded.outcome, reported: completion.outcome }
1366
- }, `external:${completion.externalReference}:contradicted:${completion.outcome}`, 1, writer.transaction);
1367
- await writer.update({ status: "reconciliation_required", error: message });
1368
- emitInvocationStatusTelemetry(invocation, "reconciliation_required", options.telemetry, now(), invocation.status);
1369
- return withConsistency(actionResult({ ...invocation, status: "reconciliation_required", error: message }), invocation.actionId);
1370
- }
1371
- const pending = invocation.pendingCompletion;
1372
- if (!pending) {
1373
- const action = actionResolver(invocation.actionId);
1374
- if ((action?.execution?.completion ?? "immediate") === "immediate") {
1375
- throw new ExternalCompletionError("immediate_contract", `Action ${invocation.actionId} declares immediate completion; nothing external completes it.`);
1376
- }
1377
- throw new ExternalCompletionError("not_awaiting", `ActionInvocation ${actionInvocationId} is not awaiting an external completion.`);
1378
- }
1379
- if (pending.externalReference !== completion.externalReference) {
1380
- throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} awaits "${pending.externalReference}", not "${completion.externalReference}".`);
1381
- }
1382
- if (completion.provider !== void 0 && completion.provider !== pending.provider) {
1383
- throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} awaits completion from "${pending.provider}", not "${completion.provider}".`);
1384
- }
1385
- const recordedAt = now();
1386
- const externalCompletion = {
1387
- ...completion,
1388
- provider: completion.provider ?? pending.provider,
1389
- observedAt: completion.observedAt instanceof Date ? completion.observedAt.toISOString() : completion.observedAt,
1390
- recordedAt,
1391
- digest: completionDigest(completion, completion.provider ?? pending.provider)
1392
- };
1393
- const subject = { subjectType: "AdapterInvocation", subjectId: pending.adapterInvocationId };
1394
- const settles = invocation.status === "running" && pending.parkedAt !== void 0 || invocation.status === "reconciliation_required" && invocation.error?.includes("did not complete by") === true;
1395
- if (completion.outcome === "completed") {
1396
- const action = actionResolver(invocation.actionId);
1397
- const merged = withoutPrivateHostFields({ ...invocation.result, ...completion.result ?? {} }, eventResultFields);
1398
- if (action?.resultSchema) {
1399
- const parsedResult = action.resultSchema.safeParse(merged);
1400
- if (!parsedResult.success) {
1401
- throw new ExternalCompletionError("result_invalid", `External completion result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
1402
- }
1403
- }
1404
- await options.store.updateAdapterInvocation(pending.adapterInvocationId, {
1405
- ...completion.result ? { output: withoutPrivateHostFields(completion.result, eventResultFields) } : {},
1406
- updatedAt: recordedAt
1407
- });
1408
- await appendEvent(invocation, {
1409
- eventType: "ExternalOperationCompleted",
1410
- ...subject,
1411
- payload: { externalReference: pending.externalReference, provider: pending.provider, ...completion.result ? { result: withoutPrivateHostFields(completion.result, eventResultFields) } : {} }
1412
- }, `external:${pending.adapterInvocationId}:completed`, 1, writer.transaction);
1413
- const patch2 = settles ? { status: "completed", result: merged, externalCompletion, pendingCompletion: void 0, error: void 0 } : { externalCompletion, pendingCompletion: void 0 };
1414
- await writer.update(patch2);
1415
- if (settles) emitInvocationStatusTelemetry(invocation, "completed", options.telemetry, now(), invocation.status);
1416
- return withConsistency(actionResult({ ...invocation, ...patch2, result: settles ? merged : invocation.result, ...settles ? { error: void 0 } : {} }), invocation.actionId);
1417
- }
1418
- const error = completion.error ?? `External operation "${pending.externalReference}" failed.`;
1419
- await options.store.updateAdapterInvocation(pending.adapterInvocationId, { status: "failed", error, updatedAt: recordedAt });
1420
- await appendEvent(invocation, {
1421
- eventType: "ExternalOperationFailed",
1422
- ...subject,
1423
- payload: { externalReference: pending.externalReference, provider: pending.provider, error }
1424
- }, `external:${pending.adapterInvocationId}:failed`, 1, writer.transaction);
1425
- const patch = settles ? { status: "failed", error, externalCompletion, pendingCompletion: void 0 } : { externalCompletion, pendingCompletion: void 0 };
1426
- await writer.update(patch);
1427
- if (settles) emitInvocationStatusTelemetry(invocation, "failed", options.telemetry, now(), invocation.status);
1428
- return withConsistency(actionResult({ ...invocation, ...patch }), invocation.actionId);
1429
- }
1430
- function completionDigest(completion, provider) {
1431
- return crypto.createHash("sha256").update(canonicalJson({
1432
- provider: provider ?? null,
1433
- externalReference: completion.externalReference,
1434
- outcome: completion.outcome,
1435
- result: completion.result ?? null,
1436
- error: completion.error ?? null,
1437
- evidenceReferences: completion.evidenceReferences ?? null
1438
- })).digest("hex");
1439
- }
1440
- async function recordCompletionReconciliation(invocation, completion, reason) {
1441
- const governanceStore = asGovernanceStore(options.store);
1442
- if (!governanceStore) throw new Error("External completion reconciliation requires a governance-capable store.");
1443
- const provider = completion.provider ?? invocation.pendingCompletion?.provider ?? invocation.externalCompletion?.provider ?? "external";
1444
- const seed = crypto.createHash("sha256").update(`${provider}\0${completion.externalReference}\0${completion.kind ?? "refused"}`).digest("hex").slice(0, 32);
1445
- await governanceStore.appendExternalReconciliation({
1446
- id: lifecycleId("rec", invocation.id, `ext:${seed}`),
1447
- actionInvocationId: invocation.id,
1448
- tenantId: invocation.tenantId,
1449
- spaceId: invocation.spaceId,
1450
- status: "pending",
1451
- provider,
1452
- externalOperationId: completion.externalReference,
1453
- attempt: 1,
1454
- reason,
1455
- ...completion.evidenceReferences ? { evidenceReferences: completion.evidenceReferences } : {},
1456
- observedAt: now()
1457
- });
1458
- }
1459
- async function reconcileOverdueCompletions(input = {}) {
1460
- const recoverable = options.store;
1461
- if (!recoverable.listActionInvocations) {
1462
- throw new Error("Overdue completion reconciliation requires a store that can list invocations.");
1463
- }
1464
- if (!asGovernanceStore(options.store)) {
1465
- throw new Error("Overdue completion reconciliation requires a governance-capable store to record its findings.");
1466
- }
1467
- const current = input.now ?? now();
1468
- const candidates = await recoverable.listActionInvocations({
1469
- statuses: ["running"],
1470
- completionDueBefore: current,
1471
- unleased: true,
1472
- ...input.tenantId ? { tenantId: input.tenantId } : {},
1473
- ...input.spaceId ? { spaceId: input.spaceId } : {},
1474
- limit: Math.max(1, Math.min(input.limit ?? 100, 1e3))
1475
- });
1476
- const overdue = [];
1477
- for (const invocation of candidates) {
1478
- const pending = invocation.pendingCompletion;
1479
- if (!pending?.dueAt || pending.dueAt.getTime() > current.getTime()) continue;
1480
- if (invocation.leaseOwner) continue;
1481
- const message = `External operation "${pending.externalReference}" from ${pending.provider} did not complete by ${pending.dueAt.toISOString()}.`;
1482
- await recordCompletionReconciliation(invocation, { externalReference: pending.externalReference, provider: pending.provider, kind: "overdue" }, message);
1483
- await persistInvocation(invocation, { status: "reconciliation_required", error: message });
1484
- overdue.push(invocation.id);
1485
- }
1486
- return { overdue };
1487
- }
1488
1534
  function declaredConsistency(actionId) {
1489
1535
  return actionResolver(actionId)?.execution?.consistency;
1490
1536
  }
@@ -1544,7 +1590,7 @@ function createGovernedActionHost(options) {
1544
1590
  await (transaction ?? options.store).appendEvent(envelope);
1545
1591
  }
1546
1592
  }
1547
- async function fail(invocation, status, error) {
1593
+ async function fail2(invocation, status, error) {
1548
1594
  await persistInvocation(invocation, { status, error });
1549
1595
  return {
1550
1596
  actionInvocationId: invocation.id,
@@ -1581,7 +1627,26 @@ function createGovernedActionHost(options) {
1581
1627
  );
1582
1628
  emitInvocationStatusTelemetry(invocation, patch.status, options.telemetry, now(), previousStatus);
1583
1629
  }
1584
- return { submitAction, executeInvocation, resumeApprovedInvocation, recordExecutionAttestation, recordExternalReconciliation, completeExternalInvocation, reconcileOverdueCompletions };
1630
+ const externalCompletion = createExternalCompletionLifecycle({
1631
+ store: options.store,
1632
+ telemetry: options.telemetry,
1633
+ eventResultFields,
1634
+ now,
1635
+ actionResolver,
1636
+ appendEvent,
1637
+ persistInvocation,
1638
+ actionResult,
1639
+ withConsistency
1640
+ });
1641
+ return {
1642
+ submitAction,
1643
+ executeInvocation,
1644
+ resumeApprovedInvocation,
1645
+ recordExecutionAttestation,
1646
+ recordExternalReconciliation,
1647
+ completeExternalInvocation: externalCompletion.completeExternalInvocation,
1648
+ reconcileOverdueCompletions: externalCompletion.reconcileOverdueCompletions
1649
+ };
1585
1650
  }
1586
1651
  function actionResult(invocation) {
1587
1652
  return {
@@ -1600,18 +1665,10 @@ function asApprovalStore(store) {
1600
1665
  const candidate = store;
1601
1666
  return typeof candidate.recordHitlDecision === "function" && typeof candidate.beginApprovalDecision === "function" ? store : void 0;
1602
1667
  }
1603
- function asAtomicMutationStore(store) {
1604
- const candidate = store;
1605
- return typeof candidate.transactionWithEvents === "function" ? store : void 0;
1606
- }
1607
1668
  function asOutboxStore(store) {
1608
1669
  const candidate = store;
1609
1670
  return typeof candidate.appendEventWithOutbox === "function" && typeof candidate.claimOutbox === "function" ? store : void 0;
1610
1671
  }
1611
- function asGovernanceStore(store) {
1612
- const candidate = store;
1613
- return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
1614
- }
1615
1672
  function numberValue(value) {
1616
1673
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1617
1674
  }
@@ -1647,39 +1704,9 @@ function initialState(machine) {
1647
1704
  function isTerminal(status) {
1648
1705
  return ["completed", "failed", "blocked_by_policy", "reconciliation_required", "validation_failed"].includes(status);
1649
1706
  }
1650
- function emitInvocationStatusTelemetry(invocation, status, telemetry, occurredAt, previousStatus) {
1651
- if (!status) return;
1652
- const event = status === "completed" ? { name: "invocation.completed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationCompleted } : status === "blocked_by_policy" ? { name: "invocation.policy_blocked", metricName: PLATFORM_HOST_METRIC_NAMES.invocationPolicyBlocked } : status === "validation_failed" ? { name: "invocation.validation_failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationValidationFailed } : status === "waiting_for_approval" ? { name: "invocation.approval_waited", metricName: PLATFORM_HOST_METRIC_NAMES.invocationApprovalWaited } : status === "reconciliation_required" ? { name: "invocation.reconciliation_required", metricName: PLATFORM_HOST_METRIC_NAMES.invocationReconciliationRequired } : status === "failed" ? { name: "invocation.failed", metricName: PLATFORM_HOST_METRIC_NAMES.invocationFailed } : void 0;
1653
- if (!event) return;
1654
- emitPlatformHostTelemetry(telemetry, {
1655
- kind: "event",
1656
- name: event.name,
1657
- metricName: event.metricName,
1658
- occurredAt,
1659
- tenantId: invocation.tenantId,
1660
- spaceId: invocation.spaceId,
1661
- attributes: {
1662
- ...invocation.actionId ? { actionId: invocation.actionId } : {},
1663
- ...invocation.actionVersion !== void 0 ? { actionVersion: invocation.actionVersion } : {},
1664
- ...previousStatus ? { fromStatus: previousStatus } : {}
1665
- }
1666
- });
1667
- }
1668
- function withoutPrivateHostFields(data, eventResultFields) {
1669
- return Object.fromEntries(
1670
- Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
1671
- );
1672
- }
1673
- function errorMessage(error) {
1674
- return error instanceof Error ? error.message : String(error);
1675
- }
1676
1707
  function isRecord(value) {
1677
1708
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1678
1709
  }
1679
- function lifecycleId(prefix, invocationId, key) {
1680
- const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
1681
- return `${prefix}_${invocationId}_${safeKey}`;
1682
- }
1683
1710
  function validateAuthorizationBinding(input, parameterDigest, authorityMoment, currentTime = /* @__PURE__ */ new Date()) {
1684
1711
  const binding = input.authorizationBinding;
1685
1712
  if (binding.id !== input.authorizationBindingId && input.authorizationBindingId) throw new Error("Authorization binding ID does not match authorizationBindingId.");
@@ -3537,6 +3564,181 @@ function createDurableSagaParentLifecycle(options) {
3537
3564
  };
3538
3565
  }
3539
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
+
3540
3742
  exports.ExternalCompletionError = ExternalCompletionError;
3541
3743
  exports.IdempotencyConflictError = IdempotencyConflictError;
3542
3744
  exports.MemoryPlatformHostStore = MemoryPlatformHostStore;
@@ -3557,6 +3759,8 @@ exports.getPlatformHostHealthSnapshot = getPlatformHostHealthSnapshot;
3557
3759
  exports.runOutboxRelayCycle = runOutboxRelayCycle;
3558
3760
  exports.runPlatformActionWorker = runPlatformActionWorker;
3559
3761
  exports.runPlatformActionWorkerCycle = runPlatformActionWorkerCycle;
3762
+ exports.runTenantIsolationChecks = runTenantIsolationChecks;
3763
+ exports.tenantIsolationChecks = tenantIsolationChecks;
3560
3764
  exports.toEnterpriseEventEnvelope = toEnterpriseEventEnvelope;
3561
3765
  //# sourceMappingURL=index.cjs.map
3562
3766
  //# sourceMappingURL=index.cjs.map