@peerbit/trusted-network 6.0.124 → 6.0.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/v2-policy-anchor.d.ts +19 -0
- package/dist/src/v2-policy-anchor.d.ts.map +1 -1
- package/dist/src/v2-policy-anchor.js +185 -6
- package/dist/src/v2-policy-anchor.js.map +1 -1
- package/dist/src/v2-policy-engine.d.ts +11 -1
- package/dist/src/v2-policy-engine.d.ts.map +1 -1
- package/dist/src/v2-policy-engine.js +155 -54
- package/dist/src/v2-policy-engine.js.map +1 -1
- package/package.json +6 -2
- package/src/v2-policy-anchor.ts +218 -14
- package/src/v2-policy-engine.ts +205 -64
package/src/v2-policy-anchor.ts
CHANGED
|
@@ -58,6 +58,7 @@ export const TRUSTED_NETWORK_V2_MAX_POLICY_ANCHOR_CHECKPOINT_PAYLOAD_BYTES =
|
|
|
58
58
|
MAX_CHECKPOINT_PAYLOAD_FRAMING_BYTES_V2;
|
|
59
59
|
const MAX_QUEUED_POLICY_INPUT_BYTES_V2 =
|
|
60
60
|
TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 2;
|
|
61
|
+
const MAX_TIMER_DELAY_MS_V2 = 0x7fffffff;
|
|
61
62
|
const ZERO_DIGEST = new Uint8Array(32);
|
|
62
63
|
const textEncoder = new TextEncoder();
|
|
63
64
|
const CHECKPOINT_SCOPE_DOMAIN = textEncoder.encode(
|
|
@@ -69,6 +70,20 @@ const ANCHOR_STATE = Object.freeze({
|
|
|
69
70
|
FORKED: 3,
|
|
70
71
|
} as const);
|
|
71
72
|
|
|
73
|
+
const isAbortSignalV2 = (value: unknown): value is AbortSignal => {
|
|
74
|
+
if (value === null || typeof value !== "object") return false;
|
|
75
|
+
try {
|
|
76
|
+
const signal = value as AbortSignal;
|
|
77
|
+
return (
|
|
78
|
+
typeof signal.aborted === "boolean" &&
|
|
79
|
+
typeof signal.addEventListener === "function" &&
|
|
80
|
+
typeof signal.removeEventListener === "function"
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
72
87
|
export type CrashSafePolicyAnchorStoreV2 = CrashSafeAtomicReplaceStore;
|
|
73
88
|
|
|
74
89
|
type DurableReducerOptionsV2 = {
|
|
@@ -88,6 +103,14 @@ export type TrustedNetworkV2DurablePolicyReducerOptions =
|
|
|
88
103
|
export type PolicyLeaseReferenceV2 = {
|
|
89
104
|
sequence: bigint;
|
|
90
105
|
digest: Uint8Array;
|
|
106
|
+
/** Maximum number of authenticated parent edges traversed for this lease. */
|
|
107
|
+
maxSteps?: number;
|
|
108
|
+
/** Absolute wall-clock deadline, in milliseconds since the Unix epoch. */
|
|
109
|
+
deadline?: number;
|
|
110
|
+
/** Relative deadline measured from the call, including operation-queue wait. */
|
|
111
|
+
timeoutMs?: number;
|
|
112
|
+
/** Cancels acquisition of this lease without halting the durable reducer. */
|
|
113
|
+
signal?: AbortSignal;
|
|
91
114
|
};
|
|
92
115
|
|
|
93
116
|
export type AcceptedPolicyLeaseV2 = {
|
|
@@ -865,10 +888,23 @@ export class TrustedNetworkV2DurablePolicyReducer {
|
|
|
865
888
|
return this.published.roles.get(keyId(subject)) ?? 0;
|
|
866
889
|
}
|
|
867
890
|
|
|
891
|
+
/**
|
|
892
|
+
* Whether the last durable policy projection is ACTIVE with no replacement
|
|
893
|
+
* publication in flight.
|
|
894
|
+
*
|
|
895
|
+
* This is the subject-independent form of the publication fence used by
|
|
896
|
+
* `isAuthorized()`. Internal resource reducers use it to stop serving an
|
|
897
|
+
* otherwise ACTIVE resource while a policy replacement is in flight or the
|
|
898
|
+
* policy anchor has failed closed. It does not prove complete external-log
|
|
899
|
+
* replay or global freshness.
|
|
900
|
+
*/
|
|
901
|
+
isUsable(): boolean {
|
|
902
|
+
return this.authorizationFences === 0 && this.state === "ACTIVE";
|
|
903
|
+
}
|
|
904
|
+
|
|
868
905
|
isAuthorized(subject: PublicSignKey, roles: number): boolean {
|
|
869
906
|
if (
|
|
870
|
-
this.
|
|
871
|
-
this.state !== "ACTIVE" ||
|
|
907
|
+
!this.isUsable() ||
|
|
872
908
|
!Number.isInteger(roles) ||
|
|
873
909
|
roles === 0 ||
|
|
874
910
|
(roles & ~TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS) !== 0
|
|
@@ -991,6 +1027,9 @@ export class TrustedNetworkV2DurablePolicyReducer {
|
|
|
991
1027
|
|
|
992
1028
|
let sequence: bigint;
|
|
993
1029
|
let suppliedDigest: Uint8Array;
|
|
1030
|
+
let maxSteps: number | undefined;
|
|
1031
|
+
let deadline: number | undefined;
|
|
1032
|
+
let signal: AbortSignal | undefined;
|
|
994
1033
|
try {
|
|
995
1034
|
sequence = reference.sequence;
|
|
996
1035
|
if (
|
|
@@ -1010,6 +1049,54 @@ export class TrustedNetworkV2DurablePolicyReducer {
|
|
|
1010
1049
|
reason: "Policy reference digest must contain exactly 32 bytes",
|
|
1011
1050
|
});
|
|
1012
1051
|
}
|
|
1052
|
+
maxSteps = reference.maxSteps;
|
|
1053
|
+
if (
|
|
1054
|
+
maxSteps !== undefined &&
|
|
1055
|
+
(!Number.isSafeInteger(maxSteps) || maxSteps < 0)
|
|
1056
|
+
) {
|
|
1057
|
+
return Promise.resolve({
|
|
1058
|
+
status: "rejected",
|
|
1059
|
+
reason: "Policy lease maxSteps must be a non-negative safe integer",
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
const suppliedDeadline = reference.deadline;
|
|
1063
|
+
if (
|
|
1064
|
+
suppliedDeadline !== undefined &&
|
|
1065
|
+
(!Number.isSafeInteger(suppliedDeadline) || suppliedDeadline < 0)
|
|
1066
|
+
) {
|
|
1067
|
+
return Promise.resolve({
|
|
1068
|
+
status: "rejected",
|
|
1069
|
+
reason: "Policy lease deadline must be a non-negative safe integer",
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
const timeoutMs = reference.timeoutMs;
|
|
1073
|
+
if (
|
|
1074
|
+
timeoutMs !== undefined &&
|
|
1075
|
+
(!Number.isSafeInteger(timeoutMs) || timeoutMs < 0)
|
|
1076
|
+
) {
|
|
1077
|
+
return Promise.resolve({
|
|
1078
|
+
status: "rejected",
|
|
1079
|
+
reason: "Policy lease timeoutMs must be a non-negative safe integer",
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
const requestedAt = Date.now();
|
|
1083
|
+
const relativeDeadline =
|
|
1084
|
+
timeoutMs === undefined
|
|
1085
|
+
? undefined
|
|
1086
|
+
: Math.min(Number.MAX_SAFE_INTEGER, requestedAt + timeoutMs);
|
|
1087
|
+
deadline =
|
|
1088
|
+
suppliedDeadline === undefined
|
|
1089
|
+
? relativeDeadline
|
|
1090
|
+
: relativeDeadline === undefined
|
|
1091
|
+
? suppliedDeadline
|
|
1092
|
+
: Math.min(suppliedDeadline, relativeDeadline);
|
|
1093
|
+
signal = reference.signal;
|
|
1094
|
+
if (signal !== undefined && !isAbortSignalV2(signal)) {
|
|
1095
|
+
return Promise.resolve({
|
|
1096
|
+
status: "rejected",
|
|
1097
|
+
reason: "Policy lease signal must be an AbortSignal",
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1013
1100
|
} catch {
|
|
1014
1101
|
return Promise.resolve({
|
|
1015
1102
|
status: "rejected",
|
|
@@ -1041,20 +1128,120 @@ export class TrustedNetworkV2DurablePolicyReducer {
|
|
|
1041
1128
|
});
|
|
1042
1129
|
}
|
|
1043
1130
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1131
|
+
let queueAcquired = false;
|
|
1132
|
+
let callbackAcquired = false;
|
|
1133
|
+
let preAcquisitionResult: PolicyLeaseResultV2<T> | undefined;
|
|
1134
|
+
let resolveEarly!: (result: PolicyLeaseResultV2<T>) => void;
|
|
1135
|
+
const earlyResult = new Promise<PolicyLeaseResultV2<T>>((resolve) => {
|
|
1136
|
+
resolveEarly = resolve;
|
|
1137
|
+
});
|
|
1138
|
+
let deadlineTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1139
|
+
let signalListenerInstalled = false;
|
|
1140
|
+
|
|
1141
|
+
const abortBeforeAcquisition = (): void => {
|
|
1142
|
+
settleBeforeAcquisition({
|
|
1143
|
+
status: "unavailable",
|
|
1144
|
+
reason: "Policy lease acquisition was aborted by the caller",
|
|
1145
|
+
});
|
|
1146
|
+
};
|
|
1147
|
+
const cleanupWakeups = (): void => {
|
|
1148
|
+
if (deadlineTimer !== undefined) {
|
|
1149
|
+
clearTimeout(deadlineTimer);
|
|
1150
|
+
deadlineTimer = undefined;
|
|
1151
|
+
}
|
|
1152
|
+
if (signalListenerInstalled) {
|
|
1153
|
+
signalListenerInstalled = false;
|
|
1154
|
+
try {
|
|
1155
|
+
signal?.removeEventListener("abort", abortBeforeAcquisition);
|
|
1156
|
+
} catch {
|
|
1157
|
+
// A structurally valid hostile signal must not break queue cleanup.
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
function settleBeforeAcquisition(result: PolicyLeaseResultV2<T>): void {
|
|
1162
|
+
if (
|
|
1163
|
+
queueAcquired ||
|
|
1164
|
+
callbackAcquired ||
|
|
1165
|
+
preAcquisitionResult !== undefined
|
|
1166
|
+
) {
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
preAcquisitionResult = result;
|
|
1170
|
+
cleanupWakeups();
|
|
1171
|
+
resolveEarly(result);
|
|
1172
|
+
}
|
|
1173
|
+
const armDeadlineWakeup = (): void => {
|
|
1174
|
+
if (
|
|
1175
|
+
deadline === undefined ||
|
|
1176
|
+
queueAcquired ||
|
|
1177
|
+
callbackAcquired ||
|
|
1178
|
+
preAcquisitionResult !== undefined
|
|
1179
|
+
) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const remaining = deadline - Date.now();
|
|
1183
|
+
if (remaining <= 0) {
|
|
1184
|
+
settleBeforeAcquisition({
|
|
1185
|
+
status: "unavailable",
|
|
1186
|
+
reason: "Policy lease acquisition deadline elapsed",
|
|
1187
|
+
});
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
try {
|
|
1191
|
+
deadlineTimer = setTimeout(
|
|
1192
|
+
() => {
|
|
1193
|
+
deadlineTimer = undefined;
|
|
1194
|
+
armDeadlineWakeup();
|
|
1195
|
+
},
|
|
1196
|
+
Math.min(remaining, MAX_TIMER_DELAY_MS_V2),
|
|
1197
|
+
);
|
|
1198
|
+
} catch {
|
|
1199
|
+
settleBeforeAcquisition({
|
|
1200
|
+
status: "unavailable",
|
|
1201
|
+
reason: "Policy lease acquisition deadline could not be scheduled",
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
const queuedResult: Promise<PolicyLeaseResultV2<T>> =
|
|
1207
|
+
this.operationTail.then(async () => {
|
|
1208
|
+
if (preAcquisitionResult !== undefined) {
|
|
1209
|
+
return preAcquisitionResult;
|
|
1210
|
+
}
|
|
1211
|
+
queueAcquired = true;
|
|
1212
|
+
cleanupWakeups();
|
|
1046
1213
|
const queuedHalt = this.immediateLeaseHaltedResult();
|
|
1047
1214
|
if (queuedHalt !== undefined) return queuedHalt;
|
|
1048
|
-
const resolution = await this.core.resolveAcceptedPolicyPrefix(
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1215
|
+
const resolution = await this.core.resolveAcceptedPolicyPrefix(
|
|
1216
|
+
{
|
|
1217
|
+
sequence,
|
|
1218
|
+
digest,
|
|
1219
|
+
},
|
|
1220
|
+
{ maxSteps, deadline, signal },
|
|
1221
|
+
);
|
|
1222
|
+
if (preAcquisitionResult !== undefined) {
|
|
1223
|
+
return preAcquisitionResult;
|
|
1224
|
+
}
|
|
1052
1225
|
if (resolution.status !== "resolved") return resolution;
|
|
1053
1226
|
// Lifecycle abort before callback invocation loses acquisition. There is
|
|
1054
1227
|
// no await between this check and invoking user code, which is the lease's
|
|
1055
1228
|
// linearization point.
|
|
1056
1229
|
const resolvedHalt = this.immediateLeaseHaltedResult();
|
|
1057
1230
|
if (resolvedHalt !== undefined) return resolvedHalt;
|
|
1231
|
+
if (signal?.aborted) {
|
|
1232
|
+
return {
|
|
1233
|
+
status: "unavailable",
|
|
1234
|
+
reason: "Policy lease acquisition was aborted by the caller",
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
if (deadline !== undefined && Date.now() >= deadline) {
|
|
1238
|
+
return {
|
|
1239
|
+
status: "unavailable",
|
|
1240
|
+
reason: "Policy lease acquisition deadline elapsed",
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
callbackAcquired = true;
|
|
1244
|
+
cleanupWakeups();
|
|
1058
1245
|
const value = await use({
|
|
1059
1246
|
// The core created these two projections independently and retains
|
|
1060
1247
|
// neither, including when the requested policy is the current head.
|
|
@@ -1062,15 +1249,32 @@ export class TrustedNetworkV2DurablePolicyReducer {
|
|
|
1062
1249
|
acceptedHead: resolution.acceptedHead,
|
|
1063
1250
|
});
|
|
1064
1251
|
return { status: "completed", value };
|
|
1065
|
-
}
|
|
1066
|
-
)
|
|
1067
|
-
|
|
1252
|
+
});
|
|
1253
|
+
const retainedResult = queuedResult.finally(() => {
|
|
1254
|
+
cleanupWakeups();
|
|
1255
|
+
this.releaseOperation(retainedReferenceBytes);
|
|
1256
|
+
});
|
|
1257
|
+
this.operationTail = retainedResult.then(
|
|
1068
1258
|
(): void => {},
|
|
1069
1259
|
(): void => {},
|
|
1070
1260
|
);
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1261
|
+
|
|
1262
|
+
if (signal !== undefined) {
|
|
1263
|
+
signalListenerInstalled = true;
|
|
1264
|
+
try {
|
|
1265
|
+
signal.addEventListener("abort", abortBeforeAcquisition, {
|
|
1266
|
+
once: true,
|
|
1267
|
+
});
|
|
1268
|
+
} catch {
|
|
1269
|
+
settleBeforeAcquisition({
|
|
1270
|
+
status: "rejected",
|
|
1271
|
+
reason: "Policy lease signal could not be observed",
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
if (signal.aborted) abortBeforeAcquisition();
|
|
1275
|
+
}
|
|
1276
|
+
armDeadlineWakeup();
|
|
1277
|
+
return Promise.race([retainedResult, earlyResult]);
|
|
1074
1278
|
}
|
|
1075
1279
|
|
|
1076
1280
|
private forkFailStopResult(): PolicyAdmissionResultV2 {
|
package/src/v2-policy-engine.ts
CHANGED
|
@@ -47,6 +47,20 @@ const TYPED_ARRAY_TAG = Object.getOwnPropertyDescriptor(
|
|
|
47
47
|
const ARRAY_BUFFER_IS_VIEW = ArrayBuffer.isView;
|
|
48
48
|
const UINT8_ARRAY_SET = Uint8Array.prototype.set;
|
|
49
49
|
|
|
50
|
+
const isAbortSignalV2 = (value: unknown): value is AbortSignal => {
|
|
51
|
+
if (value === null || typeof value !== "object") return false;
|
|
52
|
+
try {
|
|
53
|
+
const signal = value as AbortSignal;
|
|
54
|
+
return (
|
|
55
|
+
typeof signal.aborted === "boolean" &&
|
|
56
|
+
typeof signal.addEventListener === "function" &&
|
|
57
|
+
typeof signal.removeEventListener === "function"
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
50
64
|
const exactUint8ArrayByteLength = (input: unknown): number => {
|
|
51
65
|
if (
|
|
52
66
|
!ARRAY_BUFFER_IS_VIEW(input) ||
|
|
@@ -162,6 +176,16 @@ export type AcceptedPolicyPrefixResolutionV2 =
|
|
|
162
176
|
reason: string;
|
|
163
177
|
};
|
|
164
178
|
|
|
179
|
+
/** Per-call bounds for the internal accepted-prefix history walk. */
|
|
180
|
+
export type AcceptedPolicyPrefixTraversalV2 = {
|
|
181
|
+
/** Maximum number of authenticated parent edges that may be resolved. */
|
|
182
|
+
maxSteps?: number;
|
|
183
|
+
/** Absolute wall-clock deadline, in milliseconds since the Unix epoch. */
|
|
184
|
+
deadline?: number;
|
|
185
|
+
/** Cancels this resolution without halting the reducer. */
|
|
186
|
+
signal?: AbortSignal;
|
|
187
|
+
};
|
|
188
|
+
|
|
165
189
|
export type PolicyForkChildProofV2 = {
|
|
166
190
|
sequence: bigint;
|
|
167
191
|
digest: Uint8Array;
|
|
@@ -751,12 +775,16 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
751
775
|
* users of this internal class. Historical snapshots remain resolver-backed;
|
|
752
776
|
* this walk retains only one authenticated cursor at a time.
|
|
753
777
|
*/
|
|
754
|
-
resolveAcceptedPolicyPrefix(
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
778
|
+
resolveAcceptedPolicyPrefix(
|
|
779
|
+
reference: {
|
|
780
|
+
sequence: bigint;
|
|
781
|
+
digest: Uint8Array;
|
|
782
|
+
},
|
|
783
|
+
traversal?: AcceptedPolicyPrefixTraversalV2,
|
|
784
|
+
): Promise<AcceptedPolicyPrefixResolutionV2> {
|
|
758
785
|
let capturedSequence: bigint;
|
|
759
786
|
let capturedDigest: Uint8Array;
|
|
787
|
+
let capturedTraversal: AcceptedPolicyPrefixTraversalV2;
|
|
760
788
|
try {
|
|
761
789
|
capturedSequence = reference.sequence;
|
|
762
790
|
if (
|
|
@@ -774,6 +802,34 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
774
802
|
throw new Error("invalid digest");
|
|
775
803
|
}
|
|
776
804
|
capturedDigest = copyBytes(suppliedDigest);
|
|
805
|
+
const maxSteps = traversal?.maxSteps;
|
|
806
|
+
if (
|
|
807
|
+
maxSteps !== undefined &&
|
|
808
|
+
(!Number.isSafeInteger(maxSteps) || maxSteps < 0)
|
|
809
|
+
) {
|
|
810
|
+
return Promise.resolve({
|
|
811
|
+
status: "rejected",
|
|
812
|
+
reason: "Policy prefix maxSteps must be a non-negative safe integer",
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
const deadline = traversal?.deadline;
|
|
816
|
+
if (
|
|
817
|
+
deadline !== undefined &&
|
|
818
|
+
(!Number.isSafeInteger(deadline) || deadline < 0)
|
|
819
|
+
) {
|
|
820
|
+
return Promise.resolve({
|
|
821
|
+
status: "rejected",
|
|
822
|
+
reason: "Policy prefix deadline must be a non-negative safe integer",
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
const signal = traversal?.signal;
|
|
826
|
+
if (signal !== undefined && !isAbortSignalV2(signal)) {
|
|
827
|
+
return Promise.resolve({
|
|
828
|
+
status: "rejected",
|
|
829
|
+
reason: "Policy prefix signal must be an AbortSignal",
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
capturedTraversal = { maxSteps, deadline, signal };
|
|
777
833
|
} catch {
|
|
778
834
|
return Promise.resolve({
|
|
779
835
|
status: "rejected",
|
|
@@ -781,10 +837,13 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
781
837
|
});
|
|
782
838
|
}
|
|
783
839
|
const result = this.admissionTail.then(() =>
|
|
784
|
-
this.resolveAcceptedPolicyPrefixOne(
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
840
|
+
this.resolveAcceptedPolicyPrefixOne(
|
|
841
|
+
{
|
|
842
|
+
sequence: capturedSequence,
|
|
843
|
+
digest: capturedDigest,
|
|
844
|
+
},
|
|
845
|
+
capturedTraversal,
|
|
846
|
+
),
|
|
788
847
|
);
|
|
789
848
|
this.admissionTail = result.then(
|
|
790
849
|
(): void => {},
|
|
@@ -808,19 +867,15 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
808
867
|
this.lifecycleController.abort();
|
|
809
868
|
}
|
|
810
869
|
|
|
811
|
-
private async resolveAcceptedPolicyPrefixOne(
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
? "Policy reducer lifecycle is aborted"
|
|
821
|
-
: "Policy reducer is halted by authority equivocation",
|
|
822
|
-
};
|
|
823
|
-
}
|
|
870
|
+
private async resolveAcceptedPolicyPrefixOne(
|
|
871
|
+
reference: {
|
|
872
|
+
sequence: bigint;
|
|
873
|
+
digest: Uint8Array;
|
|
874
|
+
},
|
|
875
|
+
traversal: AcceptedPolicyPrefixTraversalV2,
|
|
876
|
+
): Promise<AcceptedPolicyPrefixResolutionV2> {
|
|
877
|
+
const interrupted = this.acceptedPrefixInterruption(traversal);
|
|
878
|
+
if (interrupted !== undefined) return interrupted;
|
|
824
879
|
if (this.unavailable !== undefined) {
|
|
825
880
|
return {
|
|
826
881
|
status: "unavailable",
|
|
@@ -843,7 +898,10 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
843
898
|
// The admission queue owns the core for this complete walk, so the internal
|
|
844
899
|
// accepted snapshot cannot change until the resolution settles.
|
|
845
900
|
let cursor = this.acceptedHead;
|
|
901
|
+
let steps = 0;
|
|
846
902
|
while (cursor.body.sequence > reference.sequence) {
|
|
903
|
+
const loopInterruption = this.acceptedPrefixInterruption(traversal);
|
|
904
|
+
if (loopInterruption !== undefined) return loopInterruption;
|
|
847
905
|
if (
|
|
848
906
|
cursor.body.sequence === reference.sequence + 1n &&
|
|
849
907
|
!equals(cursor.body.previousPolicyDigest, reference.digest)
|
|
@@ -853,13 +911,16 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
853
911
|
reason: "Policy reference is not an accepted prefix",
|
|
854
912
|
};
|
|
855
913
|
}
|
|
856
|
-
|
|
857
|
-
if (this.lifecycleController.signal.aborted) {
|
|
914
|
+
if (traversal.maxSteps !== undefined && steps >= traversal.maxSteps) {
|
|
858
915
|
return {
|
|
859
|
-
status: "
|
|
860
|
-
reason: "
|
|
916
|
+
status: "unavailable",
|
|
917
|
+
reason: "Accepted policy prefix traversal reached maxSteps",
|
|
861
918
|
};
|
|
862
919
|
}
|
|
920
|
+
steps += 1;
|
|
921
|
+
const parent = await this.parentOf(cursor, undefined, traversal);
|
|
922
|
+
const resolutionInterruption = this.acceptedPrefixInterruption(traversal);
|
|
923
|
+
if (resolutionInterruption !== undefined) return resolutionInterruption;
|
|
863
924
|
if (parent.status !== "found") {
|
|
864
925
|
return {
|
|
865
926
|
status: "unavailable",
|
|
@@ -868,6 +929,8 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
868
929
|
}
|
|
869
930
|
cursor = parent.parent;
|
|
870
931
|
}
|
|
932
|
+
const completedInterruption = this.acceptedPrefixInterruption(traversal);
|
|
933
|
+
if (completedInterruption !== undefined) return completedInterruption;
|
|
871
934
|
|
|
872
935
|
if (!equals(cursor.digest, reference.digest)) {
|
|
873
936
|
return {
|
|
@@ -882,6 +945,38 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
882
945
|
};
|
|
883
946
|
}
|
|
884
947
|
|
|
948
|
+
private acceptedPrefixInterruption(
|
|
949
|
+
traversal: AcceptedPolicyPrefixTraversalV2,
|
|
950
|
+
):
|
|
951
|
+
| Exclude<AcceptedPolicyPrefixResolutionV2, { status: "resolved" }>
|
|
952
|
+
| undefined {
|
|
953
|
+
if (this.lifecycleController.signal.aborted) {
|
|
954
|
+
return {
|
|
955
|
+
status: "halted",
|
|
956
|
+
reason: "Policy reducer lifecycle is aborted",
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
if (this.fork !== undefined) {
|
|
960
|
+
return {
|
|
961
|
+
status: "halted",
|
|
962
|
+
reason: "Policy reducer is halted by authority equivocation",
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (traversal.signal?.aborted) {
|
|
966
|
+
return {
|
|
967
|
+
status: "unavailable",
|
|
968
|
+
reason: "Policy prefix resolution was aborted by the caller",
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
if (traversal.deadline !== undefined && Date.now() >= traversal.deadline) {
|
|
972
|
+
return {
|
|
973
|
+
status: "unavailable",
|
|
974
|
+
reason: "Policy prefix resolution deadline elapsed",
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
return undefined;
|
|
978
|
+
}
|
|
979
|
+
|
|
885
980
|
private fetchHints(): PolicyParentFetchHintV2[] {
|
|
886
981
|
const unique = new Map<string, Uint8Array>();
|
|
887
982
|
for (const { missingParentDigest } of this.pending.values()) {
|
|
@@ -994,69 +1089,111 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
994
1089
|
|
|
995
1090
|
private async resolveExternalSnapshot(
|
|
996
1091
|
digest: Uint8Array,
|
|
1092
|
+
traversal?: AcceptedPolicyPrefixTraversalV2,
|
|
997
1093
|
): Promise<ValidatedPolicySnapshotV2 | undefined> {
|
|
998
1094
|
if (this.lifecycleController.signal.aborted) {
|
|
999
1095
|
throw new PolicyDependencyUnavailableErrorV2(
|
|
1000
1096
|
"Policy resolver lifecycle is aborted",
|
|
1001
1097
|
);
|
|
1002
1098
|
}
|
|
1099
|
+
if (traversal?.signal?.aborted) {
|
|
1100
|
+
throw new PolicyDependencyUnavailableErrorV2(
|
|
1101
|
+
"Policy resolver attempt was aborted by the caller",
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
const deadlineRemaining =
|
|
1105
|
+
traversal?.deadline === undefined
|
|
1106
|
+
? undefined
|
|
1107
|
+
: traversal.deadline - Date.now();
|
|
1108
|
+
const deadlineLimitsAttempt =
|
|
1109
|
+
deadlineRemaining !== undefined &&
|
|
1110
|
+
deadlineRemaining <= this.resolveTimeoutMs;
|
|
1111
|
+
const remaining =
|
|
1112
|
+
deadlineRemaining === undefined
|
|
1113
|
+
? this.resolveTimeoutMs
|
|
1114
|
+
: Math.min(this.resolveTimeoutMs, deadlineRemaining);
|
|
1115
|
+
if (remaining <= 0) {
|
|
1116
|
+
throw new PolicyDependencyUnavailableErrorV2(
|
|
1117
|
+
"Policy resolver deadline elapsed",
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1003
1120
|
|
|
1004
1121
|
const attemptController = new AbortController();
|
|
1005
1122
|
let timedOut = false;
|
|
1123
|
+
let callerAborted = false;
|
|
1006
1124
|
const abortFromLifecycle = (): void => attemptController.abort();
|
|
1125
|
+
const abortFromCaller = (): void => {
|
|
1126
|
+
callerAborted = true;
|
|
1127
|
+
attemptController.abort();
|
|
1128
|
+
};
|
|
1007
1129
|
this.lifecycleController.signal.addEventListener(
|
|
1008
1130
|
"abort",
|
|
1009
1131
|
abortFromLifecycle,
|
|
1010
1132
|
{ once: true },
|
|
1011
1133
|
);
|
|
1134
|
+
traversal?.signal?.addEventListener("abort", abortFromCaller, {
|
|
1135
|
+
once: true,
|
|
1136
|
+
});
|
|
1012
1137
|
const timeout = setTimeout(() => {
|
|
1013
1138
|
timedOut = true;
|
|
1014
1139
|
attemptController.abort();
|
|
1015
|
-
},
|
|
1140
|
+
}, remaining);
|
|
1016
1141
|
const abortError = (): PolicyDependencyUnavailableErrorV2 =>
|
|
1017
1142
|
new PolicyDependencyUnavailableErrorV2(
|
|
1018
|
-
|
|
1019
|
-
?
|
|
1020
|
-
:
|
|
1143
|
+
callerAborted
|
|
1144
|
+
? "Policy resolver attempt was aborted by the caller"
|
|
1145
|
+
: timedOut && deadlineLimitsAttempt
|
|
1146
|
+
? "Policy resolver deadline elapsed"
|
|
1147
|
+
: timedOut
|
|
1148
|
+
? `Policy resolver timed out after ${this.resolveTimeoutMs} ms`
|
|
1149
|
+
: "Policy resolver attempt was aborted",
|
|
1021
1150
|
);
|
|
1022
1151
|
|
|
1023
1152
|
let rejectOnAbort: (() => void) | undefined;
|
|
1024
|
-
const abortPromise = new Promise<never>((_resolve, reject) => {
|
|
1025
|
-
rejectOnAbort = (): void => {
|
|
1026
|
-
reject(abortError());
|
|
1027
|
-
};
|
|
1028
|
-
attemptController.signal.addEventListener("abort", rejectOnAbort, {
|
|
1029
|
-
once: true,
|
|
1030
|
-
});
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
const resolution = Promise.resolve().then(async () => {
|
|
1034
|
-
if (attemptController.signal.aborted) throw abortError();
|
|
1035
|
-
const entryBytes = await this.resolvePolicyEntry(copyBytes(digest), {
|
|
1036
|
-
signal: attemptController.signal,
|
|
1037
|
-
});
|
|
1038
|
-
// A resolver may ignore cancellation. Do not spend decode or signature
|
|
1039
|
-
// verification work on bytes that arrive after this attempt expired.
|
|
1040
|
-
if (attemptController.signal.aborted) throw abortError();
|
|
1041
|
-
if (entryBytes === undefined) return undefined;
|
|
1042
|
-
const snapshot = await authenticatePolicySnapshotEntryV2(
|
|
1043
|
-
entryBytes,
|
|
1044
|
-
this.descriptor,
|
|
1045
|
-
);
|
|
1046
|
-
if (!equals(snapshot.digest, digest)) {
|
|
1047
|
-
throw new Error("Policy resolver returned the wrong body digest");
|
|
1048
|
-
}
|
|
1049
|
-
return snapshot;
|
|
1050
|
-
});
|
|
1051
|
-
// Promise.race installs handlers, but this explicit observer documents and
|
|
1052
|
-
// preserves consumption if the resolver settles after its deadline.
|
|
1053
|
-
void resolution.then(
|
|
1054
|
-
(): void => undefined,
|
|
1055
|
-
(): void => undefined,
|
|
1056
|
-
);
|
|
1057
1153
|
|
|
1058
1154
|
try {
|
|
1059
|
-
return await Promise
|
|
1155
|
+
return await new Promise<ValidatedPolicySnapshotV2 | undefined>(
|
|
1156
|
+
(resolve, reject) => {
|
|
1157
|
+
let settled = false;
|
|
1158
|
+
const settle = (complete: () => void): void => {
|
|
1159
|
+
if (settled) return;
|
|
1160
|
+
settled = true;
|
|
1161
|
+
complete();
|
|
1162
|
+
};
|
|
1163
|
+
rejectOnAbort = (): void => settle(() => reject(abortError()));
|
|
1164
|
+
attemptController.signal.addEventListener("abort", rejectOnAbort, {
|
|
1165
|
+
once: true,
|
|
1166
|
+
});
|
|
1167
|
+
void Promise.resolve()
|
|
1168
|
+
.then(async () => {
|
|
1169
|
+
if (attemptController.signal.aborted) throw abortError();
|
|
1170
|
+
const entryBytes = await this.resolvePolicyEntry(
|
|
1171
|
+
copyBytes(digest),
|
|
1172
|
+
{
|
|
1173
|
+
signal: attemptController.signal,
|
|
1174
|
+
},
|
|
1175
|
+
);
|
|
1176
|
+
// A resolver may ignore cancellation. Do not spend decode or signature
|
|
1177
|
+
// verification work on bytes that arrive after this attempt expired.
|
|
1178
|
+
if (attemptController.signal.aborted) throw abortError();
|
|
1179
|
+
if (entryBytes === undefined) return undefined;
|
|
1180
|
+
const snapshot = await authenticatePolicySnapshotEntryV2(
|
|
1181
|
+
entryBytes,
|
|
1182
|
+
this.descriptor,
|
|
1183
|
+
);
|
|
1184
|
+
if (!equals(snapshot.digest, digest)) {
|
|
1185
|
+
throw new Error(
|
|
1186
|
+
"Policy resolver returned the wrong body digest",
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
return snapshot;
|
|
1190
|
+
})
|
|
1191
|
+
.then(
|
|
1192
|
+
(value) => settle(() => resolve(value)),
|
|
1193
|
+
(error: unknown) => settle(() => reject(error)),
|
|
1194
|
+
);
|
|
1195
|
+
},
|
|
1196
|
+
);
|
|
1060
1197
|
} catch (error) {
|
|
1061
1198
|
if (error instanceof PolicyDependencyUnavailableErrorV2) throw error;
|
|
1062
1199
|
throw new PolicyDependencyUnavailableErrorV2(
|
|
@@ -1068,6 +1205,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1068
1205
|
"abort",
|
|
1069
1206
|
abortFromLifecycle,
|
|
1070
1207
|
);
|
|
1208
|
+
traversal?.signal?.removeEventListener("abort", abortFromCaller);
|
|
1071
1209
|
if (rejectOnAbort !== undefined) {
|
|
1072
1210
|
attemptController.signal.removeEventListener("abort", rejectOnAbort);
|
|
1073
1211
|
}
|
|
@@ -1077,6 +1215,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1077
1215
|
private async resolveSnapshot(
|
|
1078
1216
|
digest: Uint8Array,
|
|
1079
1217
|
cache?: SnapshotResolutionCacheV2,
|
|
1218
|
+
traversal?: AcceptedPolicyPrefixTraversalV2,
|
|
1080
1219
|
): Promise<ValidatedPolicySnapshotV2 | undefined> {
|
|
1081
1220
|
const digestKey = bytesKey(digest);
|
|
1082
1221
|
if (this.acceptedHead?.digestKey === digestKey) {
|
|
@@ -1086,7 +1225,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1086
1225
|
if (pending !== undefined) return copySnapshot(pending.snapshot);
|
|
1087
1226
|
let resolution = cache?.get(digestKey);
|
|
1088
1227
|
if (resolution === undefined) {
|
|
1089
|
-
resolution = this.resolveExternalSnapshot(digest);
|
|
1228
|
+
resolution = this.resolveExternalSnapshot(digest, traversal);
|
|
1090
1229
|
cache?.set(digestKey, resolution);
|
|
1091
1230
|
}
|
|
1092
1231
|
const snapshot = await resolution;
|
|
@@ -1096,6 +1235,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1096
1235
|
private async parentOf(
|
|
1097
1236
|
child: ValidatedPolicySnapshotV2,
|
|
1098
1237
|
cache?: SnapshotResolutionCacheV2,
|
|
1238
|
+
traversal?: AcceptedPolicyPrefixTraversalV2,
|
|
1099
1239
|
): Promise<ParentResolutionV2> {
|
|
1100
1240
|
if (child.body.sequence === 0n) {
|
|
1101
1241
|
return {
|
|
@@ -1109,6 +1249,7 @@ export class TrustedNetworkV2PolicyReducer {
|
|
|
1109
1249
|
parent = await this.resolveSnapshot(
|
|
1110
1250
|
child.body.previousPolicyDigest,
|
|
1111
1251
|
cache,
|
|
1252
|
+
traversal,
|
|
1112
1253
|
);
|
|
1113
1254
|
} catch (error) {
|
|
1114
1255
|
return {
|