@fabricorg/platform-host 7.2.3 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/README.md +258 -1
- package/dist/index.cjs +838 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +310 -14
- package/dist/index.d.ts +310 -14
- package/dist/index.js +833 -108
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { AdapterRegistry, createFabricId, assertGovernanceRuntimeEvidence, FABRIC_GOVERNANCE_CONTRACT_VERSION, assertMutationGovernanceContext, evaluatePolicyDefinitions, aggregatePolicyOutcomes, evaluateStateMachineTransition, executeWithAdapterRetry, assertAdapterOutcomeConsistency } from '@fabricorg/platform';
|
|
1
|
+
import { canonicalJson, AdapterRegistry, assertAdapterCircuitBreakerConfig, AdapterCircuitBreakerRegistry, createFabricId, assertGovernanceRuntimeEvidence, FABRIC_GOVERNANCE_CONTRACT_VERSION, assertMutationGovernanceContext, evaluatePolicyDefinitions, aggregatePolicyOutcomes, evaluateStateMachineTransition, executeWithAdapterRetry, isAdapterCircuitOpenError, assertAdapterOutcomeConsistency } from '@fabricorg/platform';
|
|
2
|
+
export { canonicalJson } from '@fabricorg/platform';
|
|
3
|
+
import { performance } from 'perf_hooks';
|
|
2
4
|
import { assertAssemblyRuntimeCompatible } from '@fabricorg/assembly';
|
|
3
5
|
import { createHash } from 'crypto';
|
|
6
|
+
import { types } from 'util';
|
|
4
7
|
|
|
5
8
|
// src/host.ts
|
|
6
9
|
|
|
@@ -15,6 +18,19 @@ var IdempotencyConflictError = class extends Error {
|
|
|
15
18
|
conflict;
|
|
16
19
|
code = "IDEMPOTENCY_CONFLICT";
|
|
17
20
|
};
|
|
21
|
+
var ActionAdmissionError = class extends Error {
|
|
22
|
+
constructor(actionId, refusal, refusalCode) {
|
|
23
|
+
super(`Action ${actionId} submission was not admitted (${refusal}${refusalCode ? `: ${refusalCode}` : ""}); nothing was recorded.`);
|
|
24
|
+
this.actionId = actionId;
|
|
25
|
+
this.refusal = refusal;
|
|
26
|
+
this.refusalCode = refusalCode;
|
|
27
|
+
this.name = "ActionAdmissionError";
|
|
28
|
+
}
|
|
29
|
+
actionId;
|
|
30
|
+
refusal;
|
|
31
|
+
refusalCode;
|
|
32
|
+
code = "ACTION_ADMISSION_REFUSED";
|
|
33
|
+
};
|
|
18
34
|
var ExternalCompletionError = class extends Error {
|
|
19
35
|
constructor(refusal, message) {
|
|
20
36
|
super(message);
|
|
@@ -28,16 +44,6 @@ var ExternalCompletionError = class extends Error {
|
|
|
28
44
|
}
|
|
29
45
|
};
|
|
30
46
|
var PARAMETER_DIGEST_ALGORITHM = "fabric-canonical-json-sha256-v1";
|
|
31
|
-
function canonicalJson(value) {
|
|
32
|
-
return JSON.stringify(sort(value));
|
|
33
|
-
}
|
|
34
|
-
function sort(value) {
|
|
35
|
-
if (Array.isArray(value)) return value.map(sort);
|
|
36
|
-
if (value && typeof value === "object") {
|
|
37
|
-
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, sort(child)]));
|
|
38
|
-
}
|
|
39
|
-
return value;
|
|
40
|
-
}
|
|
41
47
|
function digestParameters(parameters) {
|
|
42
48
|
return createHash("sha256").update(canonicalJson(parameters)).digest("hex");
|
|
43
49
|
}
|
|
@@ -82,6 +88,7 @@ var PLATFORM_HOST_METRIC_NAMES = {
|
|
|
82
88
|
workerCyclesCompleted: "fabric.platform.worker.cycles.completed",
|
|
83
89
|
workerCyclesFailed: "fabric.platform.worker.cycles.failed",
|
|
84
90
|
invocationSubmitted: "fabric.platform.invocation.submitted",
|
|
91
|
+
invocationAdmissionRefused: "fabric.platform.invocation.admission_refused",
|
|
85
92
|
invocationExecutionStarted: "fabric.platform.invocation.execution_started",
|
|
86
93
|
invocationLeaseClaimed: "fabric.platform.invocation.lease_claimed",
|
|
87
94
|
invocationCompleted: "fabric.platform.invocation.completed",
|
|
@@ -97,6 +104,8 @@ var PLATFORM_HOST_METRIC_NAMES = {
|
|
|
97
104
|
outboxPublished: "fabric.platform.outbox.published",
|
|
98
105
|
outboxFailed: "fabric.platform.outbox.failed",
|
|
99
106
|
outboxDeadLettered: "fabric.platform.outbox.dead_lettered",
|
|
107
|
+
outboxRequeued: "fabric.platform.outbox.requeued",
|
|
108
|
+
outboxRequeueRefused: "fabric.platform.outbox.requeue_refused",
|
|
100
109
|
invocationBacklog: "fabric.platform.invocation.backlog",
|
|
101
110
|
invocationRunning: "fabric.platform.invocation.running",
|
|
102
111
|
invocationExpiredLeases: "fabric.platform.invocation.expired_leases",
|
|
@@ -496,6 +505,340 @@ function createExternalCompletionLifecycle(deps) {
|
|
|
496
505
|
}
|
|
497
506
|
return { completeExternalInvocation, reconcileOverdueCompletions, recordCompletionReconciliation };
|
|
498
507
|
}
|
|
508
|
+
var SubmissionUnreadableError = class extends Error {
|
|
509
|
+
name = "SubmissionUnreadableError";
|
|
510
|
+
constructor() {
|
|
511
|
+
super("Platform Host submission could not be read as plain data; nothing was recorded.");
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var NotJsonData = class extends Error {
|
|
515
|
+
};
|
|
516
|
+
function copyJsonData(value) {
|
|
517
|
+
try {
|
|
518
|
+
return { ok: true, value: copy(value, /* @__PURE__ */ new Set()) };
|
|
519
|
+
} catch {
|
|
520
|
+
return { ok: false };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function copy(value, ancestors) {
|
|
524
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
525
|
+
if (typeof value === "number") {
|
|
526
|
+
if (!Number.isFinite(value)) throw new NotJsonData();
|
|
527
|
+
return value === 0 ? 0 : value;
|
|
528
|
+
}
|
|
529
|
+
if (typeof value !== "object") throw new NotJsonData();
|
|
530
|
+
if (types.isProxy(value)) throw new NotJsonData();
|
|
531
|
+
if (ancestors.has(value)) throw new NotJsonData();
|
|
532
|
+
const prototype = Reflect.getPrototypeOf(value);
|
|
533
|
+
ancestors.add(value);
|
|
534
|
+
try {
|
|
535
|
+
if (Array.isArray(value)) {
|
|
536
|
+
if (prototype !== Array.prototype) throw new NotJsonData();
|
|
537
|
+
const length = value.length;
|
|
538
|
+
const out2 = [];
|
|
539
|
+
for (let index = 0; index < length; index += 1) {
|
|
540
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
|
|
541
|
+
if (descriptor === void 0) throw new NotJsonData();
|
|
542
|
+
const item = readOnce(value, descriptor);
|
|
543
|
+
if (item === void 0) throw new NotJsonData();
|
|
544
|
+
out2.push(copy(item, ancestors));
|
|
545
|
+
}
|
|
546
|
+
return out2;
|
|
547
|
+
}
|
|
548
|
+
if (prototype !== Object.prototype && prototype !== null) throw new NotJsonData();
|
|
549
|
+
const out = {};
|
|
550
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
551
|
+
if (typeof key !== "string") continue;
|
|
552
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
553
|
+
if (descriptor === void 0 || descriptor.enumerable !== true) continue;
|
|
554
|
+
const item = readOnce(value, descriptor);
|
|
555
|
+
if (item === void 0) continue;
|
|
556
|
+
Reflect.defineProperty(out, key, { value: copy(item, ancestors), enumerable: true, writable: true, configurable: true });
|
|
557
|
+
}
|
|
558
|
+
return out;
|
|
559
|
+
} finally {
|
|
560
|
+
ancestors.delete(value);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function readOnce(target, descriptor) {
|
|
564
|
+
if ("value" in descriptor) return descriptor.value;
|
|
565
|
+
return descriptor.get ? Reflect.apply(descriptor.get, target, []) : void 0;
|
|
566
|
+
}
|
|
567
|
+
function snapshotSubmission(input) {
|
|
568
|
+
if (input === null || typeof input !== "object") throw new SubmissionUnreadableError();
|
|
569
|
+
const scalar = (key) => primitiveOrAbsent(readField(input, key));
|
|
570
|
+
const actionId = scalar("actionId");
|
|
571
|
+
const tenantId = scalar("tenantId");
|
|
572
|
+
const spaceId = scalar("spaceId");
|
|
573
|
+
const actorId = scalar("actorId");
|
|
574
|
+
const actorType = scalar("actorType");
|
|
575
|
+
const correlationId = scalar("correlationId");
|
|
576
|
+
const causationId = scalar("causationId");
|
|
577
|
+
const executionReason = scalar("executionReason");
|
|
578
|
+
const idempotencyKey = scalar("idempotencyKey");
|
|
579
|
+
const authorizationBindingId = scalar("authorizationBindingId");
|
|
580
|
+
const scalars = {
|
|
581
|
+
actionId,
|
|
582
|
+
tenantId,
|
|
583
|
+
spaceId,
|
|
584
|
+
actorId,
|
|
585
|
+
actorType,
|
|
586
|
+
...correlationId != null ? { correlationId } : {},
|
|
587
|
+
...causationId != null ? { causationId } : {},
|
|
588
|
+
...executionReason != null ? { executionReason } : {},
|
|
589
|
+
...idempotencyKey != null ? { idempotencyKey } : {},
|
|
590
|
+
...authorizationBindingId != null ? { authorizationBindingId } : {}
|
|
591
|
+
};
|
|
592
|
+
const live = { ...scalars, parametersArePrivate: false };
|
|
593
|
+
Object.defineProperties(live, {
|
|
594
|
+
parameters: { get: () => input.parameters, enumerable: true },
|
|
595
|
+
provenance: { get: () => input.provenance, enumerable: true },
|
|
596
|
+
authorizationBinding: { get: () => input.authorizationBinding, enumerable: true }
|
|
597
|
+
});
|
|
598
|
+
return Object.freeze(live);
|
|
599
|
+
}
|
|
600
|
+
var PROVENANCE_SCALARS = ["source", "correlationId", "causationId"];
|
|
601
|
+
var PROVENANCE_ATTRIBUTES = ["traceAttributes", "auditAttributes"];
|
|
602
|
+
var BINDING_FIELDS = ["id", "tenantId", "actorId", "actionId", "parameterDigest", "resourceScope", "capturedAt", "expiresAt", "governingMoment"];
|
|
603
|
+
function privateSubmission(snapshot) {
|
|
604
|
+
const parameters = readField(snapshot, "parameters");
|
|
605
|
+
const provenanceRead = readField(snapshot, "provenance");
|
|
606
|
+
const bindingRead = readField(snapshot, "authorizationBinding");
|
|
607
|
+
const copied = copyJsonData(parameters);
|
|
608
|
+
const provenance = provenanceRead ? copyProvenance(provenanceRead) : void 0;
|
|
609
|
+
const authorizationBinding = bindingRead ? copyBinding(bindingRead) : void 0;
|
|
610
|
+
return Object.freeze({
|
|
611
|
+
actionId: snapshot.actionId,
|
|
612
|
+
tenantId: snapshot.tenantId,
|
|
613
|
+
spaceId: snapshot.spaceId,
|
|
614
|
+
actorId: snapshot.actorId,
|
|
615
|
+
actorType: snapshot.actorType,
|
|
616
|
+
...snapshot.correlationId !== void 0 ? { correlationId: snapshot.correlationId } : {},
|
|
617
|
+
...snapshot.causationId !== void 0 ? { causationId: snapshot.causationId } : {},
|
|
618
|
+
...snapshot.executionReason !== void 0 ? { executionReason: snapshot.executionReason } : {},
|
|
619
|
+
...snapshot.idempotencyKey !== void 0 ? { idempotencyKey: snapshot.idempotencyKey } : {},
|
|
620
|
+
...snapshot.authorizationBindingId !== void 0 ? { authorizationBindingId: snapshot.authorizationBindingId } : {},
|
|
621
|
+
...provenance ? { provenance } : {},
|
|
622
|
+
...authorizationBinding ? { authorizationBinding } : {},
|
|
623
|
+
parameters: copied.ok ? copied.value : parameters,
|
|
624
|
+
parametersArePrivate: copied.ok
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function readField(target, key) {
|
|
628
|
+
try {
|
|
629
|
+
return target[key];
|
|
630
|
+
} catch {
|
|
631
|
+
throw new SubmissionUnreadableError();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function primitiveOrAbsent(value) {
|
|
635
|
+
if (value !== null && (typeof value === "object" || typeof value === "function" || typeof value === "symbol")) {
|
|
636
|
+
throw new SubmissionUnreadableError();
|
|
637
|
+
}
|
|
638
|
+
return value;
|
|
639
|
+
}
|
|
640
|
+
function objectOf(value) {
|
|
641
|
+
if (value === null || typeof value !== "object") throw new SubmissionUnreadableError();
|
|
642
|
+
return value;
|
|
643
|
+
}
|
|
644
|
+
function copyFields(source, fields) {
|
|
645
|
+
const out = {};
|
|
646
|
+
for (const field of fields) {
|
|
647
|
+
const value = primitiveOrAbsent(readField(source, field));
|
|
648
|
+
if (value !== void 0) out[field] = value;
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
function copyAttributes(source, field) {
|
|
653
|
+
const value = readField(source, field);
|
|
654
|
+
if (value === void 0 || value === null) return void 0;
|
|
655
|
+
const map = objectOf(value);
|
|
656
|
+
let entries;
|
|
657
|
+
try {
|
|
658
|
+
entries = Object.entries(map);
|
|
659
|
+
} catch {
|
|
660
|
+
throw new SubmissionUnreadableError();
|
|
661
|
+
}
|
|
662
|
+
const out = {};
|
|
663
|
+
for (const [key, item] of entries) {
|
|
664
|
+
Reflect.defineProperty(out, key, { value: primitiveOrAbsent(item), enumerable: true, writable: true, configurable: true });
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
function copyProvenance(value) {
|
|
669
|
+
const source = objectOf(value);
|
|
670
|
+
const out = copyFields(source, PROVENANCE_SCALARS);
|
|
671
|
+
for (const field of PROVENANCE_ATTRIBUTES) {
|
|
672
|
+
const attributes = copyAttributes(source, field);
|
|
673
|
+
if (attributes !== void 0) out[field] = attributes;
|
|
674
|
+
}
|
|
675
|
+
return out;
|
|
676
|
+
}
|
|
677
|
+
function copyBinding(value) {
|
|
678
|
+
return copyFields(objectOf(value), BINDING_FIELDS);
|
|
679
|
+
}
|
|
680
|
+
function parametersCopy(snapshot) {
|
|
681
|
+
if (!snapshot.parametersArePrivate) throw new SubmissionUnreadableError();
|
|
682
|
+
const copied = copyJsonData(snapshot.parameters);
|
|
683
|
+
if (!copied.ok) throw new SubmissionUnreadableError();
|
|
684
|
+
return copied.value;
|
|
685
|
+
}
|
|
686
|
+
function liveSubmissionView(snapshot, input) {
|
|
687
|
+
const view = {
|
|
688
|
+
actionId: snapshot.actionId,
|
|
689
|
+
tenantId: snapshot.tenantId,
|
|
690
|
+
spaceId: snapshot.spaceId,
|
|
691
|
+
actorId: snapshot.actorId,
|
|
692
|
+
actorType: snapshot.actorType,
|
|
693
|
+
...snapshot.correlationId !== void 0 ? { correlationId: snapshot.correlationId } : {},
|
|
694
|
+
...snapshot.causationId !== void 0 ? { causationId: snapshot.causationId } : {},
|
|
695
|
+
...snapshot.executionReason !== void 0 ? { executionReason: snapshot.executionReason } : {},
|
|
696
|
+
...snapshot.idempotencyKey !== void 0 ? { idempotencyKey: snapshot.idempotencyKey } : {},
|
|
697
|
+
...snapshot.authorizationBindingId !== void 0 ? { authorizationBindingId: snapshot.authorizationBindingId } : {}
|
|
698
|
+
};
|
|
699
|
+
const caller = input;
|
|
700
|
+
for (const field of ["parameters", "provenance", "authorizationBinding"]) {
|
|
701
|
+
Object.defineProperty(view, field, {
|
|
702
|
+
get: () => caller[field],
|
|
703
|
+
set: (value) => {
|
|
704
|
+
caller[field] = value;
|
|
705
|
+
},
|
|
706
|
+
enumerable: true,
|
|
707
|
+
configurable: true
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
return view;
|
|
711
|
+
}
|
|
712
|
+
function submissionView(snapshot) {
|
|
713
|
+
const fresh = (value) => {
|
|
714
|
+
if (value === void 0) return void 0;
|
|
715
|
+
const copied = copyJsonData(value);
|
|
716
|
+
if (!copied.ok) throw new SubmissionUnreadableError();
|
|
717
|
+
return copied.value;
|
|
718
|
+
};
|
|
719
|
+
const provenance = fresh(snapshot.provenance);
|
|
720
|
+
const authorizationBinding = fresh(snapshot.authorizationBinding);
|
|
721
|
+
return {
|
|
722
|
+
actionId: snapshot.actionId,
|
|
723
|
+
parameters: parametersCopy(snapshot),
|
|
724
|
+
tenantId: snapshot.tenantId,
|
|
725
|
+
spaceId: snapshot.spaceId,
|
|
726
|
+
actorId: snapshot.actorId,
|
|
727
|
+
actorType: snapshot.actorType,
|
|
728
|
+
...snapshot.correlationId !== void 0 ? { correlationId: snapshot.correlationId } : {},
|
|
729
|
+
...snapshot.causationId !== void 0 ? { causationId: snapshot.causationId } : {},
|
|
730
|
+
...provenance ? { provenance } : {},
|
|
731
|
+
...snapshot.executionReason !== void 0 ? { executionReason: snapshot.executionReason } : {},
|
|
732
|
+
...snapshot.idempotencyKey !== void 0 ? { idempotencyKey: snapshot.idempotencyKey } : {},
|
|
733
|
+
...snapshot.authorizationBindingId !== void 0 ? { authorizationBindingId: snapshot.authorizationBindingId } : {},
|
|
734
|
+
...authorizationBinding ? { authorizationBinding } : {}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
var MAX_REASON_LENGTH = 1e3;
|
|
738
|
+
function asOutboxRequeueStore(store) {
|
|
739
|
+
const candidate = store;
|
|
740
|
+
return typeof candidate.requeueOutboxDeadLetter === "function" && typeof candidate.getOutboxRecord === "function" && typeof candidate.listOutboxRequeues === "function" ? store : void 0;
|
|
741
|
+
}
|
|
742
|
+
function createOutboxRequeue(deps) {
|
|
743
|
+
return async function requeueOutboxDeadLetter(input) {
|
|
744
|
+
for (const field of ["outboxId", "tenantId", "spaceId", "actorId", "actorType"]) {
|
|
745
|
+
if (typeof input[field] !== "string" || input[field].trim() === "") {
|
|
746
|
+
throw new Error(`Outbox requeue requires ${field}.`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (typeof input.reason !== "string" || input.reason.trim() === "") {
|
|
750
|
+
throw new Error("Outbox requeue requires a reason.");
|
|
751
|
+
}
|
|
752
|
+
if (input.reason.length > MAX_REASON_LENGTH) {
|
|
753
|
+
throw new Error(`Outbox requeue reason must be at most ${MAX_REASON_LENGTH} characters.`);
|
|
754
|
+
}
|
|
755
|
+
if (input.availableAt !== void 0 && !(input.availableAt instanceof Date && Number.isFinite(input.availableAt.getTime()))) {
|
|
756
|
+
throw new Error("Outbox requeue availableAt must be a valid Date.");
|
|
757
|
+
}
|
|
758
|
+
const store = asOutboxRequeueStore(deps.store);
|
|
759
|
+
if (!store) throw new Error("Outbox requeue requires a store with outbox requeue support.");
|
|
760
|
+
const refuse = (refusal, message, currentStatus) => {
|
|
761
|
+
emitPlatformHostTelemetry(deps.telemetry, {
|
|
762
|
+
kind: "event",
|
|
763
|
+
name: "outbox.requeue_refused",
|
|
764
|
+
metricName: PLATFORM_HOST_METRIC_NAMES.outboxRequeueRefused,
|
|
765
|
+
occurredAt: deps.now(),
|
|
766
|
+
tenantId: input.tenantId,
|
|
767
|
+
spaceId: input.spaceId,
|
|
768
|
+
attributes: { refusal }
|
|
769
|
+
});
|
|
770
|
+
return { status: "refused", refusal, message, ...currentStatus ? { currentStatus } : {} };
|
|
771
|
+
};
|
|
772
|
+
const record = await store.getOutboxRecord(input.outboxId, input.tenantId, input.spaceId);
|
|
773
|
+
const authorization = deps.authorization;
|
|
774
|
+
const hasAuthorizer = typeof authorization.authorizeOutboxRequeue === "function";
|
|
775
|
+
const authorized = hasAuthorizer ? await authorization.authorizeOutboxRequeue({
|
|
776
|
+
tenantId: input.tenantId,
|
|
777
|
+
spaceId: input.spaceId,
|
|
778
|
+
actorId: input.actorId,
|
|
779
|
+
actorType: input.actorType,
|
|
780
|
+
outboxId: input.outboxId,
|
|
781
|
+
reason: input.reason,
|
|
782
|
+
...record ? {
|
|
783
|
+
record: {
|
|
784
|
+
eventId: record.event.eventId,
|
|
785
|
+
eventType: record.event.eventType,
|
|
786
|
+
payloadClassification: record.event.payloadClassification,
|
|
787
|
+
status: record.status,
|
|
788
|
+
attemptCount: record.attemptCount
|
|
789
|
+
}
|
|
790
|
+
} : {}
|
|
791
|
+
}) : false;
|
|
792
|
+
if (authorized !== true) {
|
|
793
|
+
return refuse(
|
|
794
|
+
"not_authorized",
|
|
795
|
+
hasAuthorizer ? `Actor ${input.actorId} is not authorized to requeue outbox records in this scope.` : "Outbox requeue is refused because no authorizeOutboxRequeue is configured."
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
if (!record) return refuse("not_found", `Outbox record not found: ${input.outboxId}`);
|
|
799
|
+
if (record.status !== "dead_letter") {
|
|
800
|
+
return refuse("not_dead_letter", `Outbox record ${input.outboxId} is ${record.status}, not dead_letter.`, record.status);
|
|
801
|
+
}
|
|
802
|
+
const requeuedAt = deps.now();
|
|
803
|
+
const result = await store.requeueOutboxDeadLetter({
|
|
804
|
+
id: input.outboxId,
|
|
805
|
+
tenantId: input.tenantId,
|
|
806
|
+
spaceId: input.spaceId,
|
|
807
|
+
requeueId: createFabricId("orq"),
|
|
808
|
+
actorId: input.actorId,
|
|
809
|
+
actorType: input.actorType,
|
|
810
|
+
reason: input.reason,
|
|
811
|
+
requeuedAt,
|
|
812
|
+
availableAt: input.availableAt ?? requeuedAt,
|
|
813
|
+
expectedLeaseToken: record.leaseToken ?? 0
|
|
814
|
+
});
|
|
815
|
+
if (!result.applied) {
|
|
816
|
+
if (result.refusal === "not_found") return refuse("not_found", `Outbox record not found: ${input.outboxId}`);
|
|
817
|
+
if (result.refusal === "stale") {
|
|
818
|
+
return refuse(
|
|
819
|
+
"stale",
|
|
820
|
+
`Outbox record ${input.outboxId} changed after it was authorized; read and authorize it again.`,
|
|
821
|
+
result.currentStatus
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
return refuse(
|
|
825
|
+
"not_dead_letter",
|
|
826
|
+
`Outbox record ${input.outboxId} is ${result.currentStatus ?? "no longer dead_letter"}, not dead_letter.`,
|
|
827
|
+
result.currentStatus
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
emitPlatformHostTelemetry(deps.telemetry, {
|
|
831
|
+
kind: "event",
|
|
832
|
+
name: "outbox.requeued",
|
|
833
|
+
metricName: PLATFORM_HOST_METRIC_NAMES.outboxRequeued,
|
|
834
|
+
occurredAt: requeuedAt,
|
|
835
|
+
tenantId: input.tenantId,
|
|
836
|
+
spaceId: input.spaceId,
|
|
837
|
+
attributes: { priorAttempt: result.requeue.priorAttemptCount }
|
|
838
|
+
});
|
|
839
|
+
return { status: "requeued", record: result.record, requeue: result.requeue };
|
|
840
|
+
};
|
|
841
|
+
}
|
|
499
842
|
|
|
500
843
|
// src/host.ts
|
|
501
844
|
var DEFAULT_EXTRACT_EVENTS = (data) => {
|
|
@@ -505,6 +848,12 @@ var DEFAULT_EXTRACT_EVENTS = (data) => {
|
|
|
505
848
|
var RecoverableFinalizationError = class extends Error {
|
|
506
849
|
name = "RecoverableFinalizationError";
|
|
507
850
|
};
|
|
851
|
+
var DEFAULT_ADMISSION_TIMEOUT_MS = 1e4;
|
|
852
|
+
var MAX_TIMER_MS = 2147483647;
|
|
853
|
+
var monotonicNow = performance.now.bind(performance);
|
|
854
|
+
var ADMISSION_FAILED = Object.freeze({ kind: "failed" });
|
|
855
|
+
var ADMISSION_TIMED_OUT = Object.freeze({ kind: "timed_out" });
|
|
856
|
+
var ADMISSION_REFUSAL_CODE = /^[a-z][A-Za-z0-9_.:-]{0,127}$/;
|
|
508
857
|
function createGovernedActionHost(options) {
|
|
509
858
|
if (options.composition) {
|
|
510
859
|
if (!options.registry?.orderedModules) {
|
|
@@ -526,45 +875,63 @@ function createGovernedActionHost(options) {
|
|
|
526
875
|
}
|
|
527
876
|
}
|
|
528
877
|
const adapters = new AdapterRegistry();
|
|
529
|
-
for (const adapter of options.adapters ?? [])
|
|
878
|
+
for (const adapter of options.adapters ?? []) {
|
|
879
|
+
if (adapter.circuitBreaker) assertAdapterCircuitBreakerConfig(adapter.circuitBreaker);
|
|
880
|
+
adapters.register(adapter);
|
|
881
|
+
}
|
|
530
882
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
883
|
+
const circuitBreakers = options.adapterCircuitBreakers ?? new AdapterCircuitBreakerRegistry({ now: () => now().getTime() });
|
|
531
884
|
const extractEvents = options.extractEvents ?? DEFAULT_EXTRACT_EVENTS;
|
|
532
885
|
const configuredActionResolver = options.resolveAction ?? options.registry?.resolveAction;
|
|
533
886
|
if (!configuredActionResolver) throw new Error("Platform Host requires an explicit module registry or resolveAction function.");
|
|
534
887
|
const actionResolver = configuredActionResolver;
|
|
535
888
|
const stateMachineResolver = options.registry?.resolveStateMachine ?? (() => void 0);
|
|
536
889
|
const eventResultFields = options.eventResultFields ?? ["_events"];
|
|
890
|
+
const admissionTimeoutMs = options.admissionTimeoutMs ?? DEFAULT_ADMISSION_TIMEOUT_MS;
|
|
891
|
+
if (typeof admissionTimeoutMs !== "number" || !Number.isInteger(admissionTimeoutMs) || admissionTimeoutMs < 1 || admissionTimeoutMs > MAX_TIMER_MS) {
|
|
892
|
+
throw new Error(`admissionTimeoutMs must be a whole number of milliseconds from 1 to ${MAX_TIMER_MS}.`);
|
|
893
|
+
}
|
|
537
894
|
async function submitAction(input) {
|
|
538
|
-
|
|
539
|
-
|
|
895
|
+
const read = snapshotSubmission(input);
|
|
896
|
+
if (read.executionReason && !["initial", "offline_replay"].includes(read.executionReason)) {
|
|
897
|
+
throw new Error(`Unsupported submitted execution reason: ${read.executionReason}`);
|
|
540
898
|
}
|
|
541
|
-
const action = actionResolver(
|
|
542
|
-
if (!action) throw new Error(`Unknown action: ${
|
|
543
|
-
|
|
544
|
-
|
|
899
|
+
const action = actionResolver(read.actionId);
|
|
900
|
+
if (!action) throw new Error(`Unknown action: ${read.actionId}`);
|
|
901
|
+
const declaresAdmission = action.admitSubmission !== void 0;
|
|
902
|
+
const submission = declaresAdmission ? privateSubmission(read) : read;
|
|
903
|
+
if (action.execution?.connectivity === "online-required" && submission.executionReason === "offline_replay") {
|
|
904
|
+
throw new Error(`Action ${submission.actionId} is online-required and cannot be submitted as an offline replay.`);
|
|
545
905
|
}
|
|
546
|
-
if (action.execution?.sensitivity === "restricted" && Object.keys(
|
|
547
|
-
throw new Error(`Action ${
|
|
906
|
+
if (action.execution?.sensitivity === "restricted" && Object.keys(submission.provenance?.auditAttributes ?? {}).length > 0) {
|
|
907
|
+
throw new Error(`Action ${submission.actionId} declares restricted sensitivity; provenance audit attributes must not be durably recorded.`);
|
|
548
908
|
}
|
|
549
|
-
const authorizationInput = toAuthorizationInput(action,
|
|
909
|
+
const authorizationInput = toAuthorizationInput(action, submission);
|
|
550
910
|
if (!await options.authorization.checkEntitlement(authorizationInput)) {
|
|
551
|
-
throw new Error(`Module "${action.namespace}" is not enabled for tenant ${
|
|
911
|
+
throw new Error(`Module "${action.namespace}" is not enabled for tenant ${submission.tenantId}`);
|
|
552
912
|
}
|
|
553
913
|
if (!await options.authorization.authorize(authorizationInput)) {
|
|
554
|
-
throw new Error(`Actor ${
|
|
914
|
+
throw new Error(`Actor ${submission.actorId} is not authorized for action ${submission.actionId}`);
|
|
915
|
+
}
|
|
916
|
+
if (declaresAdmission && (!submission.parametersArePrivate || !isRecord(submission.parameters))) {
|
|
917
|
+
refuseAdmission(action, submission, "invalid_parameters");
|
|
555
918
|
}
|
|
556
919
|
const actionInvocationId = createFabricId("act");
|
|
557
|
-
if (
|
|
558
|
-
if (
|
|
559
|
-
const correlationId =
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
920
|
+
if (submission.correlationId && submission.provenance?.correlationId && submission.correlationId !== submission.provenance.correlationId) throw new Error("Invocation provenance correlationId does not match submission correlationId.");
|
|
921
|
+
if (submission.causationId && submission.provenance?.causationId && submission.causationId !== submission.provenance.causationId) throw new Error("Invocation provenance causationId does not match submission causationId.");
|
|
922
|
+
const correlationId = submission.correlationId ?? submission.provenance?.correlationId ?? createFabricId("corr");
|
|
923
|
+
const prepare = () => {
|
|
924
|
+
const durableParameters2 = options.redactActionParameters ? options.redactActionParameters(submission.actionId, submission.parameters) : submission.parameters;
|
|
925
|
+
const parameterDigest2 = digestParameters(submission.parameters);
|
|
926
|
+
if (submission.authorizationBinding) validateAuthorizationBinding(submission, parameterDigest2, action.execution?.authorityMoment, now());
|
|
927
|
+
return { durableParameters: durableParameters2, parameterDigest: parameterDigest2 };
|
|
928
|
+
};
|
|
929
|
+
const early = declaresAdmission ? void 0 : prepare();
|
|
563
930
|
const durableProvenance = normalizeProvenance(
|
|
564
|
-
|
|
931
|
+
submission.provenance,
|
|
565
932
|
options.provenance
|
|
566
933
|
);
|
|
567
|
-
const initiatingReleaseDigest = await options.composition?.resolveInitiatingReleaseDigest?.(input);
|
|
934
|
+
const initiatingReleaseDigest = await options.composition?.resolveInitiatingReleaseDigest?.(declaresAdmission ? submissionView(submission) : liveSubmissionView(submission, input));
|
|
568
935
|
const runtimeEvidence = {
|
|
569
936
|
governanceContractVersion: FABRIC_GOVERNANCE_CONTRACT_VERSION,
|
|
570
937
|
hostContractVersion: PLATFORM_HOST_CONTRACT_VERSION,
|
|
@@ -573,48 +940,51 @@ function createGovernedActionHost(options) {
|
|
|
573
940
|
...initiatingReleaseDigest ? { initiatingReleaseDigest } : {}
|
|
574
941
|
};
|
|
575
942
|
assertGovernanceRuntimeEvidence(runtimeEvidence);
|
|
943
|
+
const admission = declaresAdmission ? await admitSubmission(action, submission) : void 0;
|
|
944
|
+
const { durableParameters, parameterDigest } = early ?? prepare();
|
|
576
945
|
const durableInvocation = await options.store.createActionInvocation({
|
|
577
946
|
id: actionInvocationId,
|
|
578
|
-
tenantId:
|
|
579
|
-
spaceId:
|
|
580
|
-
actionId:
|
|
947
|
+
tenantId: submission.tenantId,
|
|
948
|
+
spaceId: submission.spaceId,
|
|
949
|
+
actionId: submission.actionId,
|
|
581
950
|
actionVersion: action.version,
|
|
582
|
-
actorId:
|
|
583
|
-
actorType:
|
|
951
|
+
actorId: submission.actorId,
|
|
952
|
+
actorType: submission.actorType,
|
|
584
953
|
status: "pending",
|
|
585
954
|
parameters: durableParameters,
|
|
586
955
|
result: {},
|
|
587
956
|
runtimeEvidence,
|
|
588
957
|
correlationId,
|
|
589
|
-
...
|
|
590
|
-
...
|
|
591
|
-
...
|
|
958
|
+
...submission.causationId ?? submission.provenance?.causationId ? { causationId: submission.causationId ?? submission.provenance?.causationId } : {},
|
|
959
|
+
...submission.idempotencyKey ? { idempotencyKey: submission.idempotencyKey } : {},
|
|
960
|
+
...submission.idempotencyKey ? {
|
|
592
961
|
parameterDigest,
|
|
593
962
|
parameterDigestAlgorithm: PARAMETER_DIGEST_ALGORITHM,
|
|
594
|
-
idempotencyActorId:
|
|
595
|
-
...
|
|
963
|
+
idempotencyActorId: submission.actorId,
|
|
964
|
+
...submission.authorizationBindingId ? { idempotencyAuthorizationBindingId: submission.authorizationBindingId } : {}
|
|
596
965
|
} : {},
|
|
597
966
|
...durableProvenance ? { provenance: durableProvenance } : {},
|
|
598
|
-
...
|
|
599
|
-
...
|
|
600
|
-
...
|
|
967
|
+
...submission.authorizationBinding ? { authorizationBinding: submission.authorizationBinding } : {},
|
|
968
|
+
...submission.executionReason ? { executionReason: submission.executionReason } : {},
|
|
969
|
+
...submission.authorizationBindingId ? { authorizationBindingId: submission.authorizationBindingId } : {},
|
|
970
|
+
...admission ? { admittedAt: admission.admittedAt } : {}
|
|
601
971
|
});
|
|
602
972
|
emitPlatformHostTelemetry(options.telemetry, {
|
|
603
973
|
kind: "event",
|
|
604
974
|
name: "invocation.submitted",
|
|
605
975
|
metricName: PLATFORM_HOST_METRIC_NAMES.invocationSubmitted,
|
|
606
976
|
occurredAt: now(),
|
|
607
|
-
tenantId:
|
|
608
|
-
spaceId:
|
|
977
|
+
tenantId: submission.tenantId,
|
|
978
|
+
spaceId: submission.spaceId,
|
|
609
979
|
attributes: { actionVersion: action.version }
|
|
610
980
|
});
|
|
611
|
-
if (durableInvocation.id !== actionInvocationId &&
|
|
981
|
+
if (durableInvocation.id !== actionInvocationId && submission.idempotencyKey) {
|
|
612
982
|
const conflict = idempotencyConflict(durableInvocation, {
|
|
613
|
-
actorId:
|
|
614
|
-
authorizationBindingId:
|
|
983
|
+
actorId: submission.actorId,
|
|
984
|
+
authorizationBindingId: submission.authorizationBindingId,
|
|
615
985
|
actionVersion: action.version,
|
|
616
986
|
parameterDigest,
|
|
617
|
-
idempotencyKey:
|
|
987
|
+
idempotencyKey: submission.idempotencyKey
|
|
618
988
|
});
|
|
619
989
|
if (!durableInvocation.parameterDigest) {
|
|
620
990
|
options.idempotency?.onLegacyRecord?.(durableInvocation);
|
|
@@ -634,8 +1004,8 @@ function createGovernedActionHost(options) {
|
|
|
634
1004
|
const dispatched = await options.dispatcher.dispatch({
|
|
635
1005
|
actionInvocationId: durableInvocation.id,
|
|
636
1006
|
actionId: durableInvocation.actionId,
|
|
637
|
-
tenantId:
|
|
638
|
-
spaceId:
|
|
1007
|
+
tenantId: submission.tenantId,
|
|
1008
|
+
spaceId: submission.spaceId,
|
|
639
1009
|
workflowId: durableWorkflowId
|
|
640
1010
|
});
|
|
641
1011
|
return withConsistency({
|
|
@@ -645,7 +1015,7 @@ function createGovernedActionHost(options) {
|
|
|
645
1015
|
...dispatched.runId ? { runId: dispatched.runId } : {},
|
|
646
1016
|
...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
|
|
647
1017
|
...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
|
|
648
|
-
},
|
|
1018
|
+
}, submission.actionId);
|
|
649
1019
|
}
|
|
650
1020
|
return withConsistency({
|
|
651
1021
|
actionInvocationId: durableInvocation.id,
|
|
@@ -658,15 +1028,15 @@ function createGovernedActionHost(options) {
|
|
|
658
1028
|
...durableInvocation.authorizationReconciliation ? { reconciliation: durableInvocation.authorizationReconciliation } : {},
|
|
659
1029
|
...durableInvocation.adapterReconciliation ? { adapterReconciliation: durableInvocation.adapterReconciliation } : {},
|
|
660
1030
|
...durableInvocation.pendingCompletion ? { pendingCompletion: durableInvocation.pendingCompletion } : {}
|
|
661
|
-
},
|
|
1031
|
+
}, submission.actionId);
|
|
662
1032
|
}
|
|
663
1033
|
if (options.dispatcher) {
|
|
664
1034
|
try {
|
|
665
1035
|
const dispatched = await options.dispatcher.dispatch({
|
|
666
1036
|
actionInvocationId: durableInvocation.id,
|
|
667
1037
|
actionId: durableInvocation.actionId,
|
|
668
|
-
tenantId:
|
|
669
|
-
spaceId:
|
|
1038
|
+
tenantId: submission.tenantId,
|
|
1039
|
+
spaceId: submission.spaceId,
|
|
670
1040
|
workflowId: durableWorkflowId
|
|
671
1041
|
});
|
|
672
1042
|
return withConsistency({
|
|
@@ -676,17 +1046,78 @@ function createGovernedActionHost(options) {
|
|
|
676
1046
|
...dispatched.runId ? { runId: dispatched.runId } : {},
|
|
677
1047
|
...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
|
|
678
1048
|
...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
|
|
679
|
-
},
|
|
1049
|
+
}, submission.actionId);
|
|
680
1050
|
} catch (error) {
|
|
681
1051
|
throw error;
|
|
682
1052
|
}
|
|
683
1053
|
}
|
|
684
1054
|
const executed = await executeInvocation(
|
|
685
1055
|
durableInvocation.id,
|
|
686
|
-
|
|
687
|
-
|
|
1056
|
+
submission.tenantId,
|
|
1057
|
+
submission.spaceId
|
|
688
1058
|
);
|
|
689
|
-
return { ...withConsistency(executed,
|
|
1059
|
+
return { ...withConsistency(executed, submission.actionId), workflowId: durableWorkflowId };
|
|
1060
|
+
}
|
|
1061
|
+
async function admitSubmission(action, submission) {
|
|
1062
|
+
const refuse = (refusal, refusalCode) => refuseAdmission(action, submission, refusal, refusalCode);
|
|
1063
|
+
const check = action.admitSubmission;
|
|
1064
|
+
if (typeof check !== "function") return refuse("failed");
|
|
1065
|
+
const checked = parametersCopy(submission);
|
|
1066
|
+
const controller = new AbortController();
|
|
1067
|
+
let timer;
|
|
1068
|
+
const deadline = new Promise((resolve) => {
|
|
1069
|
+
timer = setTimeout(() => {
|
|
1070
|
+
controller.abort();
|
|
1071
|
+
resolve(ADMISSION_TIMED_OUT);
|
|
1072
|
+
}, admissionTimeoutMs);
|
|
1073
|
+
});
|
|
1074
|
+
const startedAt = monotonicNow();
|
|
1075
|
+
const settledInTime = () => monotonicNow() - startedAt < admissionTimeoutMs;
|
|
1076
|
+
let answered;
|
|
1077
|
+
try {
|
|
1078
|
+
answered = Promise.resolve(check(Object.freeze({
|
|
1079
|
+
actionId: action.actionId,
|
|
1080
|
+
tenantId: submission.tenantId,
|
|
1081
|
+
spaceId: submission.spaceId,
|
|
1082
|
+
actorId: submission.actorId,
|
|
1083
|
+
actorType: submission.actorType,
|
|
1084
|
+
parameters: checked,
|
|
1085
|
+
signal: controller.signal
|
|
1086
|
+
}))).then(
|
|
1087
|
+
(value) => settledInTime() ? { kind: "answered", value } : ADMISSION_TIMED_OUT,
|
|
1088
|
+
() => settledInTime() ? ADMISSION_FAILED : ADMISSION_TIMED_OUT
|
|
1089
|
+
);
|
|
1090
|
+
} catch {
|
|
1091
|
+
answered = Promise.resolve(settledInTime() ? ADMISSION_FAILED : ADMISSION_TIMED_OUT);
|
|
1092
|
+
}
|
|
1093
|
+
let outcome;
|
|
1094
|
+
try {
|
|
1095
|
+
outcome = await Promise.race([answered, deadline]);
|
|
1096
|
+
} finally {
|
|
1097
|
+
clearTimeout(timer);
|
|
1098
|
+
}
|
|
1099
|
+
if (outcome.kind === "timed_out") {
|
|
1100
|
+
controller.abort();
|
|
1101
|
+
return refuse("timed_out");
|
|
1102
|
+
}
|
|
1103
|
+
if (outcome.kind === "failed") return refuse("failed");
|
|
1104
|
+
const admitted = ownDataProperty(outcome.value, "admitted");
|
|
1105
|
+
if (admitted === true) return { admittedAt: now() };
|
|
1106
|
+
const code = ownDataProperty(outcome.value, "code");
|
|
1107
|
+
if (admitted === false && typeof code === "string" && ADMISSION_REFUSAL_CODE.test(code)) return refuse("refused", code);
|
|
1108
|
+
return refuse("invalid_decision");
|
|
1109
|
+
}
|
|
1110
|
+
function refuseAdmission(action, submission, refusal, refusalCode) {
|
|
1111
|
+
emitPlatformHostTelemetry(options.telemetry, {
|
|
1112
|
+
kind: "event",
|
|
1113
|
+
name: "invocation.admission_refused",
|
|
1114
|
+
metricName: PLATFORM_HOST_METRIC_NAMES.invocationAdmissionRefused,
|
|
1115
|
+
occurredAt: now(),
|
|
1116
|
+
tenantId: submission.tenantId,
|
|
1117
|
+
spaceId: submission.spaceId,
|
|
1118
|
+
attributes: { actionVersion: action.version, refusal }
|
|
1119
|
+
});
|
|
1120
|
+
throw new ActionAdmissionError(action.actionId, refusal, refusalCode);
|
|
690
1121
|
}
|
|
691
1122
|
async function executeInvocation(actionInvocationId, tenantId, spaceId, executionOptions = {}, executionReasonOverride) {
|
|
692
1123
|
const loadedInvocation = await options.store.getActionInvocation(
|
|
@@ -715,6 +1146,13 @@ function createGovernedActionHost(options) {
|
|
|
715
1146
|
if (!action) {
|
|
716
1147
|
return fail2(invocation, "failed", `Unknown action: ${invocation.actionId}`);
|
|
717
1148
|
}
|
|
1149
|
+
if (action.admitSubmission !== void 0 && !isAdmissionTime(invocation.admittedAt)) {
|
|
1150
|
+
return fail2(
|
|
1151
|
+
invocation,
|
|
1152
|
+
"failed",
|
|
1153
|
+
`Invocation of ${action.actionId} was not admitted at submission; an action that declares admitSubmission runs only on invocations its check admitted.`
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
718
1156
|
if (action.kind === "saga") {
|
|
719
1157
|
return fail2(
|
|
720
1158
|
invocation,
|
|
@@ -882,6 +1320,10 @@ function createGovernedActionHost(options) {
|
|
|
882
1320
|
}
|
|
883
1321
|
const definitions = options.resolvePolicies ? await options.resolvePolicies({
|
|
884
1322
|
...authorizationInput,
|
|
1323
|
+
actionInvocationId,
|
|
1324
|
+
parameters: parsed.data,
|
|
1325
|
+
invocation,
|
|
1326
|
+
executionReason,
|
|
885
1327
|
declaredPolicyIds: action.policies ?? []
|
|
886
1328
|
}) : declaredPolicies(action.policies ?? []);
|
|
887
1329
|
const outcomes = await evaluatePolicyDefinitions(definitions, {
|
|
@@ -992,6 +1434,7 @@ function createGovernedActionHost(options) {
|
|
|
992
1434
|
correlationId: invocation.correlationId,
|
|
993
1435
|
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
994
1436
|
...invocation.provenance ? { provenance: invocation.provenance } : {},
|
|
1437
|
+
...isAdmissionTime(invocation.admittedAt) ? { admittedAt: invocation.admittedAt } : {},
|
|
995
1438
|
db,
|
|
996
1439
|
services: options.services
|
|
997
1440
|
},
|
|
@@ -1119,14 +1562,20 @@ function createGovernedActionHost(options) {
|
|
|
1119
1562
|
);
|
|
1120
1563
|
adapterDeadlineTimer.unref?.();
|
|
1121
1564
|
}
|
|
1565
|
+
const circuitBreaker = circuitBreakers.forAdapter(adapter);
|
|
1122
1566
|
const result2 = await executeWithAdapterRetry({
|
|
1567
|
+
...circuitBreaker ? { circuitBreaker } : {},
|
|
1123
1568
|
policy: step.retryPolicy ?? adapter.retryPolicy,
|
|
1124
1569
|
defaultIdempotent: adapter.idempotent,
|
|
1125
|
-
|
|
1570
|
+
// Every await before the call happens here, before breaker
|
|
1571
|
+
// admission, so nothing awaits between admission and the adapter.
|
|
1572
|
+
prepareAttempt: async (attempt) => {
|
|
1126
1573
|
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
1127
1574
|
attempt,
|
|
1128
1575
|
updatedAt: now()
|
|
1129
1576
|
});
|
|
1577
|
+
},
|
|
1578
|
+
execute: (attempt, maxAttempts) => {
|
|
1130
1579
|
return adapter.execute(input, {
|
|
1131
1580
|
tenantId,
|
|
1132
1581
|
spaceId,
|
|
@@ -1144,6 +1593,7 @@ function createGovernedActionHost(options) {
|
|
|
1144
1593
|
getError: (result3) => result3.error,
|
|
1145
1594
|
classifyOutcome: (result3) => result3.outcome ?? "transient_failure",
|
|
1146
1595
|
classifyThrownError: (error) => {
|
|
1596
|
+
if (isAdapterCircuitOpenError(error)) return "transient_failure";
|
|
1147
1597
|
if (adapterController?.signal.aborted) return "timeout";
|
|
1148
1598
|
const message = error instanceof Error ? error.message : String(error);
|
|
1149
1599
|
if (/timeout|deadline|timed?\s*out/i.test(message)) return "timeout";
|
|
@@ -1321,6 +1771,19 @@ function createGovernedActionHost(options) {
|
|
|
1321
1771
|
error: message,
|
|
1322
1772
|
updatedAt: now()
|
|
1323
1773
|
});
|
|
1774
|
+
if (isAdapterCircuitOpenError(error)) {
|
|
1775
|
+
await appendEvent(invocation, {
|
|
1776
|
+
eventType: "AdapterInvocationFailed",
|
|
1777
|
+
subjectType: adapterEventSubject.subjectType,
|
|
1778
|
+
subjectId: adapterEventSubject.subjectId,
|
|
1779
|
+
payload: {
|
|
1780
|
+
adapterType: step.adapterType,
|
|
1781
|
+
operation: step.operation,
|
|
1782
|
+
reason: "circuit_open",
|
|
1783
|
+
...error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
|
|
1784
|
+
}
|
|
1785
|
+
}, `adapter:${stepIndex}:failed`);
|
|
1786
|
+
}
|
|
1324
1787
|
return fail2(invocation, "failed", message);
|
|
1325
1788
|
} finally {
|
|
1326
1789
|
if (adapterDeadlineTimer) clearTimeout(adapterDeadlineTimer);
|
|
@@ -1574,6 +2037,8 @@ function createGovernedActionHost(options) {
|
|
|
1574
2037
|
producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
|
|
1575
2038
|
payloadClassification,
|
|
1576
2039
|
includeProvenance: options.outbox.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
|
|
2040
|
+
// Actor identity is excluded for every classification unless the application opts in.
|
|
2041
|
+
includeActor: options.outbox.includeActor?.(envelope, payloadClassification) === true,
|
|
1577
2042
|
...traceContext ? { traceContext } : {}
|
|
1578
2043
|
};
|
|
1579
2044
|
if (transaction) {
|
|
@@ -1643,7 +2108,13 @@ function createGovernedActionHost(options) {
|
|
|
1643
2108
|
recordExecutionAttestation,
|
|
1644
2109
|
recordExternalReconciliation,
|
|
1645
2110
|
completeExternalInvocation: externalCompletion.completeExternalInvocation,
|
|
1646
|
-
reconcileOverdueCompletions: externalCompletion.reconcileOverdueCompletions
|
|
2111
|
+
reconcileOverdueCompletions: externalCompletion.reconcileOverdueCompletions,
|
|
2112
|
+
requeueOutboxDeadLetter: createOutboxRequeue({
|
|
2113
|
+
store: options.store,
|
|
2114
|
+
authorization: options.authorization,
|
|
2115
|
+
telemetry: options.telemetry,
|
|
2116
|
+
now
|
|
2117
|
+
})
|
|
1647
2118
|
};
|
|
1648
2119
|
}
|
|
1649
2120
|
function actionResult(invocation) {
|
|
@@ -1705,6 +2176,18 @@ function isTerminal(status) {
|
|
|
1705
2176
|
function isRecord(value) {
|
|
1706
2177
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1707
2178
|
}
|
|
2179
|
+
function isAdmissionTime(value) {
|
|
2180
|
+
return value instanceof Date && Number.isFinite(value.getTime());
|
|
2181
|
+
}
|
|
2182
|
+
function ownDataProperty(value, key) {
|
|
2183
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
2184
|
+
try {
|
|
2185
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
2186
|
+
return descriptor && "value" in descriptor ? descriptor.value : void 0;
|
|
2187
|
+
} catch {
|
|
2188
|
+
return void 0;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
1708
2191
|
function validateAuthorizationBinding(input, parameterDigest, authorityMoment, currentTime = /* @__PURE__ */ new Date()) {
|
|
1709
2192
|
const binding = input.authorizationBinding;
|
|
1710
2193
|
if (binding.id !== input.authorizationBindingId && input.authorizationBindingId) throw new Error("Authorization binding ID does not match authorizationBindingId.");
|
|
@@ -1744,6 +2227,9 @@ function toEnterpriseEventEnvelope(event, metadata) {
|
|
|
1744
2227
|
...event.actionInvocationId ? { actionInvocationId: event.actionInvocationId } : {},
|
|
1745
2228
|
correlationId: event.correlationId,
|
|
1746
2229
|
...event.causationId ? { causationId: event.causationId } : {},
|
|
2230
|
+
// Opt-in only: without it the keys are absent, so existing publishers see an unchanged envelope.
|
|
2231
|
+
...includesActor(metadata) && event.actorId ? { actorId: event.actorId } : {},
|
|
2232
|
+
...includesActor(metadata) && event.actorType ? { actorType: event.actorType } : {},
|
|
1747
2233
|
occurredAt: event.occurredAt,
|
|
1748
2234
|
recordedAt: event.recordedAt,
|
|
1749
2235
|
producerModuleVersion: metadata.producerModuleVersion,
|
|
@@ -1755,6 +2241,12 @@ function toEnterpriseEventEnvelope(event, metadata) {
|
|
|
1755
2241
|
payload: event.payload
|
|
1756
2242
|
};
|
|
1757
2243
|
}
|
|
2244
|
+
function outboxActorDisclosure(metadata) {
|
|
2245
|
+
return includesActor(metadata) ? "included" : "suppressed";
|
|
2246
|
+
}
|
|
2247
|
+
function includesActor(metadata) {
|
|
2248
|
+
return metadata.includeActor === true;
|
|
2249
|
+
}
|
|
1758
2250
|
async function runOutboxRelayCycle(options) {
|
|
1759
2251
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1760
2252
|
const maxAttempts = options.maxAttempts ?? 10;
|
|
@@ -1789,7 +2281,7 @@ async function runOutboxRelayCycle(options) {
|
|
|
1789
2281
|
try {
|
|
1790
2282
|
await publishWithTimeout(
|
|
1791
2283
|
options.publisher,
|
|
1792
|
-
record
|
|
2284
|
+
record,
|
|
1793
2285
|
options.publishTimeoutMs ?? 1e4
|
|
1794
2286
|
);
|
|
1795
2287
|
await stopHeartbeat();
|
|
@@ -1851,7 +2343,7 @@ async function runOutboxRelayCycle(options) {
|
|
|
1851
2343
|
});
|
|
1852
2344
|
return result;
|
|
1853
2345
|
}
|
|
1854
|
-
async function publishWithTimeout(publisher,
|
|
2346
|
+
async function publishWithTimeout(publisher, record, timeoutMs) {
|
|
1855
2347
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1856
2348
|
throw new Error("publishTimeoutMs must be a positive finite number");
|
|
1857
2349
|
}
|
|
@@ -1865,7 +2357,11 @@ async function publishWithTimeout(publisher, event, timeoutMs) {
|
|
|
1865
2357
|
}, timeoutMs);
|
|
1866
2358
|
});
|
|
1867
2359
|
try {
|
|
1868
|
-
|
|
2360
|
+
const context = {
|
|
2361
|
+
signal: controller.signal,
|
|
2362
|
+
actorDisclosure: record.actorDisclosure ?? "legacy"
|
|
2363
|
+
};
|
|
2364
|
+
await Promise.race([publisher.publish(record.event, context), timeout]);
|
|
1869
2365
|
} finally {
|
|
1870
2366
|
if (timer) clearTimeout(timer);
|
|
1871
2367
|
}
|
|
@@ -1895,9 +2391,23 @@ function startOutboxHeartbeat(options, record, leaseDurationMs) {
|
|
|
1895
2391
|
};
|
|
1896
2392
|
}
|
|
1897
2393
|
function cloneOutboxRecord(record) {
|
|
2394
|
+
const event = {
|
|
2395
|
+
...record.event,
|
|
2396
|
+
occurredAt: new Date(record.event.occurredAt),
|
|
2397
|
+
recordedAt: new Date(record.event.recordedAt),
|
|
2398
|
+
...record.event.traceContext ? { traceContext: { ...record.event.traceContext } } : {}
|
|
2399
|
+
};
|
|
2400
|
+
if (record.actorDisclosure !== "included") {
|
|
2401
|
+
delete event.actorId;
|
|
2402
|
+
delete event.actorType;
|
|
2403
|
+
}
|
|
1898
2404
|
return {
|
|
1899
2405
|
...record,
|
|
1900
|
-
|
|
2406
|
+
availableAt: new Date(record.availableAt),
|
|
2407
|
+
createdAt: new Date(record.createdAt),
|
|
2408
|
+
...record.leaseExpiresAt ? { leaseExpiresAt: new Date(record.leaseExpiresAt) } : {},
|
|
2409
|
+
...record.publishedAt ? { publishedAt: new Date(record.publishedAt) } : {},
|
|
2410
|
+
event
|
|
1901
2411
|
};
|
|
1902
2412
|
}
|
|
1903
2413
|
function emitRelayTelemetry(telemetry, name, occurredAt, workerId, attributes) {
|
|
@@ -1929,6 +2439,13 @@ var MemoryPlatformHostStore = class {
|
|
|
1929
2439
|
executionAttestations = [];
|
|
1930
2440
|
externalReconciliations = [];
|
|
1931
2441
|
outbox = [];
|
|
2442
|
+
/**
|
|
2443
|
+
* Requeue evidence is append-only audit history, so unlike the ledger
|
|
2444
|
+
* arrays above, which tests have always inspected and seeded directly, it
|
|
2445
|
+
* is not public: `listOutboxRequeues` returns copies with their own Dates. TypeScript
|
|
2446
|
+
* `private`, as for the store's other internals, is a compile-time guard.
|
|
2447
|
+
*/
|
|
2448
|
+
outboxRequeues = [];
|
|
1932
2449
|
async transaction(run) {
|
|
1933
2450
|
return run(this.db);
|
|
1934
2451
|
}
|
|
@@ -2129,6 +2646,7 @@ var MemoryPlatformHostStore = class {
|
|
|
2129
2646
|
tenantId: event.tenantId,
|
|
2130
2647
|
spaceId: event.spaceId,
|
|
2131
2648
|
event: toEnterpriseEventEnvelope(event, metadata),
|
|
2649
|
+
actorDisclosure: outboxActorDisclosure(metadata),
|
|
2132
2650
|
status: "pending",
|
|
2133
2651
|
attemptCount: 0,
|
|
2134
2652
|
availableAt: createdAt,
|
|
@@ -2170,6 +2688,42 @@ var MemoryPlatformHostStore = class {
|
|
|
2170
2688
|
async listOutbox(input = {}) {
|
|
2171
2689
|
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
2690
|
}
|
|
2691
|
+
async getOutboxRecord(id, tenantId, spaceId) {
|
|
2692
|
+
const record = this.outbox.find((candidate) => candidate.id === id && candidate.tenantId === tenantId && candidate.spaceId === spaceId);
|
|
2693
|
+
return record ? cloneOutboxRecord(record) : void 0;
|
|
2694
|
+
}
|
|
2695
|
+
async requeueOutboxDeadLetter(input) {
|
|
2696
|
+
const record = this.outbox.find((candidate) => candidate.id === input.id && candidate.tenantId === input.tenantId && candidate.spaceId === input.spaceId);
|
|
2697
|
+
if (!record) return { applied: false, refusal: "not_found" };
|
|
2698
|
+
if (record.status !== "dead_letter") return { applied: false, refusal: "not_dead_letter", currentStatus: record.status };
|
|
2699
|
+
if ((record.leaseToken ?? 0) !== input.expectedLeaseToken) return { applied: false, refusal: "stale", currentStatus: record.status };
|
|
2700
|
+
const requeue = {
|
|
2701
|
+
id: input.requeueId,
|
|
2702
|
+
outboxId: record.id,
|
|
2703
|
+
tenantId: record.tenantId,
|
|
2704
|
+
spaceId: record.spaceId,
|
|
2705
|
+
eventId: record.event.eventId,
|
|
2706
|
+
actorId: input.actorId,
|
|
2707
|
+
actorType: input.actorType,
|
|
2708
|
+
reason: input.reason,
|
|
2709
|
+
requeuedAt: new Date(input.requeuedAt),
|
|
2710
|
+
availableAt: new Date(input.availableAt),
|
|
2711
|
+
priorAttemptCount: record.attemptCount,
|
|
2712
|
+
...record.lastError !== void 0 ? { priorLastError: record.lastError } : {},
|
|
2713
|
+
priorAvailableAt: new Date(record.availableAt),
|
|
2714
|
+
priorLeaseToken: record.leaseToken ?? 0
|
|
2715
|
+
};
|
|
2716
|
+
record.status = "pending";
|
|
2717
|
+
record.attemptCount = 0;
|
|
2718
|
+
record.availableAt = new Date(input.availableAt);
|
|
2719
|
+
delete record.leaseOwner;
|
|
2720
|
+
delete record.leaseExpiresAt;
|
|
2721
|
+
this.outboxRequeues.push(requeue);
|
|
2722
|
+
return { applied: true, record: cloneOutboxRecord(record), requeue: cloneOutboxRequeue(requeue) };
|
|
2723
|
+
}
|
|
2724
|
+
async listOutboxRequeues(input) {
|
|
2725
|
+
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);
|
|
2726
|
+
}
|
|
2173
2727
|
requireLeasedOutbox(id, workerId, leaseToken) {
|
|
2174
2728
|
const record = this.outbox.find((candidate) => candidate.id === id);
|
|
2175
2729
|
if (!record || record.leaseOwner !== workerId || (record.leaseToken ?? 0) !== (leaseToken ?? 0)) {
|
|
@@ -2251,6 +2805,14 @@ var MemoryPlatformHostStore = class {
|
|
|
2251
2805
|
return this.events.filter((event) => event.tenantId === tenantId && event.spaceId === spaceId).sort((left, right) => left.sequence - right.sequence);
|
|
2252
2806
|
}
|
|
2253
2807
|
};
|
|
2808
|
+
function cloneOutboxRequeue(requeue) {
|
|
2809
|
+
return {
|
|
2810
|
+
...requeue,
|
|
2811
|
+
requeuedAt: new Date(requeue.requeuedAt),
|
|
2812
|
+
availableAt: new Date(requeue.availableAt),
|
|
2813
|
+
priorAvailableAt: new Date(requeue.priorAvailableAt)
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2254
2816
|
async function applyPostgresMigrations(client, migrations) {
|
|
2255
2817
|
const ordered = validateMigrations(migrations);
|
|
2256
2818
|
const statements = ordered.map((migration) => migrationStatement(migration));
|
|
@@ -2492,6 +3054,43 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
2492
3054
|
ON fabric_platform.action_invocations (completion_due_at)
|
|
2493
3055
|
WHERE completion_due_at IS NOT NULL;
|
|
2494
3056
|
`
|
|
3057
|
+
}, {
|
|
3058
|
+
version: 4,
|
|
3059
|
+
name: "outbox_actor_disclosure",
|
|
3060
|
+
// Nullable with no default, so rows written before this migration stay NULL and read back
|
|
3061
|
+
// as legacy. NOT VALID skips scanning existing rows; the check still applies to every
|
|
3062
|
+
// insert and update from here on.
|
|
3063
|
+
sql: `
|
|
3064
|
+
ALTER TABLE fabric_platform.event_outbox
|
|
3065
|
+
ADD COLUMN actor_disclosure text;
|
|
3066
|
+
ALTER TABLE fabric_platform.event_outbox
|
|
3067
|
+
ADD CONSTRAINT event_outbox_actor_disclosure_check
|
|
3068
|
+
CHECK (actor_disclosure IN ('included', 'suppressed')) NOT VALID;
|
|
3069
|
+
`
|
|
3070
|
+
}, {
|
|
3071
|
+
version: 5,
|
|
3072
|
+
name: "outbox_requeue",
|
|
3073
|
+
sql: `
|
|
3074
|
+
CREATE TABLE fabric_platform.event_outbox_requeues (
|
|
3075
|
+
id text PRIMARY KEY, outbox_id text NOT NULL,
|
|
3076
|
+
tenant_id text NOT NULL, space_id text NOT NULL, event_id text NOT NULL,
|
|
3077
|
+
actor_id text NOT NULL, actor_type text NOT NULL, reason text NOT NULL,
|
|
3078
|
+
requeued_at timestamptz NOT NULL, available_at timestamptz NOT NULL,
|
|
3079
|
+
prior_attempt_count integer NOT NULL, prior_last_error text,
|
|
3080
|
+
prior_available_at timestamptz NOT NULL, prior_lease_token bigint NOT NULL
|
|
3081
|
+
);
|
|
3082
|
+
CREATE INDEX event_outbox_requeues_record_idx
|
|
3083
|
+
ON fabric_platform.event_outbox_requeues (tenant_id, space_id, outbox_id, requeued_at);
|
|
3084
|
+
`
|
|
3085
|
+
}, {
|
|
3086
|
+
version: 6,
|
|
3087
|
+
name: "invocation_admission",
|
|
3088
|
+
// Nullable with no default: rows written before this migration, and
|
|
3089
|
+
// rows of actions that declare no admission check, stay NULL.
|
|
3090
|
+
sql: `
|
|
3091
|
+
ALTER TABLE fabric_platform.action_invocations
|
|
3092
|
+
ADD COLUMN admitted_at timestamptz;
|
|
3093
|
+
`
|
|
2495
3094
|
}]);
|
|
2496
3095
|
}
|
|
2497
3096
|
async transaction(run) {
|
|
@@ -2499,44 +3098,59 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
2499
3098
|
}
|
|
2500
3099
|
async createActionInvocation(input) {
|
|
2501
3100
|
const now = /* @__PURE__ */ new Date();
|
|
2502
|
-
const
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
3101
|
+
const admitted = input.admittedAt !== void 0;
|
|
3102
|
+
const admittedColumn = admitted ? "admitted_at," : "";
|
|
3103
|
+
const admittedValue = admitted ? "$25," : "";
|
|
3104
|
+
let result;
|
|
3105
|
+
try {
|
|
3106
|
+
result = await this.sql.query(
|
|
3107
|
+
`INSERT INTO fabric_platform.action_invocations
|
|
3108
|
+
(id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
|
|
3109
|
+
parameters,result,correlation_id,causation_id,idempotency_key,
|
|
3110
|
+
parameter_digest,parameter_digest_algorithm,idempotency_actor_id,
|
|
3111
|
+
idempotency_authorization_binding_id,invocation_provenance,authorization_binding,execution_reason,
|
|
3112
|
+
authorization_binding_id,error,runtime_evidence,${admittedColumn}created_at,updated_at)
|
|
3113
|
+
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)
|
|
3114
|
+
ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
|
|
3115
|
+
WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
|
|
3116
|
+
RETURNING *`,
|
|
3117
|
+
[
|
|
3118
|
+
input.id,
|
|
3119
|
+
input.tenantId,
|
|
3120
|
+
input.spaceId,
|
|
3121
|
+
input.actionId,
|
|
3122
|
+
input.actionVersion,
|
|
3123
|
+
input.actorId,
|
|
3124
|
+
input.actorType,
|
|
3125
|
+
input.status,
|
|
3126
|
+
JSON.stringify(input.parameters),
|
|
3127
|
+
JSON.stringify(input.result),
|
|
3128
|
+
input.correlationId,
|
|
3129
|
+
input.causationId ?? null,
|
|
3130
|
+
input.idempotencyKey ?? null,
|
|
3131
|
+
input.parameterDigest ?? null,
|
|
3132
|
+
input.parameterDigestAlgorithm ?? null,
|
|
3133
|
+
input.idempotencyActorId ?? null,
|
|
3134
|
+
input.idempotencyAuthorizationBindingId ?? null,
|
|
3135
|
+
input.provenance ? JSON.stringify(input.provenance) : null,
|
|
3136
|
+
input.authorizationBinding ? JSON.stringify(input.authorizationBinding) : null,
|
|
3137
|
+
input.executionReason ?? null,
|
|
3138
|
+
input.authorizationBindingId ?? null,
|
|
3139
|
+
input.error ?? null,
|
|
3140
|
+
input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
|
|
3141
|
+
now,
|
|
3142
|
+
...admitted ? [input.admittedAt] : []
|
|
3143
|
+
]
|
|
3144
|
+
);
|
|
3145
|
+
} catch (error) {
|
|
3146
|
+
if (admitted && error?.code === "42703") {
|
|
3147
|
+
throw new Error(
|
|
3148
|
+
"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.",
|
|
3149
|
+
{ cause: error }
|
|
3150
|
+
);
|
|
3151
|
+
}
|
|
3152
|
+
throw error;
|
|
3153
|
+
}
|
|
2540
3154
|
return toActionRecord(result.rows[0]);
|
|
2541
3155
|
}
|
|
2542
3156
|
async getActionInvocation(id, tenantId, spaceId) {
|
|
@@ -2811,8 +3425,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
2811
3425
|
ON CONFLICT (id) DO NOTHING RETURNING id
|
|
2812
3426
|
)
|
|
2813
3427
|
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
|
|
3428
|
+
(id,tenant_id,space_id,event,actor_disclosure,status,attempt_count,available_at,created_at)
|
|
3429
|
+
SELECT $1,$2,$3,$18::jsonb,$20,'pending',0,$14,$14 FROM inserted_event
|
|
2816
3430
|
ON CONFLICT (id) DO NOTHING`,
|
|
2817
3431
|
[
|
|
2818
3432
|
event.id,
|
|
@@ -2833,7 +3447,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
2833
3447
|
event.causationId ?? null,
|
|
2834
3448
|
event.provenance ? JSON.stringify(event.provenance) : null,
|
|
2835
3449
|
JSON.stringify(envelope),
|
|
2836
|
-
event.consistency ?? null
|
|
3450
|
+
event.consistency ?? null,
|
|
3451
|
+
outboxActorDisclosure(metadata)
|
|
2837
3452
|
]
|
|
2838
3453
|
);
|
|
2839
3454
|
}
|
|
@@ -2901,6 +3516,90 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
2901
3516
|
);
|
|
2902
3517
|
return result.rows.map(toOutboxRecord);
|
|
2903
3518
|
}
|
|
3519
|
+
async getOutboxRecord(id, tenantId, spaceId) {
|
|
3520
|
+
const result = await this.sql.query(
|
|
3521
|
+
`SELECT * FROM fabric_platform.event_outbox
|
|
3522
|
+
WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
|
|
3523
|
+
[id, tenantId, spaceId]
|
|
3524
|
+
);
|
|
3525
|
+
return result.rows[0] ? toOutboxRecord(result.rows[0]) : void 0;
|
|
3526
|
+
}
|
|
3527
|
+
async requeueOutboxDeadLetter(input) {
|
|
3528
|
+
const result = await this.sql.query(
|
|
3529
|
+
`WITH prior AS (
|
|
3530
|
+
SELECT id, tenant_id, space_id, event->>'eventId' AS event_id,
|
|
3531
|
+
attempt_count, last_error, available_at, lease_token
|
|
3532
|
+
FROM fabric_platform.event_outbox
|
|
3533
|
+
WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND status='dead_letter'
|
|
3534
|
+
AND lease_token=$10
|
|
3535
|
+
FOR UPDATE
|
|
3536
|
+
), requeued AS (
|
|
3537
|
+
UPDATE fabric_platform.event_outbox AS item
|
|
3538
|
+
SET status='pending', attempt_count=0, available_at=$9,
|
|
3539
|
+
lease_owner=NULL, lease_expires_at=NULL
|
|
3540
|
+
FROM prior WHERE item.id=prior.id
|
|
3541
|
+
RETURNING item.*
|
|
3542
|
+
), evidence AS (
|
|
3543
|
+
INSERT INTO fabric_platform.event_outbox_requeues
|
|
3544
|
+
(id,outbox_id,tenant_id,space_id,event_id,actor_id,actor_type,reason,
|
|
3545
|
+
requeued_at,available_at,prior_attempt_count,prior_last_error,
|
|
3546
|
+
prior_available_at,prior_lease_token)
|
|
3547
|
+
SELECT $4,prior.id,prior.tenant_id,prior.space_id,prior.event_id,$5,$6,$7,
|
|
3548
|
+
$8,$9,prior.attempt_count,prior.last_error,prior.available_at,prior.lease_token
|
|
3549
|
+
FROM prior JOIN requeued ON requeued.id=prior.id
|
|
3550
|
+
RETURNING *
|
|
3551
|
+
)
|
|
3552
|
+
SELECT requeued.*,
|
|
3553
|
+
evidence.id AS requeue_id, evidence.outbox_id AS requeue_outbox_id,
|
|
3554
|
+
evidence.tenant_id AS requeue_tenant_id, evidence.space_id AS requeue_space_id,
|
|
3555
|
+
evidence.event_id AS requeue_event_id, evidence.actor_id AS requeue_actor_id,
|
|
3556
|
+
evidence.actor_type AS requeue_actor_type, evidence.reason AS requeue_reason,
|
|
3557
|
+
evidence.requeued_at AS requeue_requeued_at, evidence.available_at AS requeue_available_at,
|
|
3558
|
+
evidence.prior_attempt_count AS requeue_prior_attempt_count,
|
|
3559
|
+
evidence.prior_last_error AS requeue_prior_last_error,
|
|
3560
|
+
evidence.prior_available_at AS requeue_prior_available_at,
|
|
3561
|
+
evidence.prior_lease_token AS requeue_prior_lease_token
|
|
3562
|
+
FROM requeued JOIN evidence ON evidence.outbox_id=requeued.id`,
|
|
3563
|
+
[
|
|
3564
|
+
input.id,
|
|
3565
|
+
input.tenantId,
|
|
3566
|
+
input.spaceId,
|
|
3567
|
+
input.requeueId,
|
|
3568
|
+
input.actorId,
|
|
3569
|
+
input.actorType,
|
|
3570
|
+
input.reason,
|
|
3571
|
+
input.requeuedAt,
|
|
3572
|
+
input.availableAt,
|
|
3573
|
+
input.expectedLeaseToken
|
|
3574
|
+
]
|
|
3575
|
+
);
|
|
3576
|
+
const row = result.rows[0];
|
|
3577
|
+
if (row) {
|
|
3578
|
+
return {
|
|
3579
|
+
applied: true,
|
|
3580
|
+
record: toOutboxRecord(row),
|
|
3581
|
+
requeue: toOutboxRequeueRecord(row, "requeue_")
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3584
|
+
const current = await this.sql.query(
|
|
3585
|
+
`SELECT status FROM fabric_platform.event_outbox
|
|
3586
|
+
WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
|
|
3587
|
+
[input.id, input.tenantId, input.spaceId]
|
|
3588
|
+
);
|
|
3589
|
+
const found = current.rows[0];
|
|
3590
|
+
if (!found) return { applied: false, refusal: "not_found" };
|
|
3591
|
+
const status = found.status;
|
|
3592
|
+
return status === "dead_letter" ? { applied: false, refusal: "stale", currentStatus: status } : { applied: false, refusal: "not_dead_letter", currentStatus: status };
|
|
3593
|
+
}
|
|
3594
|
+
async listOutboxRequeues(input) {
|
|
3595
|
+
const result = await this.sql.query(
|
|
3596
|
+
`SELECT * FROM fabric_platform.event_outbox_requeues
|
|
3597
|
+
WHERE tenant_id=$1 AND space_id=$2 AND ($3::text IS NULL OR outbox_id=$3)
|
|
3598
|
+
ORDER BY requeued_at,id`,
|
|
3599
|
+
[input.tenantId, input.spaceId, input.outboxId ?? null]
|
|
3600
|
+
);
|
|
3601
|
+
return result.rows.map((row) => toOutboxRequeueRecord(row, ""));
|
|
3602
|
+
}
|
|
2904
3603
|
async nextEventSequence(tenantId, spaceId) {
|
|
2905
3604
|
const result = await this.sql.query(
|
|
2906
3605
|
`INSERT INTO fabric_platform.event_sequences (tenant_id,space_id,next_sequence)
|
|
@@ -3173,6 +3872,7 @@ function toActionRecord(row) {
|
|
|
3173
3872
|
...row.mutation_footprint ? { mutationFootprint: row.mutation_footprint } : {},
|
|
3174
3873
|
...row.execution_principal ? { executionPrincipal: row.execution_principal } : {},
|
|
3175
3874
|
...row.runtime_evidence ? { runtimeEvidence: row.runtime_evidence } : {},
|
|
3875
|
+
...row.admitted_at ? { admittedAt: new Date(row.admitted_at) } : {},
|
|
3176
3876
|
...row.error ? { error: String(row.error) } : {},
|
|
3177
3877
|
createdAt: new Date(row.created_at),
|
|
3178
3878
|
updatedAt: new Date(row.updated_at)
|
|
@@ -3241,10 +3941,13 @@ function toEventRecord(row) {
|
|
|
3241
3941
|
}
|
|
3242
3942
|
function toOutboxRecord(row) {
|
|
3243
3943
|
const event = row.event;
|
|
3944
|
+
const actorDisclosure = row.actor_disclosure === "included" || row.actor_disclosure === "suppressed" ? row.actor_disclosure : void 0;
|
|
3945
|
+
const actorIncluded = actorDisclosure === "included";
|
|
3244
3946
|
return {
|
|
3245
3947
|
id: String(row.id),
|
|
3246
3948
|
tenantId: String(row.tenant_id),
|
|
3247
3949
|
spaceId: String(row.space_id),
|
|
3950
|
+
...actorDisclosure ? { actorDisclosure } : {},
|
|
3248
3951
|
event: {
|
|
3249
3952
|
eventId: String(event.eventId),
|
|
3250
3953
|
eventType: String(event.eventType),
|
|
@@ -3257,6 +3960,8 @@ function toOutboxRecord(row) {
|
|
|
3257
3960
|
...event.actionInvocationId ? { actionInvocationId: String(event.actionInvocationId) } : {},
|
|
3258
3961
|
correlationId: String(event.correlationId),
|
|
3259
3962
|
...event.causationId ? { causationId: String(event.causationId) } : {},
|
|
3963
|
+
...actorIncluded && event.actorId ? { actorId: String(event.actorId) } : {},
|
|
3964
|
+
...actorIncluded && event.actorType ? { actorType: String(event.actorType) } : {},
|
|
3260
3965
|
...event.provenance ? { provenance: event.provenance } : {},
|
|
3261
3966
|
...event.consistency ? { consistency: event.consistency } : {},
|
|
3262
3967
|
occurredAt: new Date(event.occurredAt),
|
|
@@ -3277,6 +3982,26 @@ function toOutboxRecord(row) {
|
|
|
3277
3982
|
...row.published_at ? { publishedAt: new Date(row.published_at) } : {}
|
|
3278
3983
|
};
|
|
3279
3984
|
}
|
|
3985
|
+
function toOutboxRequeueRecord(row, prefix) {
|
|
3986
|
+
const field = (name) => row[`${prefix}${name}`];
|
|
3987
|
+
const priorLastError = field("prior_last_error");
|
|
3988
|
+
return {
|
|
3989
|
+
id: String(field("id")),
|
|
3990
|
+
outboxId: String(field("outbox_id")),
|
|
3991
|
+
tenantId: String(field("tenant_id")),
|
|
3992
|
+
spaceId: String(field("space_id")),
|
|
3993
|
+
eventId: String(field("event_id")),
|
|
3994
|
+
actorId: String(field("actor_id")),
|
|
3995
|
+
actorType: String(field("actor_type")),
|
|
3996
|
+
reason: String(field("reason")),
|
|
3997
|
+
requeuedAt: new Date(field("requeued_at")),
|
|
3998
|
+
availableAt: new Date(field("available_at")),
|
|
3999
|
+
priorAttemptCount: Number(field("prior_attempt_count")),
|
|
4000
|
+
...priorLastError !== null && priorLastError !== void 0 ? { priorLastError: String(priorLastError) } : {},
|
|
4001
|
+
priorAvailableAt: new Date(field("prior_available_at")),
|
|
4002
|
+
priorLeaseToken: Number(field("prior_lease_token"))
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
3280
4005
|
|
|
3281
4006
|
// src/worker.ts
|
|
3282
4007
|
var DEFAULT_BATCH_SIZE = 10;
|
|
@@ -3737,6 +4462,6 @@ async function runTenantIsolationChecks(subject) {
|
|
|
3737
4462
|
return { passed: checks.every((check) => check.status !== "failed"), checks };
|
|
3738
4463
|
}
|
|
3739
4464
|
|
|
3740
|
-
export { ExternalCompletionError, IdempotencyConflictError, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PLATFORM_HOST_HEALTH_CONTRACT_VERSION, PLATFORM_HOST_METRIC_NAMES, PostgresPlatformHostStore, applyPostgresMigrations,
|
|
4465
|
+
export { ActionAdmissionError, ExternalCompletionError, IdempotencyConflictError, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PLATFORM_HOST_HEALTH_CONTRACT_VERSION, PLATFORM_HOST_METRIC_NAMES, PostgresPlatformHostStore, applyPostgresMigrations, cloneOutboxRecord, createDurableSagaParentLifecycle, createGovernedActionHost, createStoreBackedActionDispatcher, digestParameters, emitPlatformHostTelemetry, getPlatformHostHealthSnapshot, outboxActorDisclosure, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, runTenantIsolationChecks, tenantIsolationChecks, toEnterpriseEventEnvelope };
|
|
3741
4466
|
//# sourceMappingURL=index.js.map
|
|
3742
4467
|
//# sourceMappingURL=index.js.map
|