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