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