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