@fabricorg/platform-host 7.2.2 → 7.3.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
@@ -1,6 +1,8 @@
1
1
  import { AdapterRegistry, createFabricId, assertGovernanceRuntimeEvidence, FABRIC_GOVERNANCE_CONTRACT_VERSION, assertMutationGovernanceContext, evaluatePolicyDefinitions, aggregatePolicyOutcomes, evaluateStateMachineTransition, executeWithAdapterRetry, assertAdapterOutcomeConsistency } from '@fabricorg/platform';
2
+ import { performance } from 'perf_hooks';
2
3
  import { assertAssemblyRuntimeCompatible } from '@fabricorg/assembly';
3
4
  import { createHash } from 'crypto';
5
+ import { types } from 'util';
4
6
 
5
7
  // src/host.ts
6
8
 
@@ -15,6 +17,19 @@ var IdempotencyConflictError = class extends Error {
15
17
  conflict;
16
18
  code = "IDEMPOTENCY_CONFLICT";
17
19
  };
20
+ var ActionAdmissionError = class extends Error {
21
+ constructor(actionId, refusal, refusalCode) {
22
+ super(`Action ${actionId} submission was not admitted (${refusal}${refusalCode ? `: ${refusalCode}` : ""}); nothing was recorded.`);
23
+ this.actionId = actionId;
24
+ this.refusal = refusal;
25
+ this.refusalCode = refusalCode;
26
+ this.name = "ActionAdmissionError";
27
+ }
28
+ actionId;
29
+ refusal;
30
+ refusalCode;
31
+ code = "ACTION_ADMISSION_REFUSED";
32
+ };
18
33
  var ExternalCompletionError = class extends Error {
19
34
  constructor(refusal, message) {
20
35
  super(message);
@@ -82,6 +97,7 @@ var PLATFORM_HOST_METRIC_NAMES = {
82
97
  workerCyclesCompleted: "fabric.platform.worker.cycles.completed",
83
98
  workerCyclesFailed: "fabric.platform.worker.cycles.failed",
84
99
  invocationSubmitted: "fabric.platform.invocation.submitted",
100
+ invocationAdmissionRefused: "fabric.platform.invocation.admission_refused",
85
101
  invocationExecutionStarted: "fabric.platform.invocation.execution_started",
86
102
  invocationLeaseClaimed: "fabric.platform.invocation.lease_claimed",
87
103
  invocationCompleted: "fabric.platform.invocation.completed",
@@ -97,6 +113,8 @@ var PLATFORM_HOST_METRIC_NAMES = {
97
113
  outboxPublished: "fabric.platform.outbox.published",
98
114
  outboxFailed: "fabric.platform.outbox.failed",
99
115
  outboxDeadLettered: "fabric.platform.outbox.dead_lettered",
116
+ outboxRequeued: "fabric.platform.outbox.requeued",
117
+ outboxRequeueRefused: "fabric.platform.outbox.requeue_refused",
100
118
  invocationBacklog: "fabric.platform.invocation.backlog",
101
119
  invocationRunning: "fabric.platform.invocation.running",
102
120
  invocationExpiredLeases: "fabric.platform.invocation.expired_leases",
@@ -496,6 +514,340 @@ function createExternalCompletionLifecycle(deps) {
496
514
  }
497
515
  return { completeExternalInvocation, reconcileOverdueCompletions, recordCompletionReconciliation };
498
516
  }
517
+ var SubmissionUnreadableError = class extends Error {
518
+ name = "SubmissionUnreadableError";
519
+ constructor() {
520
+ super("Platform Host submission could not be read as plain data; nothing was recorded.");
521
+ }
522
+ };
523
+ var NotJsonData = class extends Error {
524
+ };
525
+ function copyJsonData(value) {
526
+ try {
527
+ return { ok: true, value: copy(value, /* @__PURE__ */ new Set()) };
528
+ } catch {
529
+ return { ok: false };
530
+ }
531
+ }
532
+ function copy(value, ancestors) {
533
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
534
+ if (typeof value === "number") {
535
+ if (!Number.isFinite(value)) throw new NotJsonData();
536
+ return value === 0 ? 0 : value;
537
+ }
538
+ if (typeof value !== "object") throw new NotJsonData();
539
+ if (types.isProxy(value)) throw new NotJsonData();
540
+ if (ancestors.has(value)) throw new NotJsonData();
541
+ const prototype = Reflect.getPrototypeOf(value);
542
+ ancestors.add(value);
543
+ try {
544
+ if (Array.isArray(value)) {
545
+ if (prototype !== Array.prototype) throw new NotJsonData();
546
+ const length = value.length;
547
+ const out2 = [];
548
+ for (let index = 0; index < length; index += 1) {
549
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
550
+ if (descriptor === void 0) throw new NotJsonData();
551
+ const item = readOnce(value, descriptor);
552
+ if (item === void 0) throw new NotJsonData();
553
+ out2.push(copy(item, ancestors));
554
+ }
555
+ return out2;
556
+ }
557
+ if (prototype !== Object.prototype && prototype !== null) throw new NotJsonData();
558
+ const out = {};
559
+ for (const key of Reflect.ownKeys(value)) {
560
+ if (typeof key !== "string") continue;
561
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
562
+ if (descriptor === void 0 || descriptor.enumerable !== true) continue;
563
+ const item = readOnce(value, descriptor);
564
+ if (item === void 0) continue;
565
+ Reflect.defineProperty(out, key, { value: copy(item, ancestors), enumerable: true, writable: true, configurable: true });
566
+ }
567
+ return out;
568
+ } finally {
569
+ ancestors.delete(value);
570
+ }
571
+ }
572
+ function readOnce(target, descriptor) {
573
+ if ("value" in descriptor) return descriptor.value;
574
+ return descriptor.get ? Reflect.apply(descriptor.get, target, []) : void 0;
575
+ }
576
+ function snapshotSubmission(input) {
577
+ if (input === null || typeof input !== "object") throw new SubmissionUnreadableError();
578
+ const scalar = (key) => primitiveOrAbsent(readField(input, key));
579
+ const actionId = scalar("actionId");
580
+ const tenantId = scalar("tenantId");
581
+ const spaceId = scalar("spaceId");
582
+ const actorId = scalar("actorId");
583
+ const actorType = scalar("actorType");
584
+ const correlationId = scalar("correlationId");
585
+ const causationId = scalar("causationId");
586
+ const executionReason = scalar("executionReason");
587
+ const idempotencyKey = scalar("idempotencyKey");
588
+ const authorizationBindingId = scalar("authorizationBindingId");
589
+ const scalars = {
590
+ actionId,
591
+ tenantId,
592
+ spaceId,
593
+ actorId,
594
+ actorType,
595
+ ...correlationId != null ? { correlationId } : {},
596
+ ...causationId != null ? { causationId } : {},
597
+ ...executionReason != null ? { executionReason } : {},
598
+ ...idempotencyKey != null ? { idempotencyKey } : {},
599
+ ...authorizationBindingId != null ? { authorizationBindingId } : {}
600
+ };
601
+ const live = { ...scalars, parametersArePrivate: false };
602
+ Object.defineProperties(live, {
603
+ parameters: { get: () => input.parameters, enumerable: true },
604
+ provenance: { get: () => input.provenance, enumerable: true },
605
+ authorizationBinding: { get: () => input.authorizationBinding, enumerable: true }
606
+ });
607
+ return Object.freeze(live);
608
+ }
609
+ var PROVENANCE_SCALARS = ["source", "correlationId", "causationId"];
610
+ var PROVENANCE_ATTRIBUTES = ["traceAttributes", "auditAttributes"];
611
+ var BINDING_FIELDS = ["id", "tenantId", "actorId", "actionId", "parameterDigest", "resourceScope", "capturedAt", "expiresAt", "governingMoment"];
612
+ function privateSubmission(snapshot) {
613
+ const parameters = readField(snapshot, "parameters");
614
+ const provenanceRead = readField(snapshot, "provenance");
615
+ const bindingRead = readField(snapshot, "authorizationBinding");
616
+ const copied = copyJsonData(parameters);
617
+ const provenance = provenanceRead ? copyProvenance(provenanceRead) : void 0;
618
+ const authorizationBinding = bindingRead ? copyBinding(bindingRead) : void 0;
619
+ return Object.freeze({
620
+ actionId: snapshot.actionId,
621
+ tenantId: snapshot.tenantId,
622
+ spaceId: snapshot.spaceId,
623
+ actorId: snapshot.actorId,
624
+ actorType: snapshot.actorType,
625
+ ...snapshot.correlationId !== void 0 ? { correlationId: snapshot.correlationId } : {},
626
+ ...snapshot.causationId !== void 0 ? { causationId: snapshot.causationId } : {},
627
+ ...snapshot.executionReason !== void 0 ? { executionReason: snapshot.executionReason } : {},
628
+ ...snapshot.idempotencyKey !== void 0 ? { idempotencyKey: snapshot.idempotencyKey } : {},
629
+ ...snapshot.authorizationBindingId !== void 0 ? { authorizationBindingId: snapshot.authorizationBindingId } : {},
630
+ ...provenance ? { provenance } : {},
631
+ ...authorizationBinding ? { authorizationBinding } : {},
632
+ parameters: copied.ok ? copied.value : parameters,
633
+ parametersArePrivate: copied.ok
634
+ });
635
+ }
636
+ function readField(target, key) {
637
+ try {
638
+ return target[key];
639
+ } catch {
640
+ throw new SubmissionUnreadableError();
641
+ }
642
+ }
643
+ function primitiveOrAbsent(value) {
644
+ if (value !== null && (typeof value === "object" || typeof value === "function" || typeof value === "symbol")) {
645
+ throw new SubmissionUnreadableError();
646
+ }
647
+ return value;
648
+ }
649
+ function objectOf(value) {
650
+ if (value === null || typeof value !== "object") throw new SubmissionUnreadableError();
651
+ return value;
652
+ }
653
+ function copyFields(source, fields) {
654
+ const out = {};
655
+ for (const field of fields) {
656
+ const value = primitiveOrAbsent(readField(source, field));
657
+ if (value !== void 0) out[field] = value;
658
+ }
659
+ return out;
660
+ }
661
+ function copyAttributes(source, field) {
662
+ const value = readField(source, field);
663
+ if (value === void 0 || value === null) return void 0;
664
+ const map = objectOf(value);
665
+ let entries;
666
+ try {
667
+ entries = Object.entries(map);
668
+ } catch {
669
+ throw new SubmissionUnreadableError();
670
+ }
671
+ const out = {};
672
+ for (const [key, item] of entries) {
673
+ Reflect.defineProperty(out, key, { value: primitiveOrAbsent(item), enumerable: true, writable: true, configurable: true });
674
+ }
675
+ return out;
676
+ }
677
+ function copyProvenance(value) {
678
+ const source = objectOf(value);
679
+ const out = copyFields(source, PROVENANCE_SCALARS);
680
+ for (const field of PROVENANCE_ATTRIBUTES) {
681
+ const attributes = copyAttributes(source, field);
682
+ if (attributes !== void 0) out[field] = attributes;
683
+ }
684
+ return out;
685
+ }
686
+ function copyBinding(value) {
687
+ return copyFields(objectOf(value), BINDING_FIELDS);
688
+ }
689
+ function parametersCopy(snapshot) {
690
+ if (!snapshot.parametersArePrivate) throw new SubmissionUnreadableError();
691
+ const copied = copyJsonData(snapshot.parameters);
692
+ if (!copied.ok) throw new SubmissionUnreadableError();
693
+ return copied.value;
694
+ }
695
+ function liveSubmissionView(snapshot, input) {
696
+ const view = {
697
+ actionId: snapshot.actionId,
698
+ tenantId: snapshot.tenantId,
699
+ spaceId: snapshot.spaceId,
700
+ actorId: snapshot.actorId,
701
+ actorType: snapshot.actorType,
702
+ ...snapshot.correlationId !== void 0 ? { correlationId: snapshot.correlationId } : {},
703
+ ...snapshot.causationId !== void 0 ? { causationId: snapshot.causationId } : {},
704
+ ...snapshot.executionReason !== void 0 ? { executionReason: snapshot.executionReason } : {},
705
+ ...snapshot.idempotencyKey !== void 0 ? { idempotencyKey: snapshot.idempotencyKey } : {},
706
+ ...snapshot.authorizationBindingId !== void 0 ? { authorizationBindingId: snapshot.authorizationBindingId } : {}
707
+ };
708
+ const caller = input;
709
+ for (const field of ["parameters", "provenance", "authorizationBinding"]) {
710
+ Object.defineProperty(view, field, {
711
+ get: () => caller[field],
712
+ set: (value) => {
713
+ caller[field] = value;
714
+ },
715
+ enumerable: true,
716
+ configurable: true
717
+ });
718
+ }
719
+ return view;
720
+ }
721
+ function submissionView(snapshot) {
722
+ const fresh = (value) => {
723
+ if (value === void 0) return void 0;
724
+ const copied = copyJsonData(value);
725
+ if (!copied.ok) throw new SubmissionUnreadableError();
726
+ return copied.value;
727
+ };
728
+ const provenance = fresh(snapshot.provenance);
729
+ const authorizationBinding = fresh(snapshot.authorizationBinding);
730
+ return {
731
+ actionId: snapshot.actionId,
732
+ parameters: parametersCopy(snapshot),
733
+ tenantId: snapshot.tenantId,
734
+ spaceId: snapshot.spaceId,
735
+ actorId: snapshot.actorId,
736
+ actorType: snapshot.actorType,
737
+ ...snapshot.correlationId !== void 0 ? { correlationId: snapshot.correlationId } : {},
738
+ ...snapshot.causationId !== void 0 ? { causationId: snapshot.causationId } : {},
739
+ ...provenance ? { provenance } : {},
740
+ ...snapshot.executionReason !== void 0 ? { executionReason: snapshot.executionReason } : {},
741
+ ...snapshot.idempotencyKey !== void 0 ? { idempotencyKey: snapshot.idempotencyKey } : {},
742
+ ...snapshot.authorizationBindingId !== void 0 ? { authorizationBindingId: snapshot.authorizationBindingId } : {},
743
+ ...authorizationBinding ? { authorizationBinding } : {}
744
+ };
745
+ }
746
+ var MAX_REASON_LENGTH = 1e3;
747
+ function asOutboxRequeueStore(store) {
748
+ const candidate = store;
749
+ return typeof candidate.requeueOutboxDeadLetter === "function" && typeof candidate.getOutboxRecord === "function" && typeof candidate.listOutboxRequeues === "function" ? store : void 0;
750
+ }
751
+ function createOutboxRequeue(deps) {
752
+ return async function requeueOutboxDeadLetter(input) {
753
+ for (const field of ["outboxId", "tenantId", "spaceId", "actorId", "actorType"]) {
754
+ if (typeof input[field] !== "string" || input[field].trim() === "") {
755
+ throw new Error(`Outbox requeue requires ${field}.`);
756
+ }
757
+ }
758
+ if (typeof input.reason !== "string" || input.reason.trim() === "") {
759
+ throw new Error("Outbox requeue requires a reason.");
760
+ }
761
+ if (input.reason.length > MAX_REASON_LENGTH) {
762
+ throw new Error(`Outbox requeue reason must be at most ${MAX_REASON_LENGTH} characters.`);
763
+ }
764
+ if (input.availableAt !== void 0 && !(input.availableAt instanceof Date && Number.isFinite(input.availableAt.getTime()))) {
765
+ throw new Error("Outbox requeue availableAt must be a valid Date.");
766
+ }
767
+ const store = asOutboxRequeueStore(deps.store);
768
+ if (!store) throw new Error("Outbox requeue requires a store with outbox requeue support.");
769
+ const refuse = (refusal, message, currentStatus) => {
770
+ emitPlatformHostTelemetry(deps.telemetry, {
771
+ kind: "event",
772
+ name: "outbox.requeue_refused",
773
+ metricName: PLATFORM_HOST_METRIC_NAMES.outboxRequeueRefused,
774
+ occurredAt: deps.now(),
775
+ tenantId: input.tenantId,
776
+ spaceId: input.spaceId,
777
+ attributes: { refusal }
778
+ });
779
+ return { status: "refused", refusal, message, ...currentStatus ? { currentStatus } : {} };
780
+ };
781
+ const record = await store.getOutboxRecord(input.outboxId, input.tenantId, input.spaceId);
782
+ const authorization = deps.authorization;
783
+ const hasAuthorizer = typeof authorization.authorizeOutboxRequeue === "function";
784
+ const authorized = hasAuthorizer ? await authorization.authorizeOutboxRequeue({
785
+ tenantId: input.tenantId,
786
+ spaceId: input.spaceId,
787
+ actorId: input.actorId,
788
+ actorType: input.actorType,
789
+ outboxId: input.outboxId,
790
+ reason: input.reason,
791
+ ...record ? {
792
+ record: {
793
+ eventId: record.event.eventId,
794
+ eventType: record.event.eventType,
795
+ payloadClassification: record.event.payloadClassification,
796
+ status: record.status,
797
+ attemptCount: record.attemptCount
798
+ }
799
+ } : {}
800
+ }) : false;
801
+ if (authorized !== true) {
802
+ return refuse(
803
+ "not_authorized",
804
+ hasAuthorizer ? `Actor ${input.actorId} is not authorized to requeue outbox records in this scope.` : "Outbox requeue is refused because no authorizeOutboxRequeue is configured."
805
+ );
806
+ }
807
+ if (!record) return refuse("not_found", `Outbox record not found: ${input.outboxId}`);
808
+ if (record.status !== "dead_letter") {
809
+ return refuse("not_dead_letter", `Outbox record ${input.outboxId} is ${record.status}, not dead_letter.`, record.status);
810
+ }
811
+ const requeuedAt = deps.now();
812
+ const result = await store.requeueOutboxDeadLetter({
813
+ id: input.outboxId,
814
+ tenantId: input.tenantId,
815
+ spaceId: input.spaceId,
816
+ requeueId: createFabricId("orq"),
817
+ actorId: input.actorId,
818
+ actorType: input.actorType,
819
+ reason: input.reason,
820
+ requeuedAt,
821
+ availableAt: input.availableAt ?? requeuedAt,
822
+ expectedLeaseToken: record.leaseToken ?? 0
823
+ });
824
+ if (!result.applied) {
825
+ if (result.refusal === "not_found") return refuse("not_found", `Outbox record not found: ${input.outboxId}`);
826
+ if (result.refusal === "stale") {
827
+ return refuse(
828
+ "stale",
829
+ `Outbox record ${input.outboxId} changed after it was authorized; read and authorize it again.`,
830
+ result.currentStatus
831
+ );
832
+ }
833
+ return refuse(
834
+ "not_dead_letter",
835
+ `Outbox record ${input.outboxId} is ${result.currentStatus ?? "no longer dead_letter"}, not dead_letter.`,
836
+ result.currentStatus
837
+ );
838
+ }
839
+ emitPlatformHostTelemetry(deps.telemetry, {
840
+ kind: "event",
841
+ name: "outbox.requeued",
842
+ metricName: PLATFORM_HOST_METRIC_NAMES.outboxRequeued,
843
+ occurredAt: requeuedAt,
844
+ tenantId: input.tenantId,
845
+ spaceId: input.spaceId,
846
+ attributes: { priorAttempt: result.requeue.priorAttemptCount }
847
+ });
848
+ return { status: "requeued", record: result.record, requeue: result.requeue };
849
+ };
850
+ }
499
851
 
500
852
  // src/host.ts
501
853
  var DEFAULT_EXTRACT_EVENTS = (data) => {
@@ -505,6 +857,12 @@ var DEFAULT_EXTRACT_EVENTS = (data) => {
505
857
  var RecoverableFinalizationError = class extends Error {
506
858
  name = "RecoverableFinalizationError";
507
859
  };
860
+ var DEFAULT_ADMISSION_TIMEOUT_MS = 1e4;
861
+ var MAX_TIMER_MS = 2147483647;
862
+ var monotonicNow = performance.now.bind(performance);
863
+ var ADMISSION_FAILED = Object.freeze({ kind: "failed" });
864
+ var ADMISSION_TIMED_OUT = Object.freeze({ kind: "timed_out" });
865
+ var ADMISSION_REFUSAL_CODE = /^[a-z][A-Za-z0-9_.:-]{0,127}$/;
508
866
  function createGovernedActionHost(options) {
509
867
  if (options.composition) {
510
868
  if (!options.registry?.orderedModules) {
@@ -534,37 +892,51 @@ function createGovernedActionHost(options) {
534
892
  const actionResolver = configuredActionResolver;
535
893
  const stateMachineResolver = options.registry?.resolveStateMachine ?? (() => void 0);
536
894
  const eventResultFields = options.eventResultFields ?? ["_events"];
895
+ const admissionTimeoutMs = options.admissionTimeoutMs ?? DEFAULT_ADMISSION_TIMEOUT_MS;
896
+ if (typeof admissionTimeoutMs !== "number" || !Number.isInteger(admissionTimeoutMs) || admissionTimeoutMs < 1 || admissionTimeoutMs > MAX_TIMER_MS) {
897
+ throw new Error(`admissionTimeoutMs must be a whole number of milliseconds from 1 to ${MAX_TIMER_MS}.`);
898
+ }
537
899
  async function submitAction(input) {
538
- if (input.executionReason && !["initial", "offline_replay"].includes(input.executionReason)) {
539
- throw new Error(`Unsupported submitted execution reason: ${input.executionReason}`);
900
+ const read = snapshotSubmission(input);
901
+ if (read.executionReason && !["initial", "offline_replay"].includes(read.executionReason)) {
902
+ throw new Error(`Unsupported submitted execution reason: ${read.executionReason}`);
540
903
  }
541
- const action = actionResolver(input.actionId);
542
- if (!action) throw new Error(`Unknown action: ${input.actionId}`);
543
- if (action.execution?.connectivity === "online-required" && input.executionReason === "offline_replay") {
544
- throw new Error(`Action ${input.actionId} is online-required and cannot be submitted as an offline replay.`);
904
+ const action = actionResolver(read.actionId);
905
+ if (!action) throw new Error(`Unknown action: ${read.actionId}`);
906
+ const declaresAdmission = action.admitSubmission !== void 0;
907
+ const submission = declaresAdmission ? privateSubmission(read) : read;
908
+ if (action.execution?.connectivity === "online-required" && submission.executionReason === "offline_replay") {
909
+ throw new Error(`Action ${submission.actionId} is online-required and cannot be submitted as an offline replay.`);
545
910
  }
546
- if (action.execution?.sensitivity === "restricted" && Object.keys(input.provenance?.auditAttributes ?? {}).length > 0) {
547
- throw new Error(`Action ${input.actionId} declares restricted sensitivity; provenance audit attributes must not be durably recorded.`);
911
+ if (action.execution?.sensitivity === "restricted" && Object.keys(submission.provenance?.auditAttributes ?? {}).length > 0) {
912
+ throw new Error(`Action ${submission.actionId} declares restricted sensitivity; provenance audit attributes must not be durably recorded.`);
548
913
  }
549
- const authorizationInput = toAuthorizationInput(action, input);
914
+ const authorizationInput = toAuthorizationInput(action, submission);
550
915
  if (!await options.authorization.checkEntitlement(authorizationInput)) {
551
- throw new Error(`Module "${action.namespace}" is not enabled for tenant ${input.tenantId}`);
916
+ throw new Error(`Module "${action.namespace}" is not enabled for tenant ${submission.tenantId}`);
552
917
  }
553
918
  if (!await options.authorization.authorize(authorizationInput)) {
554
- throw new Error(`Actor ${input.actorId} is not authorized for action ${input.actionId}`);
919
+ throw new Error(`Actor ${submission.actorId} is not authorized for action ${submission.actionId}`);
920
+ }
921
+ if (declaresAdmission && (!submission.parametersArePrivate || !isRecord(submission.parameters))) {
922
+ refuseAdmission(action, submission, "invalid_parameters");
555
923
  }
556
924
  const actionInvocationId = createFabricId("act");
557
- if (input.correlationId && input.provenance?.correlationId && input.correlationId !== input.provenance.correlationId) throw new Error("Invocation provenance correlationId does not match submission correlationId.");
558
- if (input.causationId && input.provenance?.causationId && input.causationId !== input.provenance.causationId) throw new Error("Invocation provenance causationId does not match submission causationId.");
559
- const correlationId = input.correlationId ?? input.provenance?.correlationId ?? createFabricId("corr");
560
- const durableParameters = options.redactActionParameters ? options.redactActionParameters(input.actionId, input.parameters) : input.parameters;
561
- const parameterDigest = digestParameters(input.parameters);
562
- if (input.authorizationBinding) validateAuthorizationBinding(input, parameterDigest, action.execution?.authorityMoment, now());
925
+ if (submission.correlationId && submission.provenance?.correlationId && submission.correlationId !== submission.provenance.correlationId) throw new Error("Invocation provenance correlationId does not match submission correlationId.");
926
+ if (submission.causationId && submission.provenance?.causationId && submission.causationId !== submission.provenance.causationId) throw new Error("Invocation provenance causationId does not match submission causationId.");
927
+ const correlationId = submission.correlationId ?? submission.provenance?.correlationId ?? createFabricId("corr");
928
+ const prepare = () => {
929
+ const durableParameters2 = options.redactActionParameters ? options.redactActionParameters(submission.actionId, submission.parameters) : submission.parameters;
930
+ const parameterDigest2 = digestParameters(submission.parameters);
931
+ if (submission.authorizationBinding) validateAuthorizationBinding(submission, parameterDigest2, action.execution?.authorityMoment, now());
932
+ return { durableParameters: durableParameters2, parameterDigest: parameterDigest2 };
933
+ };
934
+ const early = declaresAdmission ? void 0 : prepare();
563
935
  const durableProvenance = normalizeProvenance(
564
- input.provenance,
936
+ submission.provenance,
565
937
  options.provenance
566
938
  );
567
- const initiatingReleaseDigest = await options.composition?.resolveInitiatingReleaseDigest?.(input);
939
+ const initiatingReleaseDigest = await options.composition?.resolveInitiatingReleaseDigest?.(declaresAdmission ? submissionView(submission) : liveSubmissionView(submission, input));
568
940
  const runtimeEvidence = {
569
941
  governanceContractVersion: FABRIC_GOVERNANCE_CONTRACT_VERSION,
570
942
  hostContractVersion: PLATFORM_HOST_CONTRACT_VERSION,
@@ -573,48 +945,51 @@ function createGovernedActionHost(options) {
573
945
  ...initiatingReleaseDigest ? { initiatingReleaseDigest } : {}
574
946
  };
575
947
  assertGovernanceRuntimeEvidence(runtimeEvidence);
948
+ const admission = declaresAdmission ? await admitSubmission(action, submission) : void 0;
949
+ const { durableParameters, parameterDigest } = early ?? prepare();
576
950
  const durableInvocation = await options.store.createActionInvocation({
577
951
  id: actionInvocationId,
578
- tenantId: input.tenantId,
579
- spaceId: input.spaceId,
580
- actionId: input.actionId,
952
+ tenantId: submission.tenantId,
953
+ spaceId: submission.spaceId,
954
+ actionId: submission.actionId,
581
955
  actionVersion: action.version,
582
- actorId: input.actorId,
583
- actorType: input.actorType,
956
+ actorId: submission.actorId,
957
+ actorType: submission.actorType,
584
958
  status: "pending",
585
959
  parameters: durableParameters,
586
960
  result: {},
587
961
  runtimeEvidence,
588
962
  correlationId,
589
- ...input.causationId ?? input.provenance?.causationId ? { causationId: input.causationId ?? input.provenance?.causationId } : {},
590
- ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
591
- ...input.idempotencyKey ? {
963
+ ...submission.causationId ?? submission.provenance?.causationId ? { causationId: submission.causationId ?? submission.provenance?.causationId } : {},
964
+ ...submission.idempotencyKey ? { idempotencyKey: submission.idempotencyKey } : {},
965
+ ...submission.idempotencyKey ? {
592
966
  parameterDigest,
593
967
  parameterDigestAlgorithm: PARAMETER_DIGEST_ALGORITHM,
594
- idempotencyActorId: input.actorId,
595
- ...input.authorizationBindingId ? { idempotencyAuthorizationBindingId: input.authorizationBindingId } : {}
968
+ idempotencyActorId: submission.actorId,
969
+ ...submission.authorizationBindingId ? { idempotencyAuthorizationBindingId: submission.authorizationBindingId } : {}
596
970
  } : {},
597
971
  ...durableProvenance ? { provenance: durableProvenance } : {},
598
- ...input.authorizationBinding ? { authorizationBinding: input.authorizationBinding } : {},
599
- ...input.executionReason ? { executionReason: input.executionReason } : {},
600
- ...input.authorizationBindingId ? { authorizationBindingId: input.authorizationBindingId } : {}
972
+ ...submission.authorizationBinding ? { authorizationBinding: submission.authorizationBinding } : {},
973
+ ...submission.executionReason ? { executionReason: submission.executionReason } : {},
974
+ ...submission.authorizationBindingId ? { authorizationBindingId: submission.authorizationBindingId } : {},
975
+ ...admission ? { admittedAt: admission.admittedAt } : {}
601
976
  });
602
977
  emitPlatformHostTelemetry(options.telemetry, {
603
978
  kind: "event",
604
979
  name: "invocation.submitted",
605
980
  metricName: PLATFORM_HOST_METRIC_NAMES.invocationSubmitted,
606
981
  occurredAt: now(),
607
- tenantId: input.tenantId,
608
- spaceId: input.spaceId,
982
+ tenantId: submission.tenantId,
983
+ spaceId: submission.spaceId,
609
984
  attributes: { actionVersion: action.version }
610
985
  });
611
- if (durableInvocation.id !== actionInvocationId && input.idempotencyKey) {
986
+ if (durableInvocation.id !== actionInvocationId && submission.idempotencyKey) {
612
987
  const conflict = idempotencyConflict(durableInvocation, {
613
- actorId: input.actorId,
614
- authorizationBindingId: input.authorizationBindingId,
988
+ actorId: submission.actorId,
989
+ authorizationBindingId: submission.authorizationBindingId,
615
990
  actionVersion: action.version,
616
991
  parameterDigest,
617
- idempotencyKey: input.idempotencyKey
992
+ idempotencyKey: submission.idempotencyKey
618
993
  });
619
994
  if (!durableInvocation.parameterDigest) {
620
995
  options.idempotency?.onLegacyRecord?.(durableInvocation);
@@ -634,8 +1009,8 @@ function createGovernedActionHost(options) {
634
1009
  const dispatched = await options.dispatcher.dispatch({
635
1010
  actionInvocationId: durableInvocation.id,
636
1011
  actionId: durableInvocation.actionId,
637
- tenantId: input.tenantId,
638
- spaceId: input.spaceId,
1012
+ tenantId: submission.tenantId,
1013
+ spaceId: submission.spaceId,
639
1014
  workflowId: durableWorkflowId
640
1015
  });
641
1016
  return withConsistency({
@@ -645,7 +1020,7 @@ function createGovernedActionHost(options) {
645
1020
  ...dispatched.runId ? { runId: dispatched.runId } : {},
646
1021
  ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
647
1022
  ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
648
- }, input.actionId);
1023
+ }, submission.actionId);
649
1024
  }
650
1025
  return withConsistency({
651
1026
  actionInvocationId: durableInvocation.id,
@@ -658,15 +1033,15 @@ function createGovernedActionHost(options) {
658
1033
  ...durableInvocation.authorizationReconciliation ? { reconciliation: durableInvocation.authorizationReconciliation } : {},
659
1034
  ...durableInvocation.adapterReconciliation ? { adapterReconciliation: durableInvocation.adapterReconciliation } : {},
660
1035
  ...durableInvocation.pendingCompletion ? { pendingCompletion: durableInvocation.pendingCompletion } : {}
661
- }, input.actionId);
1036
+ }, submission.actionId);
662
1037
  }
663
1038
  if (options.dispatcher) {
664
1039
  try {
665
1040
  const dispatched = await options.dispatcher.dispatch({
666
1041
  actionInvocationId: durableInvocation.id,
667
1042
  actionId: durableInvocation.actionId,
668
- tenantId: input.tenantId,
669
- spaceId: input.spaceId,
1043
+ tenantId: submission.tenantId,
1044
+ spaceId: submission.spaceId,
670
1045
  workflowId: durableWorkflowId
671
1046
  });
672
1047
  return withConsistency({
@@ -676,17 +1051,78 @@ function createGovernedActionHost(options) {
676
1051
  ...dispatched.runId ? { runId: dispatched.runId } : {},
677
1052
  ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
678
1053
  ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
679
- }, input.actionId);
1054
+ }, submission.actionId);
680
1055
  } catch (error) {
681
1056
  throw error;
682
1057
  }
683
1058
  }
684
1059
  const executed = await executeInvocation(
685
1060
  durableInvocation.id,
686
- input.tenantId,
687
- input.spaceId
1061
+ submission.tenantId,
1062
+ submission.spaceId
688
1063
  );
689
- return { ...withConsistency(executed, input.actionId), workflowId: durableWorkflowId };
1064
+ return { ...withConsistency(executed, submission.actionId), workflowId: durableWorkflowId };
1065
+ }
1066
+ async function admitSubmission(action, submission) {
1067
+ const refuse = (refusal, refusalCode) => refuseAdmission(action, submission, refusal, refusalCode);
1068
+ const check = action.admitSubmission;
1069
+ if (typeof check !== "function") return refuse("failed");
1070
+ const checked = parametersCopy(submission);
1071
+ const controller = new AbortController();
1072
+ let timer;
1073
+ const deadline = new Promise((resolve) => {
1074
+ timer = setTimeout(() => {
1075
+ controller.abort();
1076
+ resolve(ADMISSION_TIMED_OUT);
1077
+ }, admissionTimeoutMs);
1078
+ });
1079
+ const startedAt = monotonicNow();
1080
+ const settledInTime = () => monotonicNow() - startedAt < admissionTimeoutMs;
1081
+ let answered;
1082
+ try {
1083
+ answered = Promise.resolve(check(Object.freeze({
1084
+ actionId: action.actionId,
1085
+ tenantId: submission.tenantId,
1086
+ spaceId: submission.spaceId,
1087
+ actorId: submission.actorId,
1088
+ actorType: submission.actorType,
1089
+ parameters: checked,
1090
+ signal: controller.signal
1091
+ }))).then(
1092
+ (value) => settledInTime() ? { kind: "answered", value } : ADMISSION_TIMED_OUT,
1093
+ () => settledInTime() ? ADMISSION_FAILED : ADMISSION_TIMED_OUT
1094
+ );
1095
+ } catch {
1096
+ answered = Promise.resolve(settledInTime() ? ADMISSION_FAILED : ADMISSION_TIMED_OUT);
1097
+ }
1098
+ let outcome;
1099
+ try {
1100
+ outcome = await Promise.race([answered, deadline]);
1101
+ } finally {
1102
+ clearTimeout(timer);
1103
+ }
1104
+ if (outcome.kind === "timed_out") {
1105
+ controller.abort();
1106
+ return refuse("timed_out");
1107
+ }
1108
+ if (outcome.kind === "failed") return refuse("failed");
1109
+ const admitted = ownDataProperty(outcome.value, "admitted");
1110
+ if (admitted === true) return { admittedAt: now() };
1111
+ const code = ownDataProperty(outcome.value, "code");
1112
+ if (admitted === false && typeof code === "string" && ADMISSION_REFUSAL_CODE.test(code)) return refuse("refused", code);
1113
+ return refuse("invalid_decision");
1114
+ }
1115
+ function refuseAdmission(action, submission, refusal, refusalCode) {
1116
+ emitPlatformHostTelemetry(options.telemetry, {
1117
+ kind: "event",
1118
+ name: "invocation.admission_refused",
1119
+ metricName: PLATFORM_HOST_METRIC_NAMES.invocationAdmissionRefused,
1120
+ occurredAt: now(),
1121
+ tenantId: submission.tenantId,
1122
+ spaceId: submission.spaceId,
1123
+ attributes: { actionVersion: action.version, refusal }
1124
+ });
1125
+ throw new ActionAdmissionError(action.actionId, refusal, refusalCode);
690
1126
  }
691
1127
  async function executeInvocation(actionInvocationId, tenantId, spaceId, executionOptions = {}, executionReasonOverride) {
692
1128
  const loadedInvocation = await options.store.getActionInvocation(
@@ -715,6 +1151,13 @@ function createGovernedActionHost(options) {
715
1151
  if (!action) {
716
1152
  return fail2(invocation, "failed", `Unknown action: ${invocation.actionId}`);
717
1153
  }
1154
+ if (action.admitSubmission !== void 0 && !isAdmissionTime(invocation.admittedAt)) {
1155
+ return fail2(
1156
+ invocation,
1157
+ "failed",
1158
+ `Invocation of ${action.actionId} was not admitted at submission; an action that declares admitSubmission runs only on invocations its check admitted.`
1159
+ );
1160
+ }
718
1161
  if (action.kind === "saga") {
719
1162
  return fail2(
720
1163
  invocation,
@@ -882,6 +1325,10 @@ function createGovernedActionHost(options) {
882
1325
  }
883
1326
  const definitions = options.resolvePolicies ? await options.resolvePolicies({
884
1327
  ...authorizationInput,
1328
+ actionInvocationId,
1329
+ parameters: parsed.data,
1330
+ invocation,
1331
+ executionReason,
885
1332
  declaredPolicyIds: action.policies ?? []
886
1333
  }) : declaredPolicies(action.policies ?? []);
887
1334
  const outcomes = await evaluatePolicyDefinitions(definitions, {
@@ -992,6 +1439,7 @@ function createGovernedActionHost(options) {
992
1439
  correlationId: invocation.correlationId,
993
1440
  ...invocation.causationId ? { causationId: invocation.causationId } : {},
994
1441
  ...invocation.provenance ? { provenance: invocation.provenance } : {},
1442
+ ...isAdmissionTime(invocation.admittedAt) ? { admittedAt: invocation.admittedAt } : {},
995
1443
  db,
996
1444
  services: options.services
997
1445
  },
@@ -1574,6 +2022,8 @@ function createGovernedActionHost(options) {
1574
2022
  producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
1575
2023
  payloadClassification,
1576
2024
  includeProvenance: options.outbox.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
2025
+ // Actor identity is excluded for every classification unless the application opts in.
2026
+ includeActor: options.outbox.includeActor?.(envelope, payloadClassification) === true,
1577
2027
  ...traceContext ? { traceContext } : {}
1578
2028
  };
1579
2029
  if (transaction) {
@@ -1643,7 +2093,13 @@ function createGovernedActionHost(options) {
1643
2093
  recordExecutionAttestation,
1644
2094
  recordExternalReconciliation,
1645
2095
  completeExternalInvocation: externalCompletion.completeExternalInvocation,
1646
- reconcileOverdueCompletions: externalCompletion.reconcileOverdueCompletions
2096
+ reconcileOverdueCompletions: externalCompletion.reconcileOverdueCompletions,
2097
+ requeueOutboxDeadLetter: createOutboxRequeue({
2098
+ store: options.store,
2099
+ authorization: options.authorization,
2100
+ telemetry: options.telemetry,
2101
+ now
2102
+ })
1647
2103
  };
1648
2104
  }
1649
2105
  function actionResult(invocation) {
@@ -1705,6 +2161,18 @@ function isTerminal(status) {
1705
2161
  function isRecord(value) {
1706
2162
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1707
2163
  }
2164
+ function isAdmissionTime(value) {
2165
+ return value instanceof Date && Number.isFinite(value.getTime());
2166
+ }
2167
+ function ownDataProperty(value, key) {
2168
+ if (value === null || typeof value !== "object") return void 0;
2169
+ try {
2170
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
2171
+ return descriptor && "value" in descriptor ? descriptor.value : void 0;
2172
+ } catch {
2173
+ return void 0;
2174
+ }
2175
+ }
1708
2176
  function validateAuthorizationBinding(input, parameterDigest, authorityMoment, currentTime = /* @__PURE__ */ new Date()) {
1709
2177
  const binding = input.authorizationBinding;
1710
2178
  if (binding.id !== input.authorizationBindingId && input.authorizationBindingId) throw new Error("Authorization binding ID does not match authorizationBindingId.");
@@ -1744,6 +2212,9 @@ function toEnterpriseEventEnvelope(event, metadata) {
1744
2212
  ...event.actionInvocationId ? { actionInvocationId: event.actionInvocationId } : {},
1745
2213
  correlationId: event.correlationId,
1746
2214
  ...event.causationId ? { causationId: event.causationId } : {},
2215
+ // Opt-in only: without it the keys are absent, so existing publishers see an unchanged envelope.
2216
+ ...includesActor(metadata) && event.actorId ? { actorId: event.actorId } : {},
2217
+ ...includesActor(metadata) && event.actorType ? { actorType: event.actorType } : {},
1747
2218
  occurredAt: event.occurredAt,
1748
2219
  recordedAt: event.recordedAt,
1749
2220
  producerModuleVersion: metadata.producerModuleVersion,
@@ -1755,6 +2226,12 @@ function toEnterpriseEventEnvelope(event, metadata) {
1755
2226
  payload: event.payload
1756
2227
  };
1757
2228
  }
2229
+ function outboxActorDisclosure(metadata) {
2230
+ return includesActor(metadata) ? "included" : "suppressed";
2231
+ }
2232
+ function includesActor(metadata) {
2233
+ return metadata.includeActor === true;
2234
+ }
1758
2235
  async function runOutboxRelayCycle(options) {
1759
2236
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
1760
2237
  const maxAttempts = options.maxAttempts ?? 10;
@@ -1789,7 +2266,7 @@ async function runOutboxRelayCycle(options) {
1789
2266
  try {
1790
2267
  await publishWithTimeout(
1791
2268
  options.publisher,
1792
- record.event,
2269
+ record,
1793
2270
  options.publishTimeoutMs ?? 1e4
1794
2271
  );
1795
2272
  await stopHeartbeat();
@@ -1851,7 +2328,7 @@ async function runOutboxRelayCycle(options) {
1851
2328
  });
1852
2329
  return result;
1853
2330
  }
1854
- async function publishWithTimeout(publisher, event, timeoutMs) {
2331
+ async function publishWithTimeout(publisher, record, timeoutMs) {
1855
2332
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
1856
2333
  throw new Error("publishTimeoutMs must be a positive finite number");
1857
2334
  }
@@ -1865,7 +2342,11 @@ async function publishWithTimeout(publisher, event, timeoutMs) {
1865
2342
  }, timeoutMs);
1866
2343
  });
1867
2344
  try {
1868
- await Promise.race([publisher.publish(event, { signal: controller.signal }), timeout]);
2345
+ const context = {
2346
+ signal: controller.signal,
2347
+ actorDisclosure: record.actorDisclosure ?? "legacy"
2348
+ };
2349
+ await Promise.race([publisher.publish(record.event, context), timeout]);
1869
2350
  } finally {
1870
2351
  if (timer) clearTimeout(timer);
1871
2352
  }
@@ -1895,9 +2376,23 @@ function startOutboxHeartbeat(options, record, leaseDurationMs) {
1895
2376
  };
1896
2377
  }
1897
2378
  function cloneOutboxRecord(record) {
2379
+ const event = {
2380
+ ...record.event,
2381
+ occurredAt: new Date(record.event.occurredAt),
2382
+ recordedAt: new Date(record.event.recordedAt),
2383
+ ...record.event.traceContext ? { traceContext: { ...record.event.traceContext } } : {}
2384
+ };
2385
+ if (record.actorDisclosure !== "included") {
2386
+ delete event.actorId;
2387
+ delete event.actorType;
2388
+ }
1898
2389
  return {
1899
2390
  ...record,
1900
- event: { ...record.event, ...record.event.traceContext ? { traceContext: { ...record.event.traceContext } } : {} }
2391
+ availableAt: new Date(record.availableAt),
2392
+ createdAt: new Date(record.createdAt),
2393
+ ...record.leaseExpiresAt ? { leaseExpiresAt: new Date(record.leaseExpiresAt) } : {},
2394
+ ...record.publishedAt ? { publishedAt: new Date(record.publishedAt) } : {},
2395
+ event
1901
2396
  };
1902
2397
  }
1903
2398
  function emitRelayTelemetry(telemetry, name, occurredAt, workerId, attributes) {
@@ -1929,6 +2424,13 @@ var MemoryPlatformHostStore = class {
1929
2424
  executionAttestations = [];
1930
2425
  externalReconciliations = [];
1931
2426
  outbox = [];
2427
+ /**
2428
+ * Requeue evidence is append-only audit history, so unlike the ledger
2429
+ * arrays above, which tests have always inspected and seeded directly, it
2430
+ * is not public: `listOutboxRequeues` returns copies with their own Dates. TypeScript
2431
+ * `private`, as for the store's other internals, is a compile-time guard.
2432
+ */
2433
+ outboxRequeues = [];
1932
2434
  async transaction(run) {
1933
2435
  return run(this.db);
1934
2436
  }
@@ -2129,6 +2631,7 @@ var MemoryPlatformHostStore = class {
2129
2631
  tenantId: event.tenantId,
2130
2632
  spaceId: event.spaceId,
2131
2633
  event: toEnterpriseEventEnvelope(event, metadata),
2634
+ actorDisclosure: outboxActorDisclosure(metadata),
2132
2635
  status: "pending",
2133
2636
  attemptCount: 0,
2134
2637
  availableAt: createdAt,
@@ -2170,6 +2673,42 @@ var MemoryPlatformHostStore = class {
2170
2673
  async listOutbox(input = {}) {
2171
2674
  return this.outbox.filter((record) => (!input.tenantId || record.tenantId === input.tenantId) && (!input.spaceId || record.spaceId === input.spaceId) && (!input.statuses?.length || input.statuses.includes(record.status))).map(cloneOutboxRecord);
2172
2675
  }
2676
+ async getOutboxRecord(id, tenantId, spaceId) {
2677
+ const record = this.outbox.find((candidate) => candidate.id === id && candidate.tenantId === tenantId && candidate.spaceId === spaceId);
2678
+ return record ? cloneOutboxRecord(record) : void 0;
2679
+ }
2680
+ async requeueOutboxDeadLetter(input) {
2681
+ const record = this.outbox.find((candidate) => candidate.id === input.id && candidate.tenantId === input.tenantId && candidate.spaceId === input.spaceId);
2682
+ if (!record) return { applied: false, refusal: "not_found" };
2683
+ if (record.status !== "dead_letter") return { applied: false, refusal: "not_dead_letter", currentStatus: record.status };
2684
+ if ((record.leaseToken ?? 0) !== input.expectedLeaseToken) return { applied: false, refusal: "stale", currentStatus: record.status };
2685
+ const requeue = {
2686
+ id: input.requeueId,
2687
+ outboxId: record.id,
2688
+ tenantId: record.tenantId,
2689
+ spaceId: record.spaceId,
2690
+ eventId: record.event.eventId,
2691
+ actorId: input.actorId,
2692
+ actorType: input.actorType,
2693
+ reason: input.reason,
2694
+ requeuedAt: new Date(input.requeuedAt),
2695
+ availableAt: new Date(input.availableAt),
2696
+ priorAttemptCount: record.attemptCount,
2697
+ ...record.lastError !== void 0 ? { priorLastError: record.lastError } : {},
2698
+ priorAvailableAt: new Date(record.availableAt),
2699
+ priorLeaseToken: record.leaseToken ?? 0
2700
+ };
2701
+ record.status = "pending";
2702
+ record.attemptCount = 0;
2703
+ record.availableAt = new Date(input.availableAt);
2704
+ delete record.leaseOwner;
2705
+ delete record.leaseExpiresAt;
2706
+ this.outboxRequeues.push(requeue);
2707
+ return { applied: true, record: cloneOutboxRecord(record), requeue: cloneOutboxRequeue(requeue) };
2708
+ }
2709
+ async listOutboxRequeues(input) {
2710
+ return this.outboxRequeues.filter((requeue) => requeue.tenantId === input.tenantId && requeue.spaceId === input.spaceId && (input.outboxId === void 0 || requeue.outboxId === input.outboxId)).sort((left, right) => left.requeuedAt.getTime() - right.requeuedAt.getTime() || left.id.localeCompare(right.id)).map(cloneOutboxRequeue);
2711
+ }
2173
2712
  requireLeasedOutbox(id, workerId, leaseToken) {
2174
2713
  const record = this.outbox.find((candidate) => candidate.id === id);
2175
2714
  if (!record || record.leaseOwner !== workerId || (record.leaseToken ?? 0) !== (leaseToken ?? 0)) {
@@ -2251,6 +2790,14 @@ var MemoryPlatformHostStore = class {
2251
2790
  return this.events.filter((event) => event.tenantId === tenantId && event.spaceId === spaceId).sort((left, right) => left.sequence - right.sequence);
2252
2791
  }
2253
2792
  };
2793
+ function cloneOutboxRequeue(requeue) {
2794
+ return {
2795
+ ...requeue,
2796
+ requeuedAt: new Date(requeue.requeuedAt),
2797
+ availableAt: new Date(requeue.availableAt),
2798
+ priorAvailableAt: new Date(requeue.priorAvailableAt)
2799
+ };
2800
+ }
2254
2801
  async function applyPostgresMigrations(client, migrations) {
2255
2802
  const ordered = validateMigrations(migrations);
2256
2803
  const statements = ordered.map((migration) => migrationStatement(migration));
@@ -2492,6 +3039,43 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
2492
3039
  ON fabric_platform.action_invocations (completion_due_at)
2493
3040
  WHERE completion_due_at IS NOT NULL;
2494
3041
  `
3042
+ }, {
3043
+ version: 4,
3044
+ name: "outbox_actor_disclosure",
3045
+ // Nullable with no default, so rows written before this migration stay NULL and read back
3046
+ // as legacy. NOT VALID skips scanning existing rows; the check still applies to every
3047
+ // insert and update from here on.
3048
+ sql: `
3049
+ ALTER TABLE fabric_platform.event_outbox
3050
+ ADD COLUMN actor_disclosure text;
3051
+ ALTER TABLE fabric_platform.event_outbox
3052
+ ADD CONSTRAINT event_outbox_actor_disclosure_check
3053
+ CHECK (actor_disclosure IN ('included', 'suppressed')) NOT VALID;
3054
+ `
3055
+ }, {
3056
+ version: 5,
3057
+ name: "outbox_requeue",
3058
+ sql: `
3059
+ CREATE TABLE fabric_platform.event_outbox_requeues (
3060
+ id text PRIMARY KEY, outbox_id text NOT NULL,
3061
+ tenant_id text NOT NULL, space_id text NOT NULL, event_id text NOT NULL,
3062
+ actor_id text NOT NULL, actor_type text NOT NULL, reason text NOT NULL,
3063
+ requeued_at timestamptz NOT NULL, available_at timestamptz NOT NULL,
3064
+ prior_attempt_count integer NOT NULL, prior_last_error text,
3065
+ prior_available_at timestamptz NOT NULL, prior_lease_token bigint NOT NULL
3066
+ );
3067
+ CREATE INDEX event_outbox_requeues_record_idx
3068
+ ON fabric_platform.event_outbox_requeues (tenant_id, space_id, outbox_id, requeued_at);
3069
+ `
3070
+ }, {
3071
+ version: 6,
3072
+ name: "invocation_admission",
3073
+ // Nullable with no default: rows written before this migration, and
3074
+ // rows of actions that declare no admission check, stay NULL.
3075
+ sql: `
3076
+ ALTER TABLE fabric_platform.action_invocations
3077
+ ADD COLUMN admitted_at timestamptz;
3078
+ `
2495
3079
  }]);
2496
3080
  }
2497
3081
  async transaction(run) {
@@ -2499,44 +3083,59 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
2499
3083
  }
2500
3084
  async createActionInvocation(input) {
2501
3085
  const now = /* @__PURE__ */ new Date();
2502
- const result = await this.sql.query(
2503
- `INSERT INTO fabric_platform.action_invocations
2504
- (id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
2505
- parameters,result,correlation_id,causation_id,idempotency_key,
2506
- parameter_digest,parameter_digest_algorithm,idempotency_actor_id,
2507
- idempotency_authorization_binding_id,invocation_provenance,authorization_binding,execution_reason,
2508
- authorization_binding_id,error,runtime_evidence,created_at,updated_at)
2509
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,$18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,$24,$24)
2510
- ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
2511
- WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
2512
- RETURNING *`,
2513
- [
2514
- input.id,
2515
- input.tenantId,
2516
- input.spaceId,
2517
- input.actionId,
2518
- input.actionVersion,
2519
- input.actorId,
2520
- input.actorType,
2521
- input.status,
2522
- JSON.stringify(input.parameters),
2523
- JSON.stringify(input.result),
2524
- input.correlationId,
2525
- input.causationId ?? null,
2526
- input.idempotencyKey ?? null,
2527
- input.parameterDigest ?? null,
2528
- input.parameterDigestAlgorithm ?? null,
2529
- input.idempotencyActorId ?? null,
2530
- input.idempotencyAuthorizationBindingId ?? null,
2531
- input.provenance ? JSON.stringify(input.provenance) : null,
2532
- input.authorizationBinding ? JSON.stringify(input.authorizationBinding) : null,
2533
- input.executionReason ?? null,
2534
- input.authorizationBindingId ?? null,
2535
- input.error ?? null,
2536
- input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
2537
- now
2538
- ]
2539
- );
3086
+ const admitted = input.admittedAt !== void 0;
3087
+ const admittedColumn = admitted ? "admitted_at," : "";
3088
+ const admittedValue = admitted ? "$25," : "";
3089
+ let result;
3090
+ try {
3091
+ result = await this.sql.query(
3092
+ `INSERT INTO fabric_platform.action_invocations
3093
+ (id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
3094
+ parameters,result,correlation_id,causation_id,idempotency_key,
3095
+ parameter_digest,parameter_digest_algorithm,idempotency_actor_id,
3096
+ idempotency_authorization_binding_id,invocation_provenance,authorization_binding,execution_reason,
3097
+ authorization_binding_id,error,runtime_evidence,${admittedColumn}created_at,updated_at)
3098
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,$18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,${admittedValue}$24,$24)
3099
+ ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
3100
+ WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
3101
+ RETURNING *`,
3102
+ [
3103
+ input.id,
3104
+ input.tenantId,
3105
+ input.spaceId,
3106
+ input.actionId,
3107
+ input.actionVersion,
3108
+ input.actorId,
3109
+ input.actorType,
3110
+ input.status,
3111
+ JSON.stringify(input.parameters),
3112
+ JSON.stringify(input.result),
3113
+ input.correlationId,
3114
+ input.causationId ?? null,
3115
+ input.idempotencyKey ?? null,
3116
+ input.parameterDigest ?? null,
3117
+ input.parameterDigestAlgorithm ?? null,
3118
+ input.idempotencyActorId ?? null,
3119
+ input.idempotencyAuthorizationBindingId ?? null,
3120
+ input.provenance ? JSON.stringify(input.provenance) : null,
3121
+ input.authorizationBinding ? JSON.stringify(input.authorizationBinding) : null,
3122
+ input.executionReason ?? null,
3123
+ input.authorizationBindingId ?? null,
3124
+ input.error ?? null,
3125
+ input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
3126
+ now,
3127
+ ...admitted ? [input.admittedAt] : []
3128
+ ]
3129
+ );
3130
+ } catch (error) {
3131
+ if (admitted && error?.code === "42703") {
3132
+ throw new Error(
3133
+ "An admitted invocation needs the action_invocations.admitted_at column; run ensureSchema() to apply PostgreSQL migration 6 (invocation_admission) before submitting actions that declare admitSubmission.",
3134
+ { cause: error }
3135
+ );
3136
+ }
3137
+ throw error;
3138
+ }
2540
3139
  return toActionRecord(result.rows[0]);
2541
3140
  }
2542
3141
  async getActionInvocation(id, tenantId, spaceId) {
@@ -2811,8 +3410,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
2811
3410
  ON CONFLICT (id) DO NOTHING RETURNING id
2812
3411
  )
2813
3412
  INSERT INTO fabric_platform.event_outbox
2814
- (id,tenant_id,space_id,event,status,attempt_count,available_at,created_at)
2815
- SELECT $1,$2,$3,$18::jsonb,'pending',0,$14,$14 FROM inserted_event
3413
+ (id,tenant_id,space_id,event,actor_disclosure,status,attempt_count,available_at,created_at)
3414
+ SELECT $1,$2,$3,$18::jsonb,$20,'pending',0,$14,$14 FROM inserted_event
2816
3415
  ON CONFLICT (id) DO NOTHING`,
2817
3416
  [
2818
3417
  event.id,
@@ -2833,7 +3432,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
2833
3432
  event.causationId ?? null,
2834
3433
  event.provenance ? JSON.stringify(event.provenance) : null,
2835
3434
  JSON.stringify(envelope),
2836
- event.consistency ?? null
3435
+ event.consistency ?? null,
3436
+ outboxActorDisclosure(metadata)
2837
3437
  ]
2838
3438
  );
2839
3439
  }
@@ -2901,6 +3501,90 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
2901
3501
  );
2902
3502
  return result.rows.map(toOutboxRecord);
2903
3503
  }
3504
+ async getOutboxRecord(id, tenantId, spaceId) {
3505
+ const result = await this.sql.query(
3506
+ `SELECT * FROM fabric_platform.event_outbox
3507
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
3508
+ [id, tenantId, spaceId]
3509
+ );
3510
+ return result.rows[0] ? toOutboxRecord(result.rows[0]) : void 0;
3511
+ }
3512
+ async requeueOutboxDeadLetter(input) {
3513
+ const result = await this.sql.query(
3514
+ `WITH prior AS (
3515
+ SELECT id, tenant_id, space_id, event->>'eventId' AS event_id,
3516
+ attempt_count, last_error, available_at, lease_token
3517
+ FROM fabric_platform.event_outbox
3518
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND status='dead_letter'
3519
+ AND lease_token=$10
3520
+ FOR UPDATE
3521
+ ), requeued AS (
3522
+ UPDATE fabric_platform.event_outbox AS item
3523
+ SET status='pending', attempt_count=0, available_at=$9,
3524
+ lease_owner=NULL, lease_expires_at=NULL
3525
+ FROM prior WHERE item.id=prior.id
3526
+ RETURNING item.*
3527
+ ), evidence AS (
3528
+ INSERT INTO fabric_platform.event_outbox_requeues
3529
+ (id,outbox_id,tenant_id,space_id,event_id,actor_id,actor_type,reason,
3530
+ requeued_at,available_at,prior_attempt_count,prior_last_error,
3531
+ prior_available_at,prior_lease_token)
3532
+ SELECT $4,prior.id,prior.tenant_id,prior.space_id,prior.event_id,$5,$6,$7,
3533
+ $8,$9,prior.attempt_count,prior.last_error,prior.available_at,prior.lease_token
3534
+ FROM prior JOIN requeued ON requeued.id=prior.id
3535
+ RETURNING *
3536
+ )
3537
+ SELECT requeued.*,
3538
+ evidence.id AS requeue_id, evidence.outbox_id AS requeue_outbox_id,
3539
+ evidence.tenant_id AS requeue_tenant_id, evidence.space_id AS requeue_space_id,
3540
+ evidence.event_id AS requeue_event_id, evidence.actor_id AS requeue_actor_id,
3541
+ evidence.actor_type AS requeue_actor_type, evidence.reason AS requeue_reason,
3542
+ evidence.requeued_at AS requeue_requeued_at, evidence.available_at AS requeue_available_at,
3543
+ evidence.prior_attempt_count AS requeue_prior_attempt_count,
3544
+ evidence.prior_last_error AS requeue_prior_last_error,
3545
+ evidence.prior_available_at AS requeue_prior_available_at,
3546
+ evidence.prior_lease_token AS requeue_prior_lease_token
3547
+ FROM requeued JOIN evidence ON evidence.outbox_id=requeued.id`,
3548
+ [
3549
+ input.id,
3550
+ input.tenantId,
3551
+ input.spaceId,
3552
+ input.requeueId,
3553
+ input.actorId,
3554
+ input.actorType,
3555
+ input.reason,
3556
+ input.requeuedAt,
3557
+ input.availableAt,
3558
+ input.expectedLeaseToken
3559
+ ]
3560
+ );
3561
+ const row = result.rows[0];
3562
+ if (row) {
3563
+ return {
3564
+ applied: true,
3565
+ record: toOutboxRecord(row),
3566
+ requeue: toOutboxRequeueRecord(row, "requeue_")
3567
+ };
3568
+ }
3569
+ const current = await this.sql.query(
3570
+ `SELECT status FROM fabric_platform.event_outbox
3571
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
3572
+ [input.id, input.tenantId, input.spaceId]
3573
+ );
3574
+ const found = current.rows[0];
3575
+ if (!found) return { applied: false, refusal: "not_found" };
3576
+ const status = found.status;
3577
+ return status === "dead_letter" ? { applied: false, refusal: "stale", currentStatus: status } : { applied: false, refusal: "not_dead_letter", currentStatus: status };
3578
+ }
3579
+ async listOutboxRequeues(input) {
3580
+ const result = await this.sql.query(
3581
+ `SELECT * FROM fabric_platform.event_outbox_requeues
3582
+ WHERE tenant_id=$1 AND space_id=$2 AND ($3::text IS NULL OR outbox_id=$3)
3583
+ ORDER BY requeued_at,id`,
3584
+ [input.tenantId, input.spaceId, input.outboxId ?? null]
3585
+ );
3586
+ return result.rows.map((row) => toOutboxRequeueRecord(row, ""));
3587
+ }
2904
3588
  async nextEventSequence(tenantId, spaceId) {
2905
3589
  const result = await this.sql.query(
2906
3590
  `INSERT INTO fabric_platform.event_sequences (tenant_id,space_id,next_sequence)
@@ -3173,6 +3857,7 @@ function toActionRecord(row) {
3173
3857
  ...row.mutation_footprint ? { mutationFootprint: row.mutation_footprint } : {},
3174
3858
  ...row.execution_principal ? { executionPrincipal: row.execution_principal } : {},
3175
3859
  ...row.runtime_evidence ? { runtimeEvidence: row.runtime_evidence } : {},
3860
+ ...row.admitted_at ? { admittedAt: new Date(row.admitted_at) } : {},
3176
3861
  ...row.error ? { error: String(row.error) } : {},
3177
3862
  createdAt: new Date(row.created_at),
3178
3863
  updatedAt: new Date(row.updated_at)
@@ -3241,10 +3926,13 @@ function toEventRecord(row) {
3241
3926
  }
3242
3927
  function toOutboxRecord(row) {
3243
3928
  const event = row.event;
3929
+ const actorDisclosure = row.actor_disclosure === "included" || row.actor_disclosure === "suppressed" ? row.actor_disclosure : void 0;
3930
+ const actorIncluded = actorDisclosure === "included";
3244
3931
  return {
3245
3932
  id: String(row.id),
3246
3933
  tenantId: String(row.tenant_id),
3247
3934
  spaceId: String(row.space_id),
3935
+ ...actorDisclosure ? { actorDisclosure } : {},
3248
3936
  event: {
3249
3937
  eventId: String(event.eventId),
3250
3938
  eventType: String(event.eventType),
@@ -3257,6 +3945,8 @@ function toOutboxRecord(row) {
3257
3945
  ...event.actionInvocationId ? { actionInvocationId: String(event.actionInvocationId) } : {},
3258
3946
  correlationId: String(event.correlationId),
3259
3947
  ...event.causationId ? { causationId: String(event.causationId) } : {},
3948
+ ...actorIncluded && event.actorId ? { actorId: String(event.actorId) } : {},
3949
+ ...actorIncluded && event.actorType ? { actorType: String(event.actorType) } : {},
3260
3950
  ...event.provenance ? { provenance: event.provenance } : {},
3261
3951
  ...event.consistency ? { consistency: event.consistency } : {},
3262
3952
  occurredAt: new Date(event.occurredAt),
@@ -3277,6 +3967,26 @@ function toOutboxRecord(row) {
3277
3967
  ...row.published_at ? { publishedAt: new Date(row.published_at) } : {}
3278
3968
  };
3279
3969
  }
3970
+ function toOutboxRequeueRecord(row, prefix) {
3971
+ const field = (name) => row[`${prefix}${name}`];
3972
+ const priorLastError = field("prior_last_error");
3973
+ return {
3974
+ id: String(field("id")),
3975
+ outboxId: String(field("outbox_id")),
3976
+ tenantId: String(field("tenant_id")),
3977
+ spaceId: String(field("space_id")),
3978
+ eventId: String(field("event_id")),
3979
+ actorId: String(field("actor_id")),
3980
+ actorType: String(field("actor_type")),
3981
+ reason: String(field("reason")),
3982
+ requeuedAt: new Date(field("requeued_at")),
3983
+ availableAt: new Date(field("available_at")),
3984
+ priorAttemptCount: Number(field("prior_attempt_count")),
3985
+ ...priorLastError !== null && priorLastError !== void 0 ? { priorLastError: String(priorLastError) } : {},
3986
+ priorAvailableAt: new Date(field("prior_available_at")),
3987
+ priorLeaseToken: Number(field("prior_lease_token"))
3988
+ };
3989
+ }
3280
3990
 
3281
3991
  // src/worker.ts
3282
3992
  var DEFAULT_BATCH_SIZE = 10;
@@ -3737,6 +4447,6 @@ async function runTenantIsolationChecks(subject) {
3737
4447
  return { passed: checks.every((check) => check.status !== "failed"), checks };
3738
4448
  }
3739
4449
 
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 };
4450
+ export { ActionAdmissionError, 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, outboxActorDisclosure, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, runTenantIsolationChecks, tenantIsolationChecks, toEnterpriseEventEnvelope };
3741
4451
  //# sourceMappingURL=index.js.map
3742
4452
  //# sourceMappingURL=index.js.map