@sekiban/dcb-runtime 0.2.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/LICENSE +86 -0
- package/README.md +13 -0
- package/dist/allocator/AllocatorDurableObject.d.ts +36 -0
- package/dist/allocator/AllocatorDurableObject.js +658 -0
- package/dist/allocator/IssuanceLedger.d.ts +188 -0
- package/dist/allocator/IssuanceLedger.js +608 -0
- package/dist/allocator/IssuanceReconciler.d.ts +41 -0
- package/dist/allocator/IssuanceReconciler.js +191 -0
- package/dist/allocator/OrderClock.d.ts +41 -0
- package/dist/allocator/OrderClock.js +92 -0
- package/dist/allocator/SortableUniqueId.d.ts +46 -0
- package/dist/allocator/SortableUniqueId.js +156 -0
- package/dist/allocator/types.d.ts +52 -0
- package/dist/allocator/types.js +1 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +28 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +357 -0
- package/dist/bootstrap/BootstrapStoreAdapter.d.ts +51 -0
- package/dist/bootstrap/BootstrapStoreAdapter.js +162 -0
- package/dist/bootstrap/OperatorBootstrap.d.ts +20 -0
- package/dist/bootstrap/OperatorBootstrap.js +104 -0
- package/dist/bootstrap/manifest.d.ts +9 -0
- package/dist/bootstrap/manifest.js +115 -0
- package/dist/bootstrap/types.d.ts +75 -0
- package/dist/bootstrap/types.js +1 -0
- package/dist/cli/OperatorRepairCli.d.ts +11 -0
- package/dist/cli/OperatorRepairCli.js +108 -0
- package/dist/cloudflare.d.ts +164 -0
- package/dist/cloudflare.js +19487 -0
- package/dist/commit/CommitWorker.d.ts +101 -0
- package/dist/commit/CommitWorker.js +921 -0
- package/dist/commit/types.d.ts +25 -0
- package/dist/commit/types.js +13 -0
- package/dist/completeness/GlobalCompletenessReconciler.d.ts +77 -0
- package/dist/completeness/GlobalCompletenessReconciler.js +572 -0
- package/dist/completeness/types.d.ts +85 -0
- package/dist/completeness/types.js +6 -0
- package/dist/composition.d.ts +123 -0
- package/dist/composition.js +237 -0
- package/dist/cosmos.d.ts +11 -0
- package/dist/cosmos.js +1411 -0
- package/dist/d1-mv.d.ts +8 -0
- package/dist/d1-mv.js +2001 -0
- package/dist/d1.d.ts +16 -0
- package/dist/d1.js +3418 -0
- package/dist/diagnostics/G60DurableHop.d.ts +99 -0
- package/dist/diagnostics/G60DurableHop.js +248 -0
- package/dist/diagnostics/G65Admission.d.ts +23 -0
- package/dist/diagnostics/G65Admission.js +43 -0
- package/dist/diagnostics/G65DirectRing.d.ts +33 -0
- package/dist/diagnostics/G65DirectRing.js +85 -0
- package/dist/diagnostics/G69AdmissionAttempt.d.ts +43 -0
- package/dist/diagnostics/G69AdmissionAttempt.js +22 -0
- package/dist/downstream/DeliveryCore.d.ts +127 -0
- package/dist/downstream/DeliveryCore.js +304 -0
- package/dist/downstream/Doorbell.d.ts +43 -0
- package/dist/downstream/Doorbell.js +137 -0
- package/dist/downstream/DownstreamAdapter.d.ts +52 -0
- package/dist/downstream/DownstreamAdapter.js +299 -0
- package/dist/downstream/ExclusionLookup.d.ts +36 -0
- package/dist/downstream/ExclusionLookup.js +50 -0
- package/dist/downstream/InconsistencyDetector.d.ts +27 -0
- package/dist/downstream/InconsistencyDetector.js +82 -0
- package/dist/downstream/OutboxDrain.d.ts +35 -0
- package/dist/downstream/OutboxDrain.js +126 -0
- package/dist/downstream/types.d.ts +77 -0
- package/dist/downstream/types.js +64 -0
- package/dist/eventIdentity.d.ts +30 -0
- package/dist/eventIdentity.js +49 -0
- package/dist/eventRecord.d.ts +26 -0
- package/dist/eventRecord.js +53 -0
- package/dist/g42-probe.d.ts +9 -0
- package/dist/g42-probe.js +9 -0
- package/dist/http/SerializedQueryWorker.d.ts +58 -0
- package/dist/http/SerializedQueryWorker.js +497 -0
- package/dist/http/commitResponse.d.ts +46 -0
- package/dist/http/commitResponse.js +80 -0
- package/dist/http/testServiceId.d.ts +17 -0
- package/dist/http/testServiceId.js +14 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +19926 -0
- package/dist/journal/JournalDurableObject.d.ts +30 -0
- package/dist/journal/JournalDurableObject.js +481 -0
- package/dist/journal/JournalFirstTouchProbe.d.ts +190 -0
- package/dist/journal/JournalFirstTouchProbe.js +375 -0
- package/dist/journal/types.d.ts +116 -0
- package/dist/journal/types.js +52 -0
- package/dist/mv/MaterializedViewCatchUp.d.ts +60 -0
- package/dist/mv/MaterializedViewCatchUp.js +353 -0
- package/dist/mv/MaterializedViewRuntime.d.ts +80 -0
- package/dist/mv/MaterializedViewRuntime.js +328 -0
- package/dist/mv/MaterializedViewStore.d.ts +257 -0
- package/dist/mv/MaterializedViewStore.js +875 -0
- package/dist/mv/UnsafeWindowMaterializedView.d.ts +112 -0
- package/dist/mv/UnsafeWindowMaterializedView.js +495 -0
- package/dist/mv/index.d.ts +8 -0
- package/dist/mv/index.js +2357 -0
- package/dist/projection/LiveProjectionWorker.d.ts +86 -0
- package/dist/projection/LiveProjectionWorker.js +240 -0
- package/dist/projection/ProjectionRuntime.d.ts +113 -0
- package/dist/projection/ProjectionRuntime.js +346 -0
- package/dist/projection/ProjectorRegistry.d.ts +51 -0
- package/dist/projection/ProjectorRegistry.js +116 -0
- package/dist/query/ProjectionQueryStore.d.ts +102 -0
- package/dist/query/ProjectionQueryStore.js +185 -0
- package/dist/query/QueryRegistry.d.ts +22 -0
- package/dist/query/QueryRegistry.js +36 -0
- package/dist/read/SerializedReadWorker.d.ts +36 -0
- package/dist/read/SerializedReadWorker.js +225 -0
- package/dist/repair/RepairWorker.d.ts +72 -0
- package/dist/repair/RepairWorker.js +461 -0
- package/dist/safeWindow.d.ts +20 -0
- package/dist/safeWindow.js +36 -0
- package/dist/scope/ControlRouteScope.d.ts +22 -0
- package/dist/scope/ControlRouteScope.js +36 -0
- package/dist/scope/ScopeName.d.ts +34 -0
- package/dist/scope/ScopeName.js +67 -0
- package/dist/service/ServiceIdentityProvider.d.ts +35 -0
- package/dist/service/ServiceIdentityProvider.js +56 -0
- package/dist/store/CosmosEventStore.d.ts +126 -0
- package/dist/store/CosmosEventStore.js +950 -0
- package/dist/store/D1EventStore.d.ts +111 -0
- package/dist/store/D1EventStore.js +1111 -0
- package/dist/store/PostgresEventStore.d.ts +35 -0
- package/dist/store/PostgresEventStore.js +811 -0
- package/dist/store/provider.d.ts +25 -0
- package/dist/store/provider.js +32 -0
- package/dist/store/types.d.ts +205 -0
- package/dist/store/types.js +18 -0
- package/dist/tag/EventDigest.d.ts +40 -0
- package/dist/tag/EventDigest.js +158 -0
- package/dist/tag/TagDurableObject.d.ts +286 -0
- package/dist/tag/TagDurableObject.js +4071 -0
- package/dist/tag/TagSqlMeasurement.d.ts +26 -0
- package/dist/tag/TagSqlMeasurement.js +44 -0
- package/dist/tag/TagSqlSchema.d.ts +29 -0
- package/dist/tag/TagSqlSchema.js +211 -0
- package/dist/tag/types.d.ts +173 -0
- package/dist/tag/types.js +3 -0
- package/dist/tagstate/TagStateDurableObject.d.ts +57 -0
- package/dist/tagstate/TagStateDurableObject.js +499 -0
- package/dist/trace/AttributionRatio.d.ts +12 -0
- package/dist/trace/AttributionRatio.js +68 -0
- package/dist/trace/CloudflareTracing.d.ts +7 -0
- package/dist/trace/CloudflareTracing.js +9 -0
- package/dist/trace/CommitTrace.d.ts +405 -0
- package/dist/trace/CommitTrace.js +1008 -0
- package/dist/trace/CommitTraceConsoleSink.d.ts +68 -0
- package/dist/trace/CommitTraceConsoleSink.js +114 -0
- package/dist/trace/CommitTraceVerifier.d.ts +22 -0
- package/dist/trace/CommitTraceVerifier.js +295 -0
- package/dist/trace/ObservationStream.d.ts +118 -0
- package/dist/trace/ObservationStream.js +148 -0
- package/package.json +71 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { assertSortableUniqueId } from "./SortableUniqueId";
|
|
2
|
+
import { RESERVATION_WINDOW_MS } from "../tag/types";
|
|
3
|
+
/**
|
|
4
|
+
* Minimum age before reconciler may force-tombstone a never-contacted target.
|
|
5
|
+
* Matches {@link RESERVATION_WINDOW_MS}: the writer has until tag reservation
|
|
6
|
+
* expiry to acquire; elapsed time is only a trigger to inspect and fence.
|
|
7
|
+
*/
|
|
8
|
+
export const ISSUANCE_NEVER_CONTACTED_GRACE_MS = RESERVATION_WINDOW_MS;
|
|
9
|
+
export const ISSUANCE_LEDGER_VERSION = 1;
|
|
10
|
+
export const ATTEMPT_KEY_PREFIX = "attempt:";
|
|
11
|
+
export const ISSUANCE_COUNT_KEY = "issuance:unresolved-count";
|
|
12
|
+
export const ISSUANCE_RECOVERY_KEY = "issuance:recovery-schedule";
|
|
13
|
+
export const ISSUANCE_MIGRATION_KEY = "issuance:migration";
|
|
14
|
+
export const ISSUANCE_MIGRATION_PROOF_PREFIX = "issuance:migration-proof:";
|
|
15
|
+
export const ISSUANCE_PREFIX_KEY = "issuance:closed-prefix";
|
|
16
|
+
export const LEGACY_INVENTORIED_PREFIX = "issuance:legacy-inventoried:";
|
|
17
|
+
export const ENVELOPE_PREFIX = "issuance:envelope:";
|
|
18
|
+
export const TARGET_PREFIX = "issuance:target:";
|
|
19
|
+
export const ISSUED_INDEX_PREFIX = "issuance:issued:";
|
|
20
|
+
export const UNRESOLVED_INDEX_PREFIX = "issuance:unresolved:";
|
|
21
|
+
export const DEFAULT_WRITER_EPOCH = 0;
|
|
22
|
+
export const MAX_TARGET_TAGS = 32;
|
|
23
|
+
export const MAX_TAG_LENGTH = 256;
|
|
24
|
+
export class IssuanceLedgerError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "IssuanceLedgerError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isNonEmptyString(value) {
|
|
31
|
+
return typeof value === "string" && value.length > 0;
|
|
32
|
+
}
|
|
33
|
+
function isNonNegativeInteger(value) {
|
|
34
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
35
|
+
}
|
|
36
|
+
export function canonicalizeTargetTags(tags) {
|
|
37
|
+
const unique = [...new Set(tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0))];
|
|
38
|
+
if (unique.length === 0)
|
|
39
|
+
throw new IssuanceLedgerError("at least one target tag is required");
|
|
40
|
+
if (unique.length > MAX_TARGET_TAGS)
|
|
41
|
+
throw new IssuanceLedgerError("target tag count exceeds bound");
|
|
42
|
+
for (const tag of unique) {
|
|
43
|
+
if (tag.length > MAX_TAG_LENGTH)
|
|
44
|
+
throw new IssuanceLedgerError("target tag exceeds length bound");
|
|
45
|
+
}
|
|
46
|
+
return unique.sort((left, right) => left.localeCompare(right));
|
|
47
|
+
}
|
|
48
|
+
export function identityDigest(input) {
|
|
49
|
+
const payload = JSON.stringify(input);
|
|
50
|
+
let hash = 0;
|
|
51
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
52
|
+
hash = ((hash << 5) - hash + payload.charCodeAt(index)) | 0;
|
|
53
|
+
}
|
|
54
|
+
return `g77:${Math.abs(hash).toString(16)}:${payload.length}`;
|
|
55
|
+
}
|
|
56
|
+
export function envelopeKey(attemptId, candidateIndex) {
|
|
57
|
+
return `${ENVELOPE_PREFIX}${attemptId}:${candidateIndex}`;
|
|
58
|
+
}
|
|
59
|
+
export function targetKey(attemptId, candidateIndex, tag) {
|
|
60
|
+
return `${TARGET_PREFIX}${attemptId}:${candidateIndex}:${tag}`;
|
|
61
|
+
}
|
|
62
|
+
export function issuedIndexKey(suid, attemptId, candidateIndex) {
|
|
63
|
+
assertSortableUniqueId(suid);
|
|
64
|
+
return `${ISSUED_INDEX_PREFIX}${suid}:${attemptId}:${candidateIndex}`;
|
|
65
|
+
}
|
|
66
|
+
export function unresolvedIndexKey(suid, attemptId, candidateIndex) {
|
|
67
|
+
assertSortableUniqueId(suid);
|
|
68
|
+
return `${UNRESOLVED_INDEX_PREFIX}${suid}:${attemptId}:${candidateIndex}`;
|
|
69
|
+
}
|
|
70
|
+
export function parseIndexEntry(key, prefix) {
|
|
71
|
+
if (!key.startsWith(prefix))
|
|
72
|
+
return undefined;
|
|
73
|
+
const rest = key.slice(prefix.length);
|
|
74
|
+
const suidEnd = rest.indexOf(":");
|
|
75
|
+
if (suidEnd <= 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
const suid = rest.slice(0, suidEnd);
|
|
78
|
+
const tail = rest.slice(suidEnd + 1);
|
|
79
|
+
const lastColon = tail.lastIndexOf(":");
|
|
80
|
+
if (lastColon <= 0)
|
|
81
|
+
return undefined;
|
|
82
|
+
const attemptId = tail.slice(0, lastColon);
|
|
83
|
+
const candidateIndex = Number(tail.slice(lastColon + 1));
|
|
84
|
+
if (!Number.isInteger(candidateIndex) || candidateIndex < 0)
|
|
85
|
+
return undefined;
|
|
86
|
+
return { suid, attemptId, candidateIndex };
|
|
87
|
+
}
|
|
88
|
+
export function buildEnvelope(input) {
|
|
89
|
+
const canonicalTargetTags = canonicalizeTargetTags(input.targetTags);
|
|
90
|
+
assertSortableUniqueId(input.candidate.suid);
|
|
91
|
+
return {
|
|
92
|
+
ledgerVersion: ISSUANCE_LEDGER_VERSION,
|
|
93
|
+
serviceId: input.serviceId,
|
|
94
|
+
allocatorLineageId: input.allocatorLineageId,
|
|
95
|
+
attemptId: input.attemptId,
|
|
96
|
+
candidateIndex: input.candidate.candidateIndex,
|
|
97
|
+
eventId: input.candidate.eventId,
|
|
98
|
+
suid: input.candidate.suid,
|
|
99
|
+
canonicalTargetTags,
|
|
100
|
+
pinnedWriterEpoch: input.pinnedWriterEpoch,
|
|
101
|
+
identityDigest: identityDigest({
|
|
102
|
+
serviceId: input.serviceId,
|
|
103
|
+
allocatorLineageId: input.allocatorLineageId,
|
|
104
|
+
attemptId: input.attemptId,
|
|
105
|
+
candidateIndex: input.candidate.candidateIndex,
|
|
106
|
+
eventId: input.candidate.eventId,
|
|
107
|
+
suid: input.candidate.suid,
|
|
108
|
+
canonicalTargetTags,
|
|
109
|
+
pinnedWriterEpoch: input.pinnedWriterEpoch,
|
|
110
|
+
}),
|
|
111
|
+
issuedAtMs: input.issuedAtMs,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function membershipMatches(left, right) {
|
|
115
|
+
if (left.length !== right.length)
|
|
116
|
+
return false;
|
|
117
|
+
const canonicalLeft = canonicalizeTargetTags(left);
|
|
118
|
+
const canonicalRight = canonicalizeTargetTags(right);
|
|
119
|
+
return canonicalLeft.every((tag, index) => tag === canonicalRight[index]);
|
|
120
|
+
}
|
|
121
|
+
export async function registerIssuanceInTransaction(txn, input) {
|
|
122
|
+
if (input.migration !== undefined && input.candidateMembership.size === 0) {
|
|
123
|
+
throw new IssuanceLedgerError("post-cut allocation requires canonical target membership");
|
|
124
|
+
}
|
|
125
|
+
let count = await txn.get(ISSUANCE_COUNT_KEY) ?? 0;
|
|
126
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
127
|
+
throw new IssuanceLedgerError("corrupt unresolved issuance count");
|
|
128
|
+
}
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
for (const candidate of input.candidates) {
|
|
131
|
+
const membership = input.candidateMembership.get(candidate.candidateIndex);
|
|
132
|
+
if (input.migration !== undefined && membership === undefined) {
|
|
133
|
+
throw new IssuanceLedgerError("post-cut allocation requires membership for every candidate");
|
|
134
|
+
}
|
|
135
|
+
if (membership === undefined)
|
|
136
|
+
continue;
|
|
137
|
+
const envelope = buildEnvelope({
|
|
138
|
+
serviceId: input.serviceId,
|
|
139
|
+
allocatorLineageId: input.allocatorLineageId,
|
|
140
|
+
attemptId: input.attemptId,
|
|
141
|
+
candidate,
|
|
142
|
+
targetTags: membership.tags,
|
|
143
|
+
pinnedWriterEpoch: membership.pinnedWriterEpoch,
|
|
144
|
+
issuedAtMs: now,
|
|
145
|
+
});
|
|
146
|
+
const existing = await txn.get(envelopeKey(input.attemptId, candidate.candidateIndex));
|
|
147
|
+
if (existing !== undefined) {
|
|
148
|
+
if (!membershipMatches(existing.canonicalTargetTags, envelope.canonicalTargetTags)) {
|
|
149
|
+
throw new IssuanceLedgerError("divergent membership on allocation replay");
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
await txn.put(envelopeKey(input.attemptId, candidate.candidateIndex), envelope);
|
|
154
|
+
for (const tag of envelope.canonicalTargetTags) {
|
|
155
|
+
await txn.put(targetKey(input.attemptId, candidate.candidateIndex, tag), {
|
|
156
|
+
tag,
|
|
157
|
+
status: "pending",
|
|
158
|
+
envelopeDigest: envelope.identityDigest,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
await txn.put(issuedIndexKey(candidate.suid, input.attemptId, candidate.candidateIndex), {
|
|
162
|
+
attemptId: input.attemptId,
|
|
163
|
+
candidateIndex: candidate.candidateIndex,
|
|
164
|
+
suid: candidate.suid,
|
|
165
|
+
});
|
|
166
|
+
await txn.put(unresolvedIndexKey(candidate.suid, input.attemptId, candidate.candidateIndex), {
|
|
167
|
+
attemptId: input.attemptId,
|
|
168
|
+
candidateIndex: candidate.candidateIndex,
|
|
169
|
+
suid: candidate.suid,
|
|
170
|
+
});
|
|
171
|
+
count += 1;
|
|
172
|
+
}
|
|
173
|
+
await txn.put(ISSUANCE_COUNT_KEY, count);
|
|
174
|
+
const prefix = await computeClosedPrefixSuid(txn, txn, await readWatermarkFromState(txn), input.migration);
|
|
175
|
+
await txn.put(ISSUANCE_PREFIX_KEY, prefix.closedPrefixSuid);
|
|
176
|
+
const graceDueAt = now + ISSUANCE_NEVER_CONTACTED_GRACE_MS;
|
|
177
|
+
const schedule = await txn.get(ISSUANCE_RECOVERY_KEY);
|
|
178
|
+
if (schedule === undefined || schedule.nextDueAt > graceDueAt) {
|
|
179
|
+
await txn.put(ISSUANCE_RECOVERY_KEY, {
|
|
180
|
+
nextDueAt: graceDueAt,
|
|
181
|
+
cursor: schedule?.cursor ?? null,
|
|
182
|
+
attempts: schedule?.attempts ?? 0,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Arm when a recovery schedule exists and reconciliation is due (or overdue). */
|
|
187
|
+
export function shouldArmIssuanceRecovery(schedule, now) {
|
|
188
|
+
return schedule !== undefined && schedule.nextDueAt <= now;
|
|
189
|
+
}
|
|
190
|
+
export const ISSUANCE_RECOVERY_ALARM_ARMED_KEY = "issuance:recovery-alarm-armed-at";
|
|
191
|
+
/**
|
|
192
|
+
* Align the durable recovery alarm with {@link ISSUANCE_RECOVERY_KEY}.nextDueAt without
|
|
193
|
+
* pushing a pending earlier alarm later while work remains outstanding.
|
|
194
|
+
*/
|
|
195
|
+
export async function syncIssuanceRecoveryAlarm(storage, now) {
|
|
196
|
+
if (await countUnresolvedEntries(storage) === 0) {
|
|
197
|
+
await storage.deleteAlarm();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const schedule = await storage.get(ISSUANCE_RECOVERY_KEY);
|
|
201
|
+
if (schedule === undefined)
|
|
202
|
+
return;
|
|
203
|
+
const dueAt = schedule.nextDueAt;
|
|
204
|
+
const currentAlarm = await storage.getAlarm();
|
|
205
|
+
if (currentAlarm !== null && currentAlarm <= dueAt)
|
|
206
|
+
return;
|
|
207
|
+
if (currentAlarm === null
|
|
208
|
+
|| dueAt < currentAlarm
|
|
209
|
+
|| shouldArmIssuanceRecovery(schedule, now)) {
|
|
210
|
+
await storage.put(ISSUANCE_RECOVERY_ALARM_ARMED_KEY, dueAt);
|
|
211
|
+
await storage.setAlarm(dueAt);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export async function countUnresolvedEntries(reader) {
|
|
215
|
+
const listed = await reader.list({ prefix: UNRESOLVED_INDEX_PREFIX, limit: 256 });
|
|
216
|
+
return [...listed.keys()].length;
|
|
217
|
+
}
|
|
218
|
+
export async function leastUnresolvedEntry(reader) {
|
|
219
|
+
const listed = await reader.list({
|
|
220
|
+
prefix: UNRESOLVED_INDEX_PREFIX,
|
|
221
|
+
limit: 1,
|
|
222
|
+
});
|
|
223
|
+
for (const [key] of listed) {
|
|
224
|
+
const parsed = parseIndexEntry(key, UNRESOLVED_INDEX_PREFIX);
|
|
225
|
+
if (parsed !== undefined)
|
|
226
|
+
return parsed;
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
export async function predecessorIssuedSuid(reader, exclusiveSuid) {
|
|
231
|
+
assertSortableUniqueId(exclusiveSuid);
|
|
232
|
+
const endKey = `${ISSUED_INDEX_PREFIX}${exclusiveSuid}:`;
|
|
233
|
+
const listed = await reader.list({
|
|
234
|
+
prefix: ISSUED_INDEX_PREFIX,
|
|
235
|
+
end: endKey,
|
|
236
|
+
reverse: true,
|
|
237
|
+
limit: 1,
|
|
238
|
+
});
|
|
239
|
+
for (const [key, value] of listed) {
|
|
240
|
+
const suid = value?.suid ?? parseIndexEntry(key, ISSUED_INDEX_PREFIX)?.suid;
|
|
241
|
+
if (suid !== undefined && suid < exclusiveSuid)
|
|
242
|
+
return suid;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
export async function computeClosedPrefixSuid(reader, txn, allocatedWatermark, migration) {
|
|
247
|
+
const storedCount = await txn.get(ISSUANCE_COUNT_KEY);
|
|
248
|
+
const indexedCount = await countUnresolvedEntries(reader);
|
|
249
|
+
const count = Math.max(storedCount ?? 0, indexedCount);
|
|
250
|
+
if (!Number.isSafeInteger(storedCount ?? 0) || (storedCount ?? 0) < 0) {
|
|
251
|
+
throw new IssuanceLedgerError("missing or corrupt unresolved count");
|
|
252
|
+
}
|
|
253
|
+
if (migration !== undefined && (!migration.inventoryComplete || legacyMigrationBlocks(migration))) {
|
|
254
|
+
return { closedPrefixSuid: null, unresolvedCount: count, status: "unreconciled" };
|
|
255
|
+
}
|
|
256
|
+
if (migration !== undefined && migration.migrationProofId === null) {
|
|
257
|
+
return { closedPrefixSuid: null, unresolvedCount: count, status: "unreconciled" };
|
|
258
|
+
}
|
|
259
|
+
if (count > 0) {
|
|
260
|
+
const least = await leastUnresolvedEntry(reader);
|
|
261
|
+
if (least === undefined) {
|
|
262
|
+
throw new IssuanceLedgerError("corrupt unresolved index");
|
|
263
|
+
}
|
|
264
|
+
const predecessor = await predecessorIssuedSuid(reader, least.suid);
|
|
265
|
+
// AC3: the least unresolved hole is an exclusive boundary — never the hole or raw watermark.
|
|
266
|
+
let closedPrefixSuid = predecessor;
|
|
267
|
+
if (closedPrefixSuid === least.suid ||
|
|
268
|
+
(allocatedWatermark !== null &&
|
|
269
|
+
least.suid === allocatedWatermark &&
|
|
270
|
+
closedPrefixSuid === allocatedWatermark)) {
|
|
271
|
+
closedPrefixSuid = null;
|
|
272
|
+
}
|
|
273
|
+
return { closedPrefixSuid, unresolvedCount: count, status: "ready" };
|
|
274
|
+
}
|
|
275
|
+
if (allocatedWatermark === null) {
|
|
276
|
+
return { closedPrefixSuid: null, unresolvedCount: 0, status: "ready" };
|
|
277
|
+
}
|
|
278
|
+
return { closedPrefixSuid: allocatedWatermark, unresolvedCount: 0, status: "ready" };
|
|
279
|
+
}
|
|
280
|
+
export async function buildCertificateSnapshot(reader, txn, input) {
|
|
281
|
+
const migration = await txn.get(ISSUANCE_MIGRATION_KEY);
|
|
282
|
+
const prefix = await computeClosedPrefixSuid(reader, txn, input.allocatedWatermark, migration);
|
|
283
|
+
return {
|
|
284
|
+
certificateVersion: 1,
|
|
285
|
+
authority: "allocator-transaction",
|
|
286
|
+
status: prefix.status,
|
|
287
|
+
allocatorLineageId: input.allocatorLineageId,
|
|
288
|
+
serviceId: input.serviceId,
|
|
289
|
+
closedPrefixSuid: prefix.closedPrefixSuid,
|
|
290
|
+
unresolvedCount: prefix.unresolvedCount,
|
|
291
|
+
generatedAt: input.generatedAt,
|
|
292
|
+
migrationProofId: migration?.migrationProofId ?? null,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function terminalStatusFromTagObservation(observation) {
|
|
296
|
+
if (observation.terminalStatus === "installed-and-covered") {
|
|
297
|
+
if (!isNonEmptyString(observation.obligationDigest)) {
|
|
298
|
+
throw new IssuanceLedgerError("installed resolution requires tag obligation digest");
|
|
299
|
+
}
|
|
300
|
+
return "installed-and-covered";
|
|
301
|
+
}
|
|
302
|
+
if (observation.terminalStatus === "absent-and-irrevocably-fenced") {
|
|
303
|
+
if (!isNonNegativeInteger(observation.tombstoneEpoch)) {
|
|
304
|
+
throw new IssuanceLedgerError("fenced resolution requires tag tombstone epoch");
|
|
305
|
+
}
|
|
306
|
+
return "absent-and-irrevocably-fenced";
|
|
307
|
+
}
|
|
308
|
+
throw new IssuanceLedgerError("tag observation terminal status is invalid");
|
|
309
|
+
}
|
|
310
|
+
export function parseTargetResolutionEvidence(raw) {
|
|
311
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
312
|
+
throw new IssuanceLedgerError("resolution evidence body is invalid");
|
|
313
|
+
}
|
|
314
|
+
const value = raw;
|
|
315
|
+
const observationRaw = value.tagObservation;
|
|
316
|
+
if (typeof observationRaw !== "object" || observationRaw === null || Array.isArray(observationRaw)) {
|
|
317
|
+
throw new IssuanceLedgerError("tag observation artefact is required");
|
|
318
|
+
}
|
|
319
|
+
const observationValue = observationRaw;
|
|
320
|
+
const terminalStatus = observationValue.terminalStatus;
|
|
321
|
+
if (terminalStatus !== "installed-and-covered" &&
|
|
322
|
+
terminalStatus !== "absent-and-irrevocably-fenced") {
|
|
323
|
+
throw new IssuanceLedgerError("tag observation terminal status is invalid");
|
|
324
|
+
}
|
|
325
|
+
const tagObservation = {
|
|
326
|
+
terminalStatus,
|
|
327
|
+
...(isNonEmptyString(observationValue.obligationDigest)
|
|
328
|
+
? { obligationDigest: observationValue.obligationDigest }
|
|
329
|
+
: {}),
|
|
330
|
+
...(isNonNegativeInteger(observationValue.tombstoneEpoch)
|
|
331
|
+
? { tombstoneEpoch: observationValue.tombstoneEpoch }
|
|
332
|
+
: {}),
|
|
333
|
+
};
|
|
334
|
+
terminalStatusFromTagObservation(tagObservation);
|
|
335
|
+
if (!isNonEmptyString(value.serviceId) ||
|
|
336
|
+
!isNonEmptyString(value.allocatorLineageId) ||
|
|
337
|
+
!isNonEmptyString(value.attemptId) ||
|
|
338
|
+
!isNonNegativeInteger(value.candidateIndex) ||
|
|
339
|
+
!isNonEmptyString(value.suid) ||
|
|
340
|
+
!isNonEmptyString(value.tag) ||
|
|
341
|
+
!isNonEmptyString(value.identityDigest) ||
|
|
342
|
+
!isNonNegativeInteger(value.pinnedWriterEpoch)) {
|
|
343
|
+
throw new IssuanceLedgerError("resolution evidence fields are incomplete");
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
serviceId: value.serviceId,
|
|
347
|
+
allocatorLineageId: value.allocatorLineageId,
|
|
348
|
+
attemptId: value.attemptId,
|
|
349
|
+
candidateIndex: value.candidateIndex,
|
|
350
|
+
suid: value.suid,
|
|
351
|
+
tag: value.tag,
|
|
352
|
+
identityDigest: value.identityDigest,
|
|
353
|
+
pinnedWriterEpoch: value.pinnedWriterEpoch,
|
|
354
|
+
tagObservation,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
export async function applyTargetResolution(txn, evidence) {
|
|
358
|
+
const terminalStatus = terminalStatusFromTagObservation(evidence.tagObservation);
|
|
359
|
+
const envelope = await txn.get(envelopeKey(evidence.attemptId, evidence.candidateIndex));
|
|
360
|
+
if (envelope === undefined)
|
|
361
|
+
throw new IssuanceLedgerError("unknown issuance envelope");
|
|
362
|
+
if (envelope.serviceId !== evidence.serviceId ||
|
|
363
|
+
envelope.allocatorLineageId !== evidence.allocatorLineageId ||
|
|
364
|
+
envelope.suid !== evidence.suid ||
|
|
365
|
+
envelope.identityDigest !== evidence.identityDigest) {
|
|
366
|
+
throw new IssuanceLedgerError("resolution evidence does not match immutable envelope");
|
|
367
|
+
}
|
|
368
|
+
if (!envelope.canonicalTargetTags.includes(evidence.tag)) {
|
|
369
|
+
throw new IssuanceLedgerError("resolution tag is not in canonical membership");
|
|
370
|
+
}
|
|
371
|
+
const target = await txn.get(targetKey(evidence.attemptId, evidence.candidateIndex, evidence.tag));
|
|
372
|
+
if (target === undefined)
|
|
373
|
+
throw new IssuanceLedgerError("unknown target closure record");
|
|
374
|
+
if (target.status !== "pending") {
|
|
375
|
+
return { candidateResolved: await allTargetsTerminal(txn, evidence.attemptId, evidence.candidateIndex, envelope), duplicate: true };
|
|
376
|
+
}
|
|
377
|
+
await txn.put(targetKey(evidence.attemptId, evidence.candidateIndex, evidence.tag), {
|
|
378
|
+
...target,
|
|
379
|
+
status: terminalStatus,
|
|
380
|
+
});
|
|
381
|
+
const resolved = await allTargetsTerminal(txn, evidence.attemptId, evidence.candidateIndex, envelope);
|
|
382
|
+
if (!resolved)
|
|
383
|
+
return { candidateResolved: false, duplicate: false };
|
|
384
|
+
const unresolvedKey = unresolvedIndexKey(evidence.suid, evidence.attemptId, evidence.candidateIndex);
|
|
385
|
+
const hadUnresolved = await txn.get(unresolvedKey);
|
|
386
|
+
if (hadUnresolved === undefined)
|
|
387
|
+
return { candidateResolved: true, duplicate: true };
|
|
388
|
+
await txn.delete(unresolvedKey);
|
|
389
|
+
const count = await txn.get(ISSUANCE_COUNT_KEY) ?? 0;
|
|
390
|
+
if (count <= 0)
|
|
391
|
+
throw new IssuanceLedgerError("corrupt unresolved count on resolution");
|
|
392
|
+
await txn.put(ISSUANCE_COUNT_KEY, count - 1);
|
|
393
|
+
const prefix = await computeClosedPrefixSuid(txn, txn, await readWatermarkFromState(txn), await txn.get(ISSUANCE_MIGRATION_KEY));
|
|
394
|
+
await txn.put(ISSUANCE_PREFIX_KEY, prefix.closedPrefixSuid);
|
|
395
|
+
return { candidateResolved: true, duplicate: false };
|
|
396
|
+
}
|
|
397
|
+
async function allTargetsTerminal(txn, attemptId, candidateIndex, envelope) {
|
|
398
|
+
for (const tag of envelope.canonicalTargetTags) {
|
|
399
|
+
const record = await txn.get(targetKey(attemptId, candidateIndex, tag));
|
|
400
|
+
if (record === undefined || record.status === "pending")
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
async function readWatermarkFromState(txn) {
|
|
406
|
+
const state = await txn.get("allocator-state");
|
|
407
|
+
return state?.allocatedWatermark ?? null;
|
|
408
|
+
}
|
|
409
|
+
export async function registrationProbe(txn, attemptId, candidateIndex) {
|
|
410
|
+
const envelope = await txn.get(envelopeKey(attemptId, candidateIndex));
|
|
411
|
+
const suid = envelope?.suid;
|
|
412
|
+
const count = await txn.get(ISSUANCE_COUNT_KEY);
|
|
413
|
+
const schedule = await txn.get(ISSUANCE_RECOVERY_KEY);
|
|
414
|
+
let unresolvedIndex = false;
|
|
415
|
+
let issuedIndex = false;
|
|
416
|
+
if (suid !== undefined) {
|
|
417
|
+
unresolvedIndex = await txn.get(unresolvedIndexKey(suid, attemptId, candidateIndex)) !== undefined;
|
|
418
|
+
issuedIndex = await txn.get(issuedIndexKey(suid, attemptId, candidateIndex)) !== undefined;
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
envelope: envelope !== undefined,
|
|
422
|
+
unresolvedIndex,
|
|
423
|
+
issuedIndex,
|
|
424
|
+
exactCount: count !== undefined && Number.isSafeInteger(count),
|
|
425
|
+
recoverySchedule: schedule !== undefined,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
async function listAllAttemptKeys(reader) {
|
|
429
|
+
const keys = [];
|
|
430
|
+
let start;
|
|
431
|
+
const scanLimit = 16;
|
|
432
|
+
while (true) {
|
|
433
|
+
const batch = await reader.list({
|
|
434
|
+
prefix: ATTEMPT_KEY_PREFIX,
|
|
435
|
+
limit: scanLimit,
|
|
436
|
+
...(start !== undefined ? { start } : {}),
|
|
437
|
+
});
|
|
438
|
+
const rawKeys = [...batch.keys()];
|
|
439
|
+
const cursor = start;
|
|
440
|
+
const batchKeys = cursor === undefined
|
|
441
|
+
? rawKeys
|
|
442
|
+
: rawKeys.filter((key) => key > cursor);
|
|
443
|
+
if (batchKeys.length === 0)
|
|
444
|
+
break;
|
|
445
|
+
keys.push(...batchKeys);
|
|
446
|
+
if (rawKeys.length < scanLimit)
|
|
447
|
+
break;
|
|
448
|
+
start = batchKeys[batchKeys.length - 1];
|
|
449
|
+
}
|
|
450
|
+
return keys.sort((left, right) => left.localeCompare(right));
|
|
451
|
+
}
|
|
452
|
+
function emptyLegacyInventorySummary() {
|
|
453
|
+
return {
|
|
454
|
+
vectorOnly: 0,
|
|
455
|
+
resolvedRegistration: 0,
|
|
456
|
+
unresolvedRegistration: 0,
|
|
457
|
+
unclassifiableBlocking: 0,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function legacyMigrationBlocks(migration) {
|
|
461
|
+
const summary = migration.legacyInventorySummary;
|
|
462
|
+
if (summary === undefined)
|
|
463
|
+
return false;
|
|
464
|
+
return summary.unclassifiableBlocking > 0 || summary.unresolvedRegistration > 0;
|
|
465
|
+
}
|
|
466
|
+
async function classifyLegacyAttempt(txn, attemptKey) {
|
|
467
|
+
const attemptId = attemptKey.slice(ATTEMPT_KEY_PREFIX.length);
|
|
468
|
+
if (!isNonEmptyString(attemptId)) {
|
|
469
|
+
return {
|
|
470
|
+
classification: "legacy-unclassifiable-blocking",
|
|
471
|
+
reason: "attempt key is malformed",
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const envelopes = await txn.list({ prefix: `${ENVELOPE_PREFIX}${attemptId}:` });
|
|
475
|
+
if ([...envelopes.keys()].length === 0) {
|
|
476
|
+
return {
|
|
477
|
+
classification: "legacy-vector-only",
|
|
478
|
+
reason: "pre-cut vector without issuance registration",
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
let sawPending = false;
|
|
482
|
+
let sawTerminal = false;
|
|
483
|
+
for (const [, envelope] of envelopes) {
|
|
484
|
+
if (envelope === undefined) {
|
|
485
|
+
return {
|
|
486
|
+
classification: "legacy-unclassifiable-blocking",
|
|
487
|
+
reason: "legacy envelope record is missing",
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
for (const tag of envelope.canonicalTargetTags) {
|
|
491
|
+
const target = await txn.get(targetKey(envelope.attemptId, envelope.candidateIndex, tag));
|
|
492
|
+
if (target === undefined) {
|
|
493
|
+
return {
|
|
494
|
+
classification: "legacy-unclassifiable-blocking",
|
|
495
|
+
reason: "legacy target closure record is missing",
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (target.status === "pending")
|
|
499
|
+
sawPending = true;
|
|
500
|
+
else
|
|
501
|
+
sawTerminal = true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (sawPending) {
|
|
505
|
+
return {
|
|
506
|
+
classification: "legacy-unresolved-registration",
|
|
507
|
+
reason: "legacy issuance registration remains unresolved",
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
if (sawTerminal) {
|
|
511
|
+
return {
|
|
512
|
+
classification: "legacy-resolved-registration",
|
|
513
|
+
reason: "legacy issuance registration is terminal",
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
classification: "legacy-unclassifiable-blocking",
|
|
518
|
+
reason: "legacy attempt has no classifiable target facts",
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function incrementLegacySummary(summary, classification) {
|
|
522
|
+
switch (classification) {
|
|
523
|
+
case "legacy-vector-only":
|
|
524
|
+
return { ...summary, vectorOnly: summary.vectorOnly + 1 };
|
|
525
|
+
case "legacy-resolved-registration":
|
|
526
|
+
return { ...summary, resolvedRegistration: summary.resolvedRegistration + 1 };
|
|
527
|
+
case "legacy-unresolved-registration":
|
|
528
|
+
return { ...summary, unresolvedRegistration: summary.unresolvedRegistration + 1 };
|
|
529
|
+
case "legacy-unclassifiable-blocking":
|
|
530
|
+
return { ...summary, unclassifiableBlocking: summary.unclassifiableBlocking + 1 };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
export async function pageLegacyInventoryInTransaction(txn, input, reader = txn) {
|
|
534
|
+
const migration = await txn.get(ISSUANCE_MIGRATION_KEY);
|
|
535
|
+
if (migration === undefined)
|
|
536
|
+
throw new IssuanceLedgerError("migration cut is required");
|
|
537
|
+
if (migration.inventoryComplete) {
|
|
538
|
+
return { pageCount: 0, nextCursor: migration.legacyInventoryCursor, inventoryComplete: true };
|
|
539
|
+
}
|
|
540
|
+
if (!Number.isSafeInteger(input.pageSize) || input.pageSize < 1) {
|
|
541
|
+
throw new IssuanceLedgerError("inventory page size must be a positive integer");
|
|
542
|
+
}
|
|
543
|
+
const allAttemptKeys = await listAllAttemptKeys(reader);
|
|
544
|
+
const pendingKeys = [];
|
|
545
|
+
for (const key of allAttemptKeys) {
|
|
546
|
+
if (await txn.get(`${LEGACY_INVENTORIED_PREFIX}${key}`) === undefined) {
|
|
547
|
+
pendingKeys.push(key);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const keysToProcess = pendingKeys.slice(0, input.pageSize);
|
|
551
|
+
let lastProcessed = migration.legacyInventoryCursor;
|
|
552
|
+
let summary = migration.legacyInventorySummary ?? emptyLegacyInventorySummary();
|
|
553
|
+
for (const key of keysToProcess) {
|
|
554
|
+
lastProcessed = key;
|
|
555
|
+
const classified = await classifyLegacyAttempt(txn, key);
|
|
556
|
+
summary = incrementLegacySummary(summary, classified.classification);
|
|
557
|
+
await txn.put(`${LEGACY_INVENTORIED_PREFIX}${key}`, {
|
|
558
|
+
inventoriedAt: Date.now(),
|
|
559
|
+
classification: classified.classification,
|
|
560
|
+
reason: classified.reason,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const inventoryComplete = pendingKeys.length <= input.pageSize;
|
|
564
|
+
const updated = {
|
|
565
|
+
...migration,
|
|
566
|
+
legacyInventoryCursor: lastProcessed,
|
|
567
|
+
inventoryComplete,
|
|
568
|
+
legacyInventorySummary: summary,
|
|
569
|
+
};
|
|
570
|
+
await txn.put(ISSUANCE_MIGRATION_KEY, updated);
|
|
571
|
+
if (input.faultInjection === "after-cursor-persist") {
|
|
572
|
+
throw new IssuanceLedgerError("simulated inventory page crash");
|
|
573
|
+
}
|
|
574
|
+
return { pageCount: keysToProcess.length, nextCursor: lastProcessed, inventoryComplete };
|
|
575
|
+
}
|
|
576
|
+
export async function completeMigrationProofInTransaction(txn, input) {
|
|
577
|
+
const migration = await txn.get(ISSUANCE_MIGRATION_KEY);
|
|
578
|
+
if (migration === undefined)
|
|
579
|
+
throw new IssuanceLedgerError("migration cut is required");
|
|
580
|
+
if (!migration.inventoryComplete)
|
|
581
|
+
throw new IssuanceLedgerError("legacy inventory is incomplete");
|
|
582
|
+
if (legacyMigrationBlocks(migration)) {
|
|
583
|
+
throw new IssuanceLedgerError("legacy inventory contains blocking classifications");
|
|
584
|
+
}
|
|
585
|
+
if (!isNonEmptyString(input.migrationProofId)) {
|
|
586
|
+
throw new IssuanceLedgerError("migration proof id is required");
|
|
587
|
+
}
|
|
588
|
+
await txn.put(ISSUANCE_MIGRATION_KEY, {
|
|
589
|
+
...migration,
|
|
590
|
+
migrationProofId: input.migrationProofId,
|
|
591
|
+
});
|
|
592
|
+
await txn.put(`${ISSUANCE_MIGRATION_PROOF_PREFIX}${input.migrationProofId}`, input.boundEvidence);
|
|
593
|
+
}
|
|
594
|
+
export function parseCandidateMembership(raw) {
|
|
595
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
596
|
+
return undefined;
|
|
597
|
+
const value = raw;
|
|
598
|
+
const tags = value.targetTags;
|
|
599
|
+
if (!Array.isArray(tags))
|
|
600
|
+
return undefined;
|
|
601
|
+
const stringTags = tags.filter(isNonEmptyString);
|
|
602
|
+
if (stringTags.length !== tags.length)
|
|
603
|
+
return undefined;
|
|
604
|
+
const epoch = value.pinnedWriterEpoch === undefined ? DEFAULT_WRITER_EPOCH : value.pinnedWriterEpoch;
|
|
605
|
+
if (!isNonNegativeInteger(epoch))
|
|
606
|
+
return undefined;
|
|
607
|
+
return { tags: canonicalizeTargetTags(stringTags), pinnedWriterEpoch: epoch };
|
|
608
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IssuanceEnvelope, TagResolutionObservation, TargetResolutionEvidence } from "./IssuanceLedger";
|
|
2
|
+
export interface TagInspectResult {
|
|
3
|
+
readonly terminalStatus: "installed-and-covered" | "absent-and-irrevocably-fenced";
|
|
4
|
+
readonly eventId?: string;
|
|
5
|
+
readonly obligationDigest?: string;
|
|
6
|
+
readonly tombstoneEpoch?: number;
|
|
7
|
+
}
|
|
8
|
+
export type TagInspectOutcome = {
|
|
9
|
+
readonly kind: "terminal";
|
|
10
|
+
readonly result: TagInspectResult;
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: "absent-never-contacted";
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "absent-but-unfenced";
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: "not-terminal-yet";
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "unreachable";
|
|
19
|
+
};
|
|
20
|
+
export interface IssuanceReconcilerEnv {
|
|
21
|
+
readonly TAG: DurableObjectNamespace;
|
|
22
|
+
readonly ALLOCATOR: DurableObjectNamespace;
|
|
23
|
+
}
|
|
24
|
+
type TagReconciliationEnv = Pick<IssuanceReconcilerEnv, "TAG">;
|
|
25
|
+
export declare function inspectTagTarget(env: TagReconciliationEnv, serviceId: string, tag: string, envelope: IssuanceEnvelope): Promise<TagInspectOutcome>;
|
|
26
|
+
export declare function fenceAbsentTarget(env: TagReconciliationEnv, serviceId: string, tag: string, envelope: IssuanceEnvelope): Promise<boolean>;
|
|
27
|
+
export declare function tagObservationMatchesInspect(submitted: TagResolutionObservation, result: TagInspectResult): boolean;
|
|
28
|
+
export declare function verifyTargetResolutionEvidence(env: TagReconciliationEnv, serviceId: string, evidence: TargetResolutionEvidence, envelope: IssuanceEnvelope): Promise<void>;
|
|
29
|
+
export declare function submitTargetResolution(env: IssuanceReconcilerEnv, serviceId: string, evidence: TargetResolutionEvidence): Promise<boolean>;
|
|
30
|
+
export interface ReconcileIssuanceBatchOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Test/admin `/__internal/g77/reconcile-now` bypasses never-contacted grace so
|
|
33
|
+
* bounded-reconciliation oracles can fence immediately without advancing DO time.
|
|
34
|
+
*/
|
|
35
|
+
readonly bypassNeverContactedGrace?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function reconcileIssuanceBatch(storage: DurableObjectStorage, env: Pick<IssuanceReconcilerEnv, "TAG">, serviceId: string, resolveTarget?: (evidence: TargetResolutionEvidence) => Promise<boolean>, options?: ReconcileIssuanceBatchOptions): Promise<{
|
|
38
|
+
processed: number;
|
|
39
|
+
rearmAt: number;
|
|
40
|
+
}>;
|
|
41
|
+
export {};
|