@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.js CHANGED
@@ -282,6 +282,221 @@ function isPromiseLike(value) {
282
282
  );
283
283
  }
284
284
 
285
+ // src/host-internals.ts
286
+ function asAtomicMutationStore(store) {
287
+ const candidate = store;
288
+ return typeof candidate.transactionWithEvents === "function" ? store : void 0;
289
+ }
290
+ function asGovernanceStore(store) {
291
+ const candidate = store;
292
+ return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
293
+ }
294
+ function withoutPrivateHostFields(data, eventResultFields) {
295
+ return Object.fromEntries(
296
+ Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
297
+ );
298
+ }
299
+ function errorMessage(error) {
300
+ return error instanceof Error ? error.message : String(error);
301
+ }
302
+ function lifecycleId(prefix, invocationId, key) {
303
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
304
+ return `${prefix}_${invocationId}_${safeKey}`;
305
+ }
306
+ function emitInvocationStatusTelemetry(invocation, status, telemetry, occurredAt, previousStatus) {
307
+ if (!status) return;
308
+ 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;
309
+ if (!event) return;
310
+ emitPlatformHostTelemetry(telemetry, {
311
+ kind: "event",
312
+ name: event.name,
313
+ metricName: event.metricName,
314
+ occurredAt,
315
+ tenantId: invocation.tenantId,
316
+ spaceId: invocation.spaceId,
317
+ attributes: {
318
+ ...invocation.actionId ? { actionId: invocation.actionId } : {},
319
+ ...invocation.actionVersion !== void 0 ? { actionVersion: invocation.actionVersion } : {},
320
+ ...previousStatus ? { fromStatus: previousStatus } : {}
321
+ }
322
+ });
323
+ }
324
+
325
+ // src/external-completion.ts
326
+ function createExternalCompletionLifecycle(deps) {
327
+ const { now, actionResolver, appendEvent, persistInvocation, actionResult: actionResult2, withConsistency, eventResultFields } = deps;
328
+ async function completeExternalInvocation(actionInvocationId, tenantId, spaceId, completion) {
329
+ if (typeof completion.externalReference !== "string" || completion.externalReference.trim() === "") {
330
+ throw new ExternalCompletionError("invalid_completion", "External completion must name the external reference it completes.");
331
+ }
332
+ if (completion.outcome !== "completed" && completion.outcome !== "failed") {
333
+ throw new ExternalCompletionError("invalid_completion", `External completion outcome must be "completed" or "failed", received "${String(completion.outcome)}".`);
334
+ }
335
+ const atomicStore = asAtomicMutationStore(deps.store);
336
+ if (atomicStore) {
337
+ return atomicStore.transactionWithEvents(async (transaction) => {
338
+ const read2 = transaction.getActionInvocationForUpdate ? await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId) : await deps.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
339
+ return settleExternalCompletion(read2, actionInvocationId, completion, {
340
+ update: (patch) => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch),
341
+ transaction
342
+ });
343
+ });
344
+ }
345
+ const read = await deps.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
346
+ return settleExternalCompletion(read, actionInvocationId, completion, {
347
+ update: (patch) => deps.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch)
348
+ });
349
+ }
350
+ async function settleExternalCompletion(invocation, actionInvocationId, completion, writer) {
351
+ if (!invocation) throw new ExternalCompletionError("not_found", `ActionInvocation not found: ${actionInvocationId}`);
352
+ const recorded = invocation.externalCompletion;
353
+ const stillExecuting = !recorded && (invocation.status === "pending" || invocation.status === "running" && (invocation.leaseOwner !== void 0 || invocation.pendingCompletion?.parkedAt === void 0));
354
+ if (stillExecuting) {
355
+ throw new ExternalCompletionError("still_executing", `ActionInvocation ${actionInvocationId} is still executing; retry the completion after it parks.`);
356
+ }
357
+ if (recorded) {
358
+ if (recorded.externalReference !== completion.externalReference) {
359
+ throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.externalReference}", not "${completion.externalReference}".`);
360
+ }
361
+ if (completion.provider !== void 0 && recorded.provider !== void 0 && completion.provider !== recorded.provider) {
362
+ throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.provider}", not "${completion.provider}".`);
363
+ }
364
+ const incomingDigest = completionDigest(completion, recorded.provider ?? completion.provider);
365
+ if (recorded.digest === incomingDigest) {
366
+ return withConsistency(actionResult2(invocation), invocation.actionId);
367
+ }
368
+ 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}.`;
369
+ await recordCompletionReconciliation(invocation, { ...completion, kind: `contradicted:${incomingDigest.slice(0, 16)}` }, message);
370
+ await appendEvent(invocation, {
371
+ eventType: "ExternalOperationContradicted",
372
+ subjectType: "AdapterInvocation",
373
+ subjectId: invocation.pendingCompletion?.adapterInvocationId ?? actionInvocationId,
374
+ payload: { externalReference: completion.externalReference, recorded: recorded.outcome, reported: completion.outcome }
375
+ }, `external:${completion.externalReference}:contradicted:${completion.outcome}`, 1, writer.transaction);
376
+ await writer.update({ status: "reconciliation_required", error: message });
377
+ emitInvocationStatusTelemetry(invocation, "reconciliation_required", deps.telemetry, now(), invocation.status);
378
+ return withConsistency(actionResult2({ ...invocation, status: "reconciliation_required", error: message }), invocation.actionId);
379
+ }
380
+ const pending = invocation.pendingCompletion;
381
+ if (!pending) {
382
+ const action = actionResolver(invocation.actionId);
383
+ if ((action?.execution?.completion ?? "immediate") === "immediate") {
384
+ throw new ExternalCompletionError("immediate_contract", `Action ${invocation.actionId} declares immediate completion; nothing external completes it.`);
385
+ }
386
+ throw new ExternalCompletionError("not_awaiting", `ActionInvocation ${actionInvocationId} is not awaiting an external completion.`);
387
+ }
388
+ if (pending.externalReference !== completion.externalReference) {
389
+ throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} awaits "${pending.externalReference}", not "${completion.externalReference}".`);
390
+ }
391
+ if (completion.provider !== void 0 && completion.provider !== pending.provider) {
392
+ throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} awaits completion from "${pending.provider}", not "${completion.provider}".`);
393
+ }
394
+ const recordedAt = now();
395
+ const externalCompletion = {
396
+ ...completion,
397
+ provider: completion.provider ?? pending.provider,
398
+ observedAt: completion.observedAt instanceof Date ? completion.observedAt.toISOString() : completion.observedAt,
399
+ recordedAt,
400
+ digest: completionDigest(completion, completion.provider ?? pending.provider)
401
+ };
402
+ const subject = { subjectType: "AdapterInvocation", subjectId: pending.adapterInvocationId };
403
+ const settles = invocation.status === "running" && pending.parkedAt !== void 0 || invocation.status === "reconciliation_required" && invocation.error?.includes("did not complete by") === true;
404
+ if (completion.outcome === "completed") {
405
+ const action = actionResolver(invocation.actionId);
406
+ const merged = withoutPrivateHostFields({ ...invocation.result, ...completion.result ?? {} }, eventResultFields);
407
+ if (action?.resultSchema) {
408
+ const parsedResult = action.resultSchema.safeParse(merged);
409
+ if (!parsedResult.success) {
410
+ throw new ExternalCompletionError("result_invalid", `External completion result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
411
+ }
412
+ }
413
+ await deps.store.updateAdapterInvocation(pending.adapterInvocationId, {
414
+ ...completion.result ? { output: withoutPrivateHostFields(completion.result, eventResultFields) } : {},
415
+ updatedAt: recordedAt
416
+ });
417
+ await appendEvent(invocation, {
418
+ eventType: "ExternalOperationCompleted",
419
+ ...subject,
420
+ payload: { externalReference: pending.externalReference, provider: pending.provider, ...completion.result ? { result: withoutPrivateHostFields(completion.result, eventResultFields) } : {} }
421
+ }, `external:${pending.adapterInvocationId}:completed`, 1, writer.transaction);
422
+ const patch2 = settles ? { status: "completed", result: merged, externalCompletion, pendingCompletion: void 0, error: void 0 } : { externalCompletion, pendingCompletion: void 0 };
423
+ await writer.update(patch2);
424
+ if (settles) emitInvocationStatusTelemetry(invocation, "completed", deps.telemetry, now(), invocation.status);
425
+ return withConsistency(actionResult2({ ...invocation, ...patch2, result: settles ? merged : invocation.result, ...settles ? { error: void 0 } : {} }), invocation.actionId);
426
+ }
427
+ const error = completion.error ?? `External operation "${pending.externalReference}" failed.`;
428
+ await deps.store.updateAdapterInvocation(pending.adapterInvocationId, { status: "failed", error, updatedAt: recordedAt });
429
+ await appendEvent(invocation, {
430
+ eventType: "ExternalOperationFailed",
431
+ ...subject,
432
+ payload: { externalReference: pending.externalReference, provider: pending.provider, error }
433
+ }, `external:${pending.adapterInvocationId}:failed`, 1, writer.transaction);
434
+ const patch = settles ? { status: "failed", error, externalCompletion, pendingCompletion: void 0 } : { externalCompletion, pendingCompletion: void 0 };
435
+ await writer.update(patch);
436
+ if (settles) emitInvocationStatusTelemetry(invocation, "failed", deps.telemetry, now(), invocation.status);
437
+ return withConsistency(actionResult2({ ...invocation, ...patch }), invocation.actionId);
438
+ }
439
+ function completionDigest(completion, provider) {
440
+ return createHash("sha256").update(canonicalJson({
441
+ provider: provider ?? null,
442
+ externalReference: completion.externalReference,
443
+ outcome: completion.outcome,
444
+ result: completion.result ?? null,
445
+ error: completion.error ?? null,
446
+ evidenceReferences: completion.evidenceReferences ?? null
447
+ })).digest("hex");
448
+ }
449
+ async function recordCompletionReconciliation(invocation, completion, reason) {
450
+ const governanceStore = asGovernanceStore(deps.store);
451
+ if (!governanceStore) throw new Error("External completion reconciliation requires a governance-capable store.");
452
+ const provider = completion.provider ?? invocation.pendingCompletion?.provider ?? invocation.externalCompletion?.provider ?? "external";
453
+ const seed = createHash("sha256").update(`${provider}\0${completion.externalReference}\0${completion.kind ?? "refused"}`).digest("hex").slice(0, 32);
454
+ await governanceStore.appendExternalReconciliation({
455
+ id: lifecycleId("rec", invocation.id, `ext:${seed}`),
456
+ actionInvocationId: invocation.id,
457
+ tenantId: invocation.tenantId,
458
+ spaceId: invocation.spaceId,
459
+ status: "pending",
460
+ provider,
461
+ externalOperationId: completion.externalReference,
462
+ attempt: 1,
463
+ reason,
464
+ ...completion.evidenceReferences ? { evidenceReferences: completion.evidenceReferences } : {},
465
+ observedAt: now()
466
+ });
467
+ }
468
+ async function reconcileOverdueCompletions(input = {}) {
469
+ const recoverable = deps.store;
470
+ if (!recoverable.listActionInvocations) {
471
+ throw new Error("Overdue completion reconciliation requires a store that can list invocations.");
472
+ }
473
+ if (!asGovernanceStore(deps.store)) {
474
+ throw new Error("Overdue completion reconciliation requires a governance-capable store to record its findings.");
475
+ }
476
+ const current = input.now ?? now();
477
+ const candidates = await recoverable.listActionInvocations({
478
+ statuses: ["running"],
479
+ completionDueBefore: current,
480
+ unleased: true,
481
+ ...input.tenantId ? { tenantId: input.tenantId } : {},
482
+ ...input.spaceId ? { spaceId: input.spaceId } : {},
483
+ limit: Math.max(1, Math.min(input.limit ?? 100, 1e3))
484
+ });
485
+ const overdue = [];
486
+ for (const invocation of candidates) {
487
+ const pending = invocation.pendingCompletion;
488
+ if (!pending?.dueAt || pending.dueAt.getTime() > current.getTime()) continue;
489
+ if (invocation.leaseOwner) continue;
490
+ const message = `External operation "${pending.externalReference}" from ${pending.provider} did not complete by ${pending.dueAt.toISOString()}.`;
491
+ await recordCompletionReconciliation(invocation, { externalReference: pending.externalReference, provider: pending.provider, kind: "overdue" }, message);
492
+ await persistInvocation(invocation, { status: "reconciliation_required", error: message });
493
+ overdue.push(invocation.id);
494
+ }
495
+ return { overdue };
496
+ }
497
+ return { completeExternalInvocation, reconcileOverdueCompletions, recordCompletionReconciliation };
498
+ }
499
+
285
500
  // src/host.ts
286
501
  var DEFAULT_EXTRACT_EVENTS = (data) => {
287
502
  const value = data._events;
@@ -498,24 +713,24 @@ function createGovernedActionHost(options) {
498
713
  }
499
714
  const action = actionResolver(invocation.actionId);
500
715
  if (!action) {
501
- return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
716
+ return fail2(invocation, "failed", `Unknown action: ${invocation.actionId}`);
502
717
  }
503
718
  if (action.kind === "saga") {
504
- return fail(
719
+ return fail2(
505
720
  invocation,
506
721
  "failed",
507
722
  `Saga action ${invocation.actionId} requires a durable PlatformActionDispatcher and cannot execute as an atomic Host action.`
508
723
  );
509
724
  }
510
725
  if (resumingRunningInvocation && invocation.attemptCount === 0 && !invocation.leaseOwner && !action.idempotent) {
511
- return fail(
726
+ return fail2(
512
727
  invocation,
513
728
  "failed",
514
729
  `Interrupted inline action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`
515
730
  );
516
731
  }
517
732
  if (invocation.attemptCount > 1 && !action.idempotent) {
518
- return fail(
733
+ return fail2(
519
734
  invocation,
520
735
  "failed",
521
736
  `Interrupted action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`
@@ -539,12 +754,12 @@ function createGovernedActionHost(options) {
539
754
  const parsed = action.schema.safeParse(invocation.parameters);
540
755
  if (!parsed.success) {
541
756
  const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
542
- return fail(invocation, "validation_failed", message);
757
+ return fail2(invocation, "validation_failed", message);
543
758
  }
544
759
  if (options.resolveMutationGovernance && !invocation.mutationFootprint) {
545
760
  const governanceStore2 = asGovernanceStore(options.store);
546
761
  if (!governanceStore2) {
547
- return fail(invocation, "failed", "The configured mutation-governance resolver requires a governance-capable store.");
762
+ return fail2(invocation, "failed", "The configured mutation-governance resolver requires a governance-capable store.");
548
763
  }
549
764
  const context = await options.resolveMutationGovernance({
550
765
  actionInvocationId,
@@ -562,7 +777,7 @@ function createGovernedActionHost(options) {
562
777
  const hitlParameters = isRecord(parsed.data) ? parsed.data : invocation.parameters;
563
778
  const approvalStore = asApprovalStore(options.store);
564
779
  if (!approvalStore) {
565
- return fail(
780
+ return fail2(
566
781
  invocation,
567
782
  "failed",
568
783
  "The configured HITL evaluator requires a store with approval persistence support."
@@ -593,7 +808,7 @@ function createGovernedActionHost(options) {
593
808
  );
594
809
  }
595
810
  if (invocation.hitlRoute === "rejected") {
596
- return fail(
811
+ return fail2(
597
812
  invocation,
598
813
  "failed",
599
814
  `HITL rejected: ${invocation.hitlReason ?? "no reason provided"}`
@@ -612,7 +827,7 @@ function createGovernedActionHost(options) {
612
827
  const authorizationInput = toAuthorizationInput(action, invocation);
613
828
  const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : invocation.executionReason ?? "initial");
614
829
  if (!await options.authorization.checkEntitlement(authorizationInput)) {
615
- return fail(
830
+ return fail2(
616
831
  invocation,
617
832
  "failed",
618
833
  `Module "${action.namespace}" is no longer enabled for tenant ${tenantId}`
@@ -652,7 +867,7 @@ function createGovernedActionHost(options) {
652
867
  }) : await options.authorization.authorize(authorizationInput);
653
868
  if (!executionAuthorized) {
654
869
  if (!action.execution) {
655
- return fail(invocation, "failed", `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`);
870
+ return fail2(invocation, "failed", `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`);
656
871
  }
657
872
  const reconciliation = {
658
873
  kind: "authorization_denied",
@@ -702,12 +917,12 @@ function createGovernedActionHost(options) {
702
917
  subjectId: actionInvocationId,
703
918
  payload: { actionId: action.actionId, policyId: aggregate.policyId, reason }
704
919
  }, `compliance:${aggregate.policyId}`);
705
- return fail(invocation, "blocked_by_policy", reason);
920
+ return fail2(invocation, "blocked_by_policy", reason);
706
921
  }
707
922
  if (options.resolvePolicyObligations) {
708
923
  const governanceStore2 = asGovernanceStore(options.store);
709
924
  if (!governanceStore2) {
710
- return fail(invocation, "failed", "Policy obligations require a governance-capable store.");
925
+ return fail2(invocation, "failed", "Policy obligations require a governance-capable store.");
711
926
  }
712
927
  const existing = await governanceStore2.listPolicyObligations(actionInvocationId, tenantId, spaceId);
713
928
  if (existing.length === 0) {
@@ -822,7 +1037,7 @@ function createGovernedActionHost(options) {
822
1037
  data = executed.data;
823
1038
  domainEvents = executed.domainEvents;
824
1039
  } catch (error) {
825
- return fail(invocation, "failed", errorMessage(error));
1040
+ return fail2(invocation, "failed", errorMessage(error));
826
1041
  }
827
1042
  let pendingHandoff = invocation.pendingCompletion;
828
1043
  for (const [stepIndex, step] of (action.adapterSteps ?? []).entries()) {
@@ -1002,7 +1217,7 @@ function createGovernedActionHost(options) {
1002
1217
  subjectId: adapterEventSubject.subjectId,
1003
1218
  payload: { adapterType: step.adapterType, operation: step.operation }
1004
1219
  }, `adapter:${stepIndex}:failed`);
1005
- return fail(invocation, "failed", result2.error ?? "Adapter failed");
1220
+ return fail2(invocation, "failed", result2.error ?? "Adapter failed");
1006
1221
  }
1007
1222
  const resultRecord = result2;
1008
1223
  const output = isRecord(resultRecord.output) ? resultRecord.output : Object.fromEntries(
@@ -1040,7 +1255,7 @@ function createGovernedActionHost(options) {
1040
1255
  });
1041
1256
  let error = refuse;
1042
1257
  try {
1043
- await recordCompletionReconciliation(invocation, { externalReference: reference, provider: adapter.vendor }, refuse);
1258
+ await externalCompletion.recordCompletionReconciliation(invocation, { externalReference: reference, provider: adapter.vendor }, refuse);
1044
1259
  } catch (logError) {
1045
1260
  error = `${refuse} Reconciliation log unavailable: ${errorMessage(logError)}`;
1046
1261
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { error });
@@ -1106,7 +1321,7 @@ function createGovernedActionHost(options) {
1106
1321
  error: message,
1107
1322
  updatedAt: now()
1108
1323
  });
1109
- return fail(invocation, "failed", message);
1324
+ return fail2(invocation, "failed", message);
1110
1325
  } finally {
1111
1326
  if (adapterDeadlineTimer) clearTimeout(adapterDeadlineTimer);
1112
1327
  }
@@ -1121,14 +1336,14 @@ function createGovernedActionHost(options) {
1121
1336
  await persistInvocation(invocation, { status: "reconciliation_required", error: message });
1122
1337
  let error = message;
1123
1338
  try {
1124
- await recordCompletionReconciliation(invocation, { externalReference: pendingHandoff.externalReference, provider: pendingHandoff.provider, kind: "obligations" }, message);
1339
+ await externalCompletion.recordCompletionReconciliation(invocation, { externalReference: pendingHandoff.externalReference, provider: pendingHandoff.provider, kind: "obligations" }, message);
1125
1340
  } catch (logError) {
1126
1341
  error = `${message} Reconciliation log unavailable: ${errorMessage(logError)}`;
1127
1342
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { error });
1128
1343
  }
1129
1344
  return withConsistency({ actionInvocationId, status: "reconciliation_required", error, pendingCompletion: pendingHandoff }, action.actionId);
1130
1345
  }
1131
- return fail(invocation, "failed", message);
1346
+ return fail2(invocation, "failed", message);
1132
1347
  }
1133
1348
  }
1134
1349
  const result = withoutPrivateHostFields(data, eventResultFields);
@@ -1201,7 +1416,7 @@ function createGovernedActionHost(options) {
1201
1416
  };
1202
1417
  } catch (error) {
1203
1418
  if (error instanceof RecoverableFinalizationError) throw error;
1204
- return fail(invocation, "failed", errorMessage(error));
1419
+ return fail2(invocation, "failed", errorMessage(error));
1205
1420
  }
1206
1421
  }
1207
1422
  async function resumeApprovedInvocation(actionInvocationId, tenantId, spaceId, decision) {
@@ -1220,7 +1435,7 @@ function createGovernedActionHost(options) {
1220
1435
  };
1221
1436
  }
1222
1437
  const action = actionResolver(invocation.actionId);
1223
- if (!action) return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
1438
+ if (!action) return fail2(invocation, "failed", `Unknown action: ${invocation.actionId}`);
1224
1439
  const approvalAuthorizationInput = toAuthorizationInput(action, {
1225
1440
  tenantId,
1226
1441
  spaceId,
@@ -1314,175 +1529,6 @@ function createGovernedActionHost(options) {
1314
1529
  spaceId
1315
1530
  });
1316
1531
  }
1317
- async function completeExternalInvocation(actionInvocationId, tenantId, spaceId, completion) {
1318
- if (typeof completion.externalReference !== "string" || completion.externalReference.trim() === "") {
1319
- throw new ExternalCompletionError("invalid_completion", "External completion must name the external reference it completes.");
1320
- }
1321
- if (completion.outcome !== "completed" && completion.outcome !== "failed") {
1322
- throw new ExternalCompletionError("invalid_completion", `External completion outcome must be "completed" or "failed", received "${String(completion.outcome)}".`);
1323
- }
1324
- const atomicStore = asAtomicMutationStore(options.store);
1325
- if (atomicStore) {
1326
- return atomicStore.transactionWithEvents(async (transaction) => {
1327
- const read2 = transaction.getActionInvocationForUpdate ? await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId) : await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
1328
- return settleExternalCompletion(read2, actionInvocationId, completion, {
1329
- update: (patch) => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch),
1330
- transaction
1331
- });
1332
- });
1333
- }
1334
- const read = await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
1335
- return settleExternalCompletion(read, actionInvocationId, completion, {
1336
- update: (patch) => options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, patch)
1337
- });
1338
- }
1339
- async function settleExternalCompletion(invocation, actionInvocationId, completion, writer) {
1340
- if (!invocation) throw new ExternalCompletionError("not_found", `ActionInvocation not found: ${actionInvocationId}`);
1341
- const recorded = invocation.externalCompletion;
1342
- const stillExecuting = !recorded && (invocation.status === "pending" || invocation.status === "running" && (invocation.leaseOwner !== void 0 || invocation.pendingCompletion?.parkedAt === void 0));
1343
- if (stillExecuting) {
1344
- throw new ExternalCompletionError("still_executing", `ActionInvocation ${actionInvocationId} is still executing; retry the completion after it parks.`);
1345
- }
1346
- if (recorded) {
1347
- if (recorded.externalReference !== completion.externalReference) {
1348
- throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.externalReference}", not "${completion.externalReference}".`);
1349
- }
1350
- if (completion.provider !== void 0 && recorded.provider !== void 0 && completion.provider !== recorded.provider) {
1351
- throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.provider}", not "${completion.provider}".`);
1352
- }
1353
- const incomingDigest = completionDigest(completion, recorded.provider ?? completion.provider);
1354
- if (recorded.digest === incomingDigest) {
1355
- return withConsistency(actionResult(invocation), invocation.actionId);
1356
- }
1357
- 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}.`;
1358
- await recordCompletionReconciliation(invocation, { ...completion, kind: `contradicted:${incomingDigest.slice(0, 16)}` }, message);
1359
- await appendEvent(invocation, {
1360
- eventType: "ExternalOperationContradicted",
1361
- subjectType: "AdapterInvocation",
1362
- subjectId: invocation.pendingCompletion?.adapterInvocationId ?? actionInvocationId,
1363
- payload: { externalReference: completion.externalReference, recorded: recorded.outcome, reported: completion.outcome }
1364
- }, `external:${completion.externalReference}:contradicted:${completion.outcome}`, 1, writer.transaction);
1365
- await writer.update({ status: "reconciliation_required", error: message });
1366
- emitInvocationStatusTelemetry(invocation, "reconciliation_required", options.telemetry, now(), invocation.status);
1367
- return withConsistency(actionResult({ ...invocation, status: "reconciliation_required", error: message }), invocation.actionId);
1368
- }
1369
- const pending = invocation.pendingCompletion;
1370
- if (!pending) {
1371
- const action = actionResolver(invocation.actionId);
1372
- if ((action?.execution?.completion ?? "immediate") === "immediate") {
1373
- throw new ExternalCompletionError("immediate_contract", `Action ${invocation.actionId} declares immediate completion; nothing external completes it.`);
1374
- }
1375
- throw new ExternalCompletionError("not_awaiting", `ActionInvocation ${actionInvocationId} is not awaiting an external completion.`);
1376
- }
1377
- if (pending.externalReference !== completion.externalReference) {
1378
- throw new ExternalCompletionError("reference_mismatch", `ActionInvocation ${actionInvocationId} awaits "${pending.externalReference}", not "${completion.externalReference}".`);
1379
- }
1380
- if (completion.provider !== void 0 && completion.provider !== pending.provider) {
1381
- throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} awaits completion from "${pending.provider}", not "${completion.provider}".`);
1382
- }
1383
- const recordedAt = now();
1384
- const externalCompletion = {
1385
- ...completion,
1386
- provider: completion.provider ?? pending.provider,
1387
- observedAt: completion.observedAt instanceof Date ? completion.observedAt.toISOString() : completion.observedAt,
1388
- recordedAt,
1389
- digest: completionDigest(completion, completion.provider ?? pending.provider)
1390
- };
1391
- const subject = { subjectType: "AdapterInvocation", subjectId: pending.adapterInvocationId };
1392
- const settles = invocation.status === "running" && pending.parkedAt !== void 0 || invocation.status === "reconciliation_required" && invocation.error?.includes("did not complete by") === true;
1393
- if (completion.outcome === "completed") {
1394
- const action = actionResolver(invocation.actionId);
1395
- const merged = withoutPrivateHostFields({ ...invocation.result, ...completion.result ?? {} }, eventResultFields);
1396
- if (action?.resultSchema) {
1397
- const parsedResult = action.resultSchema.safeParse(merged);
1398
- if (!parsedResult.success) {
1399
- throw new ExternalCompletionError("result_invalid", `External completion result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
1400
- }
1401
- }
1402
- await options.store.updateAdapterInvocation(pending.adapterInvocationId, {
1403
- ...completion.result ? { output: withoutPrivateHostFields(completion.result, eventResultFields) } : {},
1404
- updatedAt: recordedAt
1405
- });
1406
- await appendEvent(invocation, {
1407
- eventType: "ExternalOperationCompleted",
1408
- ...subject,
1409
- payload: { externalReference: pending.externalReference, provider: pending.provider, ...completion.result ? { result: withoutPrivateHostFields(completion.result, eventResultFields) } : {} }
1410
- }, `external:${pending.adapterInvocationId}:completed`, 1, writer.transaction);
1411
- const patch2 = settles ? { status: "completed", result: merged, externalCompletion, pendingCompletion: void 0, error: void 0 } : { externalCompletion, pendingCompletion: void 0 };
1412
- await writer.update(patch2);
1413
- if (settles) emitInvocationStatusTelemetry(invocation, "completed", options.telemetry, now(), invocation.status);
1414
- return withConsistency(actionResult({ ...invocation, ...patch2, result: settles ? merged : invocation.result, ...settles ? { error: void 0 } : {} }), invocation.actionId);
1415
- }
1416
- const error = completion.error ?? `External operation "${pending.externalReference}" failed.`;
1417
- await options.store.updateAdapterInvocation(pending.adapterInvocationId, { status: "failed", error, updatedAt: recordedAt });
1418
- await appendEvent(invocation, {
1419
- eventType: "ExternalOperationFailed",
1420
- ...subject,
1421
- payload: { externalReference: pending.externalReference, provider: pending.provider, error }
1422
- }, `external:${pending.adapterInvocationId}:failed`, 1, writer.transaction);
1423
- const patch = settles ? { status: "failed", error, externalCompletion, pendingCompletion: void 0 } : { externalCompletion, pendingCompletion: void 0 };
1424
- await writer.update(patch);
1425
- if (settles) emitInvocationStatusTelemetry(invocation, "failed", options.telemetry, now(), invocation.status);
1426
- return withConsistency(actionResult({ ...invocation, ...patch }), invocation.actionId);
1427
- }
1428
- function completionDigest(completion, provider) {
1429
- return createHash("sha256").update(canonicalJson({
1430
- provider: provider ?? null,
1431
- externalReference: completion.externalReference,
1432
- outcome: completion.outcome,
1433
- result: completion.result ?? null,
1434
- error: completion.error ?? null,
1435
- evidenceReferences: completion.evidenceReferences ?? null
1436
- })).digest("hex");
1437
- }
1438
- async function recordCompletionReconciliation(invocation, completion, reason) {
1439
- const governanceStore = asGovernanceStore(options.store);
1440
- if (!governanceStore) throw new Error("External completion reconciliation requires a governance-capable store.");
1441
- const provider = completion.provider ?? invocation.pendingCompletion?.provider ?? invocation.externalCompletion?.provider ?? "external";
1442
- const seed = createHash("sha256").update(`${provider}\0${completion.externalReference}\0${completion.kind ?? "refused"}`).digest("hex").slice(0, 32);
1443
- await governanceStore.appendExternalReconciliation({
1444
- id: lifecycleId("rec", invocation.id, `ext:${seed}`),
1445
- actionInvocationId: invocation.id,
1446
- tenantId: invocation.tenantId,
1447
- spaceId: invocation.spaceId,
1448
- status: "pending",
1449
- provider,
1450
- externalOperationId: completion.externalReference,
1451
- attempt: 1,
1452
- reason,
1453
- ...completion.evidenceReferences ? { evidenceReferences: completion.evidenceReferences } : {},
1454
- observedAt: now()
1455
- });
1456
- }
1457
- async function reconcileOverdueCompletions(input = {}) {
1458
- const recoverable = options.store;
1459
- if (!recoverable.listActionInvocations) {
1460
- throw new Error("Overdue completion reconciliation requires a store that can list invocations.");
1461
- }
1462
- if (!asGovernanceStore(options.store)) {
1463
- throw new Error("Overdue completion reconciliation requires a governance-capable store to record its findings.");
1464
- }
1465
- const current = input.now ?? now();
1466
- const candidates = await recoverable.listActionInvocations({
1467
- statuses: ["running"],
1468
- completionDueBefore: current,
1469
- unleased: true,
1470
- ...input.tenantId ? { tenantId: input.tenantId } : {},
1471
- ...input.spaceId ? { spaceId: input.spaceId } : {},
1472
- limit: Math.max(1, Math.min(input.limit ?? 100, 1e3))
1473
- });
1474
- const overdue = [];
1475
- for (const invocation of candidates) {
1476
- const pending = invocation.pendingCompletion;
1477
- if (!pending?.dueAt || pending.dueAt.getTime() > current.getTime()) continue;
1478
- if (invocation.leaseOwner) continue;
1479
- const message = `External operation "${pending.externalReference}" from ${pending.provider} did not complete by ${pending.dueAt.toISOString()}.`;
1480
- await recordCompletionReconciliation(invocation, { externalReference: pending.externalReference, provider: pending.provider, kind: "overdue" }, message);
1481
- await persistInvocation(invocation, { status: "reconciliation_required", error: message });
1482
- overdue.push(invocation.id);
1483
- }
1484
- return { overdue };
1485
- }
1486
1532
  function declaredConsistency(actionId) {
1487
1533
  return actionResolver(actionId)?.execution?.consistency;
1488
1534
  }
@@ -1542,7 +1588,7 @@ function createGovernedActionHost(options) {
1542
1588
  await (transaction ?? options.store).appendEvent(envelope);
1543
1589
  }
1544
1590
  }
1545
- async function fail(invocation, status, error) {
1591
+ async function fail2(invocation, status, error) {
1546
1592
  await persistInvocation(invocation, { status, error });
1547
1593
  return {
1548
1594
  actionInvocationId: invocation.id,
@@ -1579,7 +1625,26 @@ function createGovernedActionHost(options) {
1579
1625
  );
1580
1626
  emitInvocationStatusTelemetry(invocation, patch.status, options.telemetry, now(), previousStatus);
1581
1627
  }
1582
- return { submitAction, executeInvocation, resumeApprovedInvocation, recordExecutionAttestation, recordExternalReconciliation, completeExternalInvocation, reconcileOverdueCompletions };
1628
+ const externalCompletion = createExternalCompletionLifecycle({
1629
+ store: options.store,
1630
+ telemetry: options.telemetry,
1631
+ eventResultFields,
1632
+ now,
1633
+ actionResolver,
1634
+ appendEvent,
1635
+ persistInvocation,
1636
+ actionResult,
1637
+ withConsistency
1638
+ });
1639
+ return {
1640
+ submitAction,
1641
+ executeInvocation,
1642
+ resumeApprovedInvocation,
1643
+ recordExecutionAttestation,
1644
+ recordExternalReconciliation,
1645
+ completeExternalInvocation: externalCompletion.completeExternalInvocation,
1646
+ reconcileOverdueCompletions: externalCompletion.reconcileOverdueCompletions
1647
+ };
1583
1648
  }
1584
1649
  function actionResult(invocation) {
1585
1650
  return {
@@ -1598,18 +1663,10 @@ function asApprovalStore(store) {
1598
1663
  const candidate = store;
1599
1664
  return typeof candidate.recordHitlDecision === "function" && typeof candidate.beginApprovalDecision === "function" ? store : void 0;
1600
1665
  }
1601
- function asAtomicMutationStore(store) {
1602
- const candidate = store;
1603
- return typeof candidate.transactionWithEvents === "function" ? store : void 0;
1604
- }
1605
1666
  function asOutboxStore(store) {
1606
1667
  const candidate = store;
1607
1668
  return typeof candidate.appendEventWithOutbox === "function" && typeof candidate.claimOutbox === "function" ? store : void 0;
1608
1669
  }
1609
- function asGovernanceStore(store) {
1610
- const candidate = store;
1611
- return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
1612
- }
1613
1670
  function numberValue(value) {
1614
1671
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1615
1672
  }
@@ -1645,39 +1702,9 @@ function initialState(machine) {
1645
1702
  function isTerminal(status) {
1646
1703
  return ["completed", "failed", "blocked_by_policy", "reconciliation_required", "validation_failed"].includes(status);
1647
1704
  }
1648
- function emitInvocationStatusTelemetry(invocation, status, telemetry, occurredAt, previousStatus) {
1649
- if (!status) return;
1650
- 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;
1651
- if (!event) return;
1652
- emitPlatformHostTelemetry(telemetry, {
1653
- kind: "event",
1654
- name: event.name,
1655
- metricName: event.metricName,
1656
- occurredAt,
1657
- tenantId: invocation.tenantId,
1658
- spaceId: invocation.spaceId,
1659
- attributes: {
1660
- ...invocation.actionId ? { actionId: invocation.actionId } : {},
1661
- ...invocation.actionVersion !== void 0 ? { actionVersion: invocation.actionVersion } : {},
1662
- ...previousStatus ? { fromStatus: previousStatus } : {}
1663
- }
1664
- });
1665
- }
1666
- function withoutPrivateHostFields(data, eventResultFields) {
1667
- return Object.fromEntries(
1668
- Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
1669
- );
1670
- }
1671
- function errorMessage(error) {
1672
- return error instanceof Error ? error.message : String(error);
1673
- }
1674
1705
  function isRecord(value) {
1675
1706
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1676
1707
  }
1677
- function lifecycleId(prefix, invocationId, key) {
1678
- const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
1679
- return `${prefix}_${invocationId}_${safeKey}`;
1680
- }
1681
1708
  function validateAuthorizationBinding(input, parameterDigest, authorityMoment, currentTime = /* @__PURE__ */ new Date()) {
1682
1709
  const binding = input.authorizationBinding;
1683
1710
  if (binding.id !== input.authorizationBindingId && input.authorizationBindingId) throw new Error("Authorization binding ID does not match authorizationBindingId.");
@@ -3535,6 +3562,181 @@ function createDurableSagaParentLifecycle(options) {
3535
3562
  };
3536
3563
  }
3537
3564
 
3538
- export { ExternalCompletionError, IdempotencyConflictError, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PLATFORM_HOST_HEALTH_CONTRACT_VERSION, PLATFORM_HOST_METRIC_NAMES, PostgresPlatformHostStore, applyPostgresMigrations, canonicalJson, cloneOutboxRecord, createDurableSagaParentLifecycle, createGovernedActionHost, createStoreBackedActionDispatcher, digestParameters, emitPlatformHostTelemetry, getPlatformHostHealthSnapshot, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, toEnterpriseEventEnvelope };
3565
+ // src/tenant-isolation.ts
3566
+ function fail(message) {
3567
+ throw new Error(message);
3568
+ }
3569
+ function submitInput(subject, scope, idempotencyKey) {
3570
+ return {
3571
+ actionId: subject.actionId,
3572
+ parameters: subject.parameters ?? {},
3573
+ tenantId: scope.tenantId,
3574
+ spaceId: scope.spaceId,
3575
+ actorId: scope.actorId,
3576
+ actorType: scope.actorType,
3577
+ idempotencyKey
3578
+ };
3579
+ }
3580
+ function tenantIsolationChecks() {
3581
+ return [
3582
+ {
3583
+ id: "fabric.tenancy.invocation-scoped-read.v1",
3584
+ async run(subject) {
3585
+ await subject.reset?.();
3586
+ if (subject.ours.tenantId === subject.theirs.tenantId) fail("the two scopes must be in different tenants");
3587
+ const ours = await subject.host.submitAction(submitInput(subject, subject.ours, "iso:read"));
3588
+ const crossRead = await subject.store.getActionInvocation(ours.actionInvocationId, subject.theirs.tenantId, subject.theirs.spaceId);
3589
+ if (crossRead !== void 0) fail("an invocation was readable under another tenant's scope");
3590
+ const shape = (record) => JSON.stringify({ status: record?.status, result: record?.result, error: record?.error, attemptCount: record?.attemptCount, updatedAt: record?.updatedAt });
3591
+ const before = await subject.store.getActionInvocation(ours.actionInvocationId, subject.ours.tenantId, subject.ours.spaceId);
3592
+ if (!before) fail("our own invocation was not readable under our scope, so the check proves nothing");
3593
+ const untouched = shape(before);
3594
+ let crossExecute;
3595
+ try {
3596
+ crossExecute = await subject.host.executeInvocation(ours.actionInvocationId, subject.theirs.tenantId, subject.theirs.spaceId);
3597
+ } catch {
3598
+ crossExecute = void 0;
3599
+ }
3600
+ if (crossExecute !== void 0) fail("an invocation was executable under another tenant's scope");
3601
+ const after = await subject.store.getActionInvocation(ours.actionInvocationId, subject.ours.tenantId, subject.ours.spaceId);
3602
+ if (untouched !== shape(after)) fail("an execution attempted under another tenant's scope changed our invocation");
3603
+ return ["invocation readable under our scope, invisible and untouched under the other tenant's"];
3604
+ }
3605
+ },
3606
+ {
3607
+ id: "fabric.tenancy.idempotency-key-scoped.v1",
3608
+ async run(subject) {
3609
+ await subject.reset?.();
3610
+ const ours = await subject.host.submitAction(submitInput(subject, subject.ours, "iso:shared-key"));
3611
+ const again = await subject.host.submitAction(submitInput(subject, subject.ours, "iso:shared-key"));
3612
+ 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");
3613
+ const theirs = await subject.host.submitAction(submitInput(subject, subject.theirs, "iso:shared-key"));
3614
+ if (ours.actionInvocationId === theirs.actionInvocationId) fail("the same idempotency key in two tenants resolved to one invocation");
3615
+ return ["one idempotency key: one invocation within a tenant, two across tenants"];
3616
+ }
3617
+ },
3618
+ {
3619
+ id: "fabric.tenancy.events-scoped.v1",
3620
+ async run(subject) {
3621
+ await subject.reset?.();
3622
+ const ours = await subject.host.submitAction(submitInput(subject, subject.ours, "iso:events"));
3623
+ const theirEvents = await subject.store.listEvents(subject.theirs.tenantId, subject.theirs.spaceId);
3624
+ const leaked = theirEvents.filter((event) => event.tenantId === subject.ours.tenantId || event.actionInvocationId === ours.actionInvocationId);
3625
+ if (leaked.length > 0) fail(`${leaked.length} of our events appeared in the other tenant's stream`);
3626
+ const ourEvents = await subject.store.listEvents(subject.ours.tenantId, subject.ours.spaceId);
3627
+ 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");
3628
+ if (ourEvents.some((event) => event.tenantId !== subject.ours.tenantId)) fail("another tenant's event appeared in our stream");
3629
+ return ["event streams carry their own tenant's events and only those"];
3630
+ }
3631
+ },
3632
+ {
3633
+ id: "fabric.tenancy.worker-claim-scoped.v1",
3634
+ async run(subject) {
3635
+ await subject.reset?.();
3636
+ const probeSpace = `iso-probe-${Date.now()}`;
3637
+ const created = await subject.store.createActionInvocation({
3638
+ id: `iso_pending_${Date.now()}`,
3639
+ tenantId: subject.ours.tenantId,
3640
+ spaceId: probeSpace,
3641
+ actionId: subject.actionId,
3642
+ actionVersion: 1,
3643
+ actorId: subject.ours.actorId,
3644
+ actorType: subject.ours.actorType,
3645
+ status: "pending",
3646
+ parameters: subject.parameters ?? {},
3647
+ result: {},
3648
+ correlationId: `iso-corr-${Date.now()}`
3649
+ });
3650
+ try {
3651
+ const theirs = await subject.store.claimActionInvocations({
3652
+ workerId: "iso-worker",
3653
+ leaseDurationMs: 1e3,
3654
+ limit: 1e3,
3655
+ tenantId: subject.theirs.tenantId,
3656
+ spaceId: probeSpace
3657
+ });
3658
+ if (theirs.some((row) => row.id === created.id)) fail("a worker scoped to another tenant claimed our pending invocation");
3659
+ const ours = await subject.store.claimActionInvocations({
3660
+ workerId: "iso-worker",
3661
+ leaseDurationMs: 1e3,
3662
+ limit: 1e3,
3663
+ tenantId: subject.ours.tenantId,
3664
+ spaceId: probeSpace
3665
+ });
3666
+ 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");
3667
+ } finally {
3668
+ await subject.store.updateActionInvocation(created.id, subject.ours.tenantId, probeSpace, {
3669
+ status: "failed",
3670
+ error: "tenant isolation probe; never executed"
3671
+ });
3672
+ }
3673
+ return ["tenant-scoped claim takes our work and leaves other tenants' alone"];
3674
+ }
3675
+ },
3676
+ {
3677
+ id: "fabric.tenancy.completion-scoped.v1",
3678
+ async run(subject) {
3679
+ await subject.reset?.();
3680
+ const ours = await subject.host.submitAction(submitInput(subject, subject.ours, "iso:complete"));
3681
+ let outcome;
3682
+ try {
3683
+ await subject.host.completeExternalInvocation(ours.actionInvocationId, subject.theirs.tenantId, subject.theirs.spaceId, {
3684
+ externalReference: "iso-ref",
3685
+ outcome: "completed",
3686
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
3687
+ });
3688
+ outcome = "accepted";
3689
+ } catch (error) {
3690
+ const refusal = error.refusal;
3691
+ outcome = refusal ?? "threw";
3692
+ }
3693
+ if (outcome !== "not_found") fail(`a completion under another tenant's scope was ${outcome}; it must be refused as not_found`);
3694
+ let mine;
3695
+ try {
3696
+ await subject.host.completeExternalInvocation(ours.actionInvocationId, subject.ours.tenantId, subject.ours.spaceId, {
3697
+ externalReference: "iso-ref",
3698
+ outcome: "completed",
3699
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
3700
+ });
3701
+ mine = "accepted";
3702
+ } catch (error) {
3703
+ mine = error.refusal ?? "threw";
3704
+ }
3705
+ if (mine === "not_found") fail("a completion under our own scope was refused as not_found, so the check proves nothing");
3706
+ return ["completion under the other tenant's scope refused as not_found; found under ours"];
3707
+ }
3708
+ },
3709
+ {
3710
+ id: "fabric.tenancy.projection-scoped.v1",
3711
+ async run(subject) {
3712
+ if (!subject.read) return ["skipped: subject lends no read"];
3713
+ await subject.reset?.();
3714
+ await subject.host.submitAction(submitInput(subject, subject.ours, "iso:projection"));
3715
+ const ours = await subject.read(subject.ours);
3716
+ const theirs = await subject.read(subject.theirs);
3717
+ if (!ours.allowed) fail("our own viewer was denied the read");
3718
+ if (ours.rows.length === 0) fail("our own viewer saw nothing, so the check proves nothing");
3719
+ 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");
3720
+ const leaked = theirs.allowed ? theirs.rows.filter((row) => row?.tenantId === subject.ours.tenantId) : [];
3721
+ if (leaked.length > 0) fail(`a viewer in the other tenant saw ${leaked.length} of our rows`);
3722
+ return [theirs.allowed ? "other tenant's viewer sees none of our rows" : "other tenant's viewer is denied"];
3723
+ }
3724
+ }
3725
+ ];
3726
+ }
3727
+ async function runTenantIsolationChecks(subject) {
3728
+ const checks = [];
3729
+ for (const check of tenantIsolationChecks()) {
3730
+ try {
3731
+ const evidence = await check.run(subject);
3732
+ checks.push({ id: check.id, status: evidence[0]?.startsWith("skipped") ? "skipped" : "passed", evidence });
3733
+ } catch (error) {
3734
+ checks.push({ id: check.id, status: "failed", evidence: [], error: error instanceof Error ? error.message : String(error) });
3735
+ }
3736
+ }
3737
+ return { passed: checks.every((check) => check.status !== "failed"), checks };
3738
+ }
3739
+
3740
+ export { ExternalCompletionError, IdempotencyConflictError, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PLATFORM_HOST_HEALTH_CONTRACT_VERSION, PLATFORM_HOST_METRIC_NAMES, PostgresPlatformHostStore, applyPostgresMigrations, canonicalJson, cloneOutboxRecord, createDurableSagaParentLifecycle, createGovernedActionHost, createStoreBackedActionDispatcher, digestParameters, emitPlatformHostTelemetry, getPlatformHostHealthSnapshot, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, runTenantIsolationChecks, tenantIsolationChecks, toEnterpriseEventEnvelope };
3539
3741
  //# sourceMappingURL=index.js.map
3540
3742
  //# sourceMappingURL=index.js.map