sealbridge 0.4.1 → 0.5.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/README.ja.md +4 -2
- package/README.md +6 -2
- package/dist/cli/dist/accept.d.ts +2 -1
- package/dist/cli/dist/accept.js +19 -8
- package/dist/cli/dist/accept.js.map +1 -1
- package/dist/cli/dist/board.js +21 -0
- package/dist/cli/dist/board.js.map +1 -1
- package/dist/cli/dist/config.js +1 -1
- package/dist/cli/dist/config.js.map +1 -1
- package/dist/cli/dist/connections.js +7 -2
- package/dist/cli/dist/connections.js.map +1 -1
- package/dist/cli/dist/contacts.js +6 -1
- package/dist/cli/dist/contacts.js.map +1 -1
- package/dist/cli/dist/index.js +65 -20
- package/dist/cli/dist/index.js.map +1 -1
- package/dist/cli/dist/invite-links.d.ts +14 -0
- package/dist/cli/dist/invite-links.js +263 -0
- package/dist/cli/dist/invite-links.js.map +1 -0
- package/dist/cli/dist/invite-output.d.ts +1 -1
- package/dist/cli/dist/invite-output.js +3 -2
- package/dist/cli/dist/invite-output.js.map +1 -1
- package/dist/cli/dist/invite-share.js +14 -0
- package/dist/cli/dist/invite-share.js.map +1 -1
- package/dist/cli/dist/invites-migrate.js +1 -1
- package/dist/cli/dist/invites-migrate.js.map +1 -1
- package/dist/cli/dist/mcp.js +1 -1
- package/dist/cli/dist/mcp.js.map +1 -1
- package/dist/cli/dist/network.js +107 -48
- package/dist/cli/dist/network.js.map +1 -1
- package/dist/cli/dist/pending.js +1 -1
- package/dist/cli/dist/pending.js.map +1 -1
- package/dist/cli/dist/receipt.d.ts +12 -2
- package/dist/cli/dist/receipt.js +10 -5
- package/dist/cli/dist/receipt.js.map +1 -1
- package/dist/cli/dist/requests.js +1 -1
- package/dist/cli/dist/requests.js.map +1 -1
- package/dist/cli/dist/sdk.d.ts +6 -4
- package/dist/cli/dist/sdk.js +3 -2
- package/dist/cli/dist/sdk.js.map +1 -1
- package/dist/cli/dist/setup.js +1 -1
- package/dist/cli/dist/setup.js.map +1 -1
- package/dist/cli/dist/tasks.js +1 -1
- package/dist/cli/dist/tasks.js.map +1 -1
- package/dist/examples/two-owner-demo/dist/run-demo.js +3 -1
- package/dist/examples/two-owner-demo/dist/run-demo.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/packages/core/dist/context-grant.d.ts +13 -0
- package/dist/packages/core/dist/context-grant.js +65 -0
- package/dist/packages/core/dist/context-grant.js.map +1 -0
- package/dist/packages/core/dist/delegation.d.ts +17 -0
- package/dist/packages/core/dist/delegation.js +126 -0
- package/dist/packages/core/dist/delegation.js.map +1 -0
- package/dist/packages/core/dist/edge.d.ts +2 -0
- package/dist/packages/core/dist/edge.js +2 -0
- package/dist/packages/core/dist/edge.js.map +1 -1
- package/dist/packages/core/dist/index.d.ts +4 -0
- package/dist/packages/core/dist/index.js +4 -0
- package/dist/packages/core/dist/index.js.map +1 -1
- package/dist/packages/core/dist/invite-link.d.ts +72 -0
- package/dist/packages/core/dist/invite-link.js +225 -0
- package/dist/packages/core/dist/invite-link.js.map +1 -0
- package/dist/packages/core/dist/invite-sas.d.ts +7 -0
- package/dist/packages/core/dist/invite-sas.js +15 -0
- package/dist/packages/core/dist/invite-sas.js.map +1 -0
- package/dist/packages/core/dist/relay-auth.d.ts +1 -1
- package/dist/packages/core/dist/relay-auth.js +1 -1
- package/dist/packages/core/dist/relay-auth.js.map +1 -1
- package/dist/packages/core/dist/schema.d.ts +1 -1
- package/dist/packages/core/dist/schema.js +4 -0
- package/dist/packages/core/dist/schema.js.map +1 -1
- package/dist/packages/core/dist/types.d.ts +52 -0
- package/dist/packages/ledger/dist/index.d.ts +70 -2
- package/dist/packages/ledger/dist/index.js +404 -26
- package/dist/packages/ledger/dist/index.js.map +1 -1
- package/dist/packages/relay/dist/fetch.js +4 -0
- package/dist/packages/relay/dist/fetch.js.map +1 -1
- package/dist/packages/relay/dist/invites.d.ts +42 -0
- package/dist/packages/relay/dist/invites.js +347 -0
- package/dist/packages/relay/dist/invites.js.map +1 -0
- package/dist/packages/relay/dist/worker.d.ts +3 -0
- package/dist/packages/relay/dist/worker.js +21 -0
- package/dist/packages/relay/dist/worker.js.map +1 -1
- package/dist/packages/runtime/dist/a2a.js +1 -1
- package/dist/packages/runtime/dist/adapter.d.ts +1 -0
- package/dist/packages/runtime/dist/approval-providers.d.ts +4 -0
- package/dist/packages/runtime/dist/approval-providers.js +3 -0
- package/dist/packages/runtime/dist/approval-providers.js.map +1 -1
- package/dist/packages/runtime/dist/connection-persistence.d.ts +1 -1
- package/dist/packages/runtime/dist/context-grant-audit.d.ts +8 -0
- package/dist/packages/runtime/dist/context-grant-audit.js +41 -0
- package/dist/packages/runtime/dist/context-grant-audit.js.map +1 -0
- package/dist/packages/runtime/dist/delegation.d.ts +37 -0
- package/dist/packages/runtime/dist/delegation.js +125 -0
- package/dist/packages/runtime/dist/delegation.js.map +1 -0
- package/dist/packages/runtime/dist/gateway.d.ts +1 -0
- package/dist/packages/runtime/dist/gateway.js.map +1 -1
- package/dist/packages/runtime/dist/index.d.ts +2 -0
- package/dist/packages/runtime/dist/index.js +2 -0
- package/dist/packages/runtime/dist/index.js.map +1 -1
- package/dist/packages/runtime/dist/mcp-server.js +22 -8
- package/dist/packages/runtime/dist/mcp-server.js.map +1 -1
- package/dist/packages/runtime/dist/mcp-tool-catalog.js +44 -5
- package/dist/packages/runtime/dist/mcp-tool-catalog.js.map +1 -1
- package/dist/packages/runtime/dist/protocol-release.js +5 -2
- package/dist/packages/runtime/dist/protocol-release.js.map +1 -1
- package/dist/packages/runtime/dist/session-audit.d.ts +6 -1
- package/dist/packages/runtime/dist/session-audit.js +35 -4
- package/dist/packages/runtime/dist/session-audit.js.map +1 -1
- package/dist/packages/runtime/dist/session.d.ts +21 -1
- package/dist/packages/runtime/dist/session.js +427 -62
- package/dist/packages/runtime/dist/session.js.map +1 -1
- package/dist/packages/runtime/dist/task-journal.d.ts +10 -1
- package/dist/packages/runtime/dist/task-journal.js +26 -1
- package/dist/packages/runtime/dist/task-journal.js.map +1 -1
- package/dist/packages/runtime/dist/verifier.d.ts +5 -2
- package/dist/packages/runtime/dist/verifier.js +14 -4
- package/dist/packages/runtime/dist/verifier.js.map +1 -1
- package/dist/spec/audit-entry.schema.json +25 -1
- package/dist/spec/audit-evidence.schema.json +179 -0
- package/dist/spec/audit-file-record.schema.json +177 -0
- package/dist/spec/audit-interruption.schema.json +110 -0
- package/dist/spec/context-grant-audit-payload.schema.json +24 -0
- package/dist/spec/context-grant.schema.json +146 -0
- package/dist/spec/delegation-attachment.schema.json +167 -0
- package/dist/spec/delegation-link.schema.json +141 -0
- package/dist/spec/examples/context-pooling/approval_redelegation/sample-delegation-attachment.json +413 -0
- package/dist/spec/examples/context-pooling/hand_over/sample-context-grant.json +18 -0
- package/dist/spec/examples/context-pooling/hand_over/sample-task-manifest.0.2.0.json +94 -0
- package/dist/spec/examples/context-pooling/sample-context-grant-audit-payload.json +4 -0
- package/dist/spec/examples/context-pooling/view_only/sample-context-grant.json +18 -0
- package/dist/spec/examples/context-pooling/view_only/sample-task-manifest.0.2.0.json +94 -0
- package/dist/spec/examples/sample-audit-evidence.json +138 -0
- package/dist/spec/examples/sample-audit-file-record.json +31 -0
- package/dist/spec/examples/sample-audit-interruption.json +29 -0
- package/dist/spec/task-manifest.0.2.0.schema.json +87 -1
- package/docs/context-grants.md +221 -0
- package/docs/sdk.ja.md +4 -0
- package/docs/sdk.md +4 -0
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,OAAO,MAAM,aAAa,CAAC;AAElC,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC1D,MAAM,UAAU,SAAS;IACvB,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC;IAClF,OAA2C,CAAC,GAAG,CAAC,CAAC;IAClD,OAAO,GAAG,CAAC;AACb,CAAC;AACD,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxE,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,eAAe,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,YAAY;IAC3E,iBAAiB,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,aAAa;
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -97,6 +97,7 @@ export interface BudgetSpec {
|
|
|
97
97
|
onOverrun: 'hard_stop' | 'request_hitl_amendment';
|
|
98
98
|
}
|
|
99
99
|
export interface TaskManifest {
|
|
100
|
+
contextGrant?: ContextGrant;
|
|
100
101
|
protocolVersion: string;
|
|
101
102
|
taskId: string;
|
|
102
103
|
createdAt: string;
|
|
@@ -170,3 +171,54 @@ export interface TaskManifest {
|
|
|
170
171
|
};
|
|
171
172
|
signatures?: Record<string, Signature>;
|
|
172
173
|
}
|
|
174
|
+
export type DeliveryMode = 'view_only' | 'approval_redelegation' | 'hand_over';
|
|
175
|
+
export interface ContextGrant {
|
|
176
|
+
deliveryMode: DeliveryMode;
|
|
177
|
+
/** Opaque selection declarations; CP1 does not extract data from a source. */
|
|
178
|
+
selectors: string[];
|
|
179
|
+
fieldsAllowed?: string[];
|
|
180
|
+
fieldsDenied?: string[];
|
|
181
|
+
angle: string;
|
|
182
|
+
expiresAt: string;
|
|
183
|
+
maxBytes: number;
|
|
184
|
+
retention: 'ephemeral_view' | 'keep_deliverable';
|
|
185
|
+
inputRetentionAllowed?: boolean;
|
|
186
|
+
outputSchema?: Schema;
|
|
187
|
+
}
|
|
188
|
+
export type ContextGrantOptions = Pick<ContextGrant, 'selectors' | 'angle'> & Partial<Omit<ContextGrant, 'selectors' | 'angle'>>;
|
|
189
|
+
export type ContextGrantAuditEventType = 'CONTEXT_GRANT' | 'CONTEXT_OPENED' | 'CONTEXT_DISCARDED' | 'CONTEXT_DELETION_ATTEMPTED' | 'CONTEXT_MODE';
|
|
190
|
+
export interface ContextGrantAuditPayload {
|
|
191
|
+
grantDigest: string;
|
|
192
|
+
deliveryMode: DeliveryMode;
|
|
193
|
+
}
|
|
194
|
+
/** Cross-task provenance, distinct from the same-task capability chain. */
|
|
195
|
+
export interface DelegationLimits {
|
|
196
|
+
deadline: string;
|
|
197
|
+
expiresAt: string;
|
|
198
|
+
budget: TaskManifest['budget'];
|
|
199
|
+
sandboxPolicy: TaskManifest['sandboxPolicy'];
|
|
200
|
+
verification: TaskManifest['verification'];
|
|
201
|
+
depthRemaining: number;
|
|
202
|
+
}
|
|
203
|
+
export interface DelegationLinkPayload {
|
|
204
|
+
type: 'DelegationLink';
|
|
205
|
+
parentTaskId: string;
|
|
206
|
+
parentContractDigest: string;
|
|
207
|
+
childTaskId: string;
|
|
208
|
+
childContractDigest: string;
|
|
209
|
+
delegatorOwnerDid: string;
|
|
210
|
+
delegateeOwnerDid: string;
|
|
211
|
+
depthRemaining: number;
|
|
212
|
+
limits: DelegationLimits;
|
|
213
|
+
issuedAt: string;
|
|
214
|
+
prevLinkDigest: string | null;
|
|
215
|
+
}
|
|
216
|
+
export type DelegationLink = Signed<DelegationLinkPayload>;
|
|
217
|
+
export interface DelegationProof {
|
|
218
|
+
link: DelegationLink;
|
|
219
|
+
childContract: import('./contract.js').TaskContract;
|
|
220
|
+
}
|
|
221
|
+
export interface DelegationAttachment {
|
|
222
|
+
parentContract: import('./contract.js').TaskContract;
|
|
223
|
+
children: DelegationProof[];
|
|
224
|
+
}
|
|
@@ -28,23 +28,64 @@ export interface AuditBundle {
|
|
|
28
28
|
receiver: Signed<Signed<Checkpoint>>;
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
/** Local storage format 1; never a wire event or bilateral checkpoint. */
|
|
32
|
+
export interface AuditInterruption {
|
|
33
|
+
type: 'AuditInterruption';
|
|
34
|
+
formatVersion: 1;
|
|
35
|
+
taskId: string;
|
|
36
|
+
actors: [string, string];
|
|
37
|
+
seq: number;
|
|
38
|
+
prevEntryHash: string;
|
|
39
|
+
acknowledgedPrefix: {
|
|
40
|
+
count: number;
|
|
41
|
+
lastSeq: number;
|
|
42
|
+
head: string;
|
|
43
|
+
entriesDigest: string;
|
|
44
|
+
};
|
|
45
|
+
phase: 'OWNER_APPROVAL' | 'ACCEPT_ACK_WAIT';
|
|
46
|
+
reason: 'SESSION_TIMEOUT' | 'WORKER_STOPPED' | 'WORKER_RESTART_OBSERVED';
|
|
47
|
+
observedAt: string;
|
|
48
|
+
}
|
|
49
|
+
export interface InterruptedAuditBundle {
|
|
50
|
+
entries: AuditEntry[];
|
|
51
|
+
interruption: Signed<AuditInterruption>;
|
|
52
|
+
}
|
|
53
|
+
export type AuditEvidence = AuditBundle | InterruptedAuditBundle;
|
|
54
|
+
export type AuditFileRecord = {
|
|
55
|
+
entry: AuditEntry;
|
|
56
|
+
} | {
|
|
57
|
+
checkpoint: AuditBundle['checkpoint'];
|
|
58
|
+
} | {
|
|
59
|
+
interruption: Signed<AuditInterruption>;
|
|
60
|
+
};
|
|
61
|
+
export interface InterruptionStore {
|
|
62
|
+
/** Idempotent only for exactly the same signed terminal record and prefix.
|
|
63
|
+
* beforeWrite must synchronously persist the signed intent; promises are rejected. */
|
|
64
|
+
closeInterrupted(bundle: InterruptedAuditBundle, beforeWrite?: () => void): void;
|
|
65
|
+
}
|
|
31
66
|
export interface AuditStore {
|
|
32
67
|
read(): AuditEntry[];
|
|
33
68
|
append(entry: AuditEntry, checkpoint: AuditBundle['checkpoint']): void;
|
|
34
69
|
seal?(checkpoint: AuditBundle['checkpoint']): void;
|
|
35
70
|
}
|
|
36
|
-
export declare class MemoryStore implements AuditStore {
|
|
71
|
+
export declare class MemoryStore implements AuditStore, InterruptionStore {
|
|
37
72
|
#private;
|
|
38
73
|
read(): AuditEntry[];
|
|
39
74
|
append(entry: AuditEntry): void;
|
|
75
|
+
seal(checkpoint: AuditBundle['checkpoint']): void;
|
|
76
|
+
closeInterrupted(bundle: InterruptedAuditBundle, beforeWrite?: () => void): void;
|
|
40
77
|
}
|
|
41
|
-
export declare class JsonlStore implements AuditStore {
|
|
78
|
+
export declare class JsonlStore implements AuditStore, InterruptionStore {
|
|
42
79
|
readonly path: string;
|
|
43
80
|
constructor(path: string);
|
|
81
|
+
/** All cooperating writers use this exclusive file lock. Never share with an older writer.
|
|
82
|
+
* A lock left by process death is an explicit local recovery operation, never stolen. */
|
|
83
|
+
private exclusive;
|
|
44
84
|
private records;
|
|
45
85
|
read(): AuditEntry[];
|
|
46
86
|
append(entry: AuditEntry): void;
|
|
47
87
|
seal(checkpoint: AuditBundle['checkpoint']): void;
|
|
88
|
+
closeInterrupted(bundle: InterruptedAuditBundle, beforeWrite?: () => void): void;
|
|
48
89
|
}
|
|
49
90
|
export declare function parseJsonl(text: string): AuditBundle;
|
|
50
91
|
export declare class BilateralLedger {
|
|
@@ -82,3 +123,30 @@ export interface AuditVerificationResult {
|
|
|
82
123
|
*/
|
|
83
124
|
export declare const AUDIT_VALID_WITHOUT_TRUST_ANCHOR = "VALID (no trust anchor: truncation not checked)";
|
|
84
125
|
export declare function verifyAudit(bundle: AuditBundle, expected?: AuditVerificationOptions): AuditVerificationResult;
|
|
126
|
+
export interface AuditEvidenceTrustAnchor extends AuditTrustAnchor {
|
|
127
|
+
expectedClosureKind?: 'bilateral' | 'interruption';
|
|
128
|
+
expectedInterruptionHash?: string;
|
|
129
|
+
}
|
|
130
|
+
export interface AuditEvidenceVerificationOptions extends AuditEvidenceTrustAnchor {
|
|
131
|
+
trustAnchor?: AuditEvidenceTrustAnchor;
|
|
132
|
+
}
|
|
133
|
+
export interface InterruptedAuditVerificationResult {
|
|
134
|
+
valid: false;
|
|
135
|
+
status: 'UNILATERALLY_CLOSED' | 'INVALID';
|
|
136
|
+
statusLabel: string;
|
|
137
|
+
reason: string;
|
|
138
|
+
prefixVerified: boolean;
|
|
139
|
+
terminalSignatureVerified: boolean;
|
|
140
|
+
bilateralCheckpointVerified: false;
|
|
141
|
+
trustAnchorVerified: boolean;
|
|
142
|
+
/** For interruption only: an actual tail condition was compared, not identity alone. */
|
|
143
|
+
truncationChecked: boolean;
|
|
144
|
+
closureKind: 'interruption';
|
|
145
|
+
}
|
|
146
|
+
export type AuditEvidenceVerificationResult = AuditVerificationResult | InterruptedAuditVerificationResult;
|
|
147
|
+
/** Read either terminal form, retaining physical JSONL locations out of band. */
|
|
148
|
+
export declare function parseAuditJsonl(text: string): AuditEvidence;
|
|
149
|
+
/** Both formats are verified locally. A worker-only observation never becomes VALID. */
|
|
150
|
+
export declare function verifyAuditEvidence(bundle: unknown, expected?: AuditEvidenceVerificationOptions): AuditEvidenceVerificationResult;
|
|
151
|
+
/** Creates a worker observation; callers must preserve the original prefix and journal the intent before writing. */
|
|
152
|
+
export declare function createAuditInterruption(entries: AuditEntry[], worker: KeyPair, phase: AuditInterruption['phase'], reason: AuditInterruption['reason'], observedAt?: string): Signed<AuditInterruption>;
|
|
@@ -1,27 +1,151 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
import { appendFileSync, readFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
2
|
+
import { appendFileSync, readFileSync, openSync, closeSync, fsyncSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { digest, signPayload, signAck, verifySigned, verifySignature, validateSchema, validateManifest, } from '../../core/dist/index.js';
|
|
5
|
+
function persistInterruptionIntent(beforeWrite) {
|
|
6
|
+
const result = beforeWrite?.();
|
|
7
|
+
if (result && typeof result.then === 'function') {
|
|
8
|
+
void Promise.resolve(result).catch(() => { });
|
|
9
|
+
throw new Error('ASYNC_AUDIT_INTENT_FORBIDDEN');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// Source locations are diagnostic metadata only: never serialize them into a bundle.
|
|
13
|
+
const jsonlLocations = new WeakMap();
|
|
14
|
+
function object(value) {
|
|
15
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
16
|
+
? value : undefined;
|
|
17
|
+
}
|
|
18
|
+
function diagnosticText(value) {
|
|
19
|
+
if (typeof value !== 'string')
|
|
20
|
+
return undefined;
|
|
21
|
+
if (/acp-invite:/i.test(value))
|
|
22
|
+
return '[redacted]';
|
|
23
|
+
return value.length > 160 ? value.slice(0, 160) + '...' : value;
|
|
24
|
+
}
|
|
25
|
+
function diagnosticJson(value) {
|
|
26
|
+
// JSON escaping covers C0 controls; also escape DEL, C1 and Unicode display controls.
|
|
27
|
+
return JSON.stringify(value).replace(/[\u007f-\uffff]/g, char => '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
28
|
+
}
|
|
29
|
+
function entryLocation(entry, locationKind, index, line) {
|
|
30
|
+
const e = object(entry), body = object(e?.body);
|
|
31
|
+
return {
|
|
32
|
+
locationKind,
|
|
33
|
+
taskId: diagnosticText(body?.taskId),
|
|
34
|
+
seq: typeof body?.seq === 'number' && Number.isSafeInteger(body.seq) && body.seq >= 0
|
|
35
|
+
? body.seq : undefined,
|
|
36
|
+
entryHash: diagnosticText(e?.entryHash),
|
|
37
|
+
line,
|
|
38
|
+
index,
|
|
39
|
+
verified: false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function checkpointLocation(bundle) {
|
|
43
|
+
const checkpoint = object(bundle?.checkpoint), payload = object(object(checkpoint?.actor)?.payload);
|
|
44
|
+
const entries = Array.isArray(bundle?.entries) ? bundle.entries : [];
|
|
45
|
+
const source = jsonlLocations.get(bundle);
|
|
46
|
+
return {
|
|
47
|
+
locationKind: 'checkpoint',
|
|
48
|
+
taskId: diagnosticText(payload?.taskId),
|
|
49
|
+
head: diagnosticText(payload?.head),
|
|
50
|
+
line: source?.checkpointLine,
|
|
51
|
+
lastEntry: entries.length
|
|
52
|
+
? entryLocation(entries.at(-1), 'last-entry', entries.length - 1, source?.entryLines.at(-1)) : undefined,
|
|
53
|
+
verified: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function parseRecords(text) {
|
|
57
|
+
// Keep the existing trim/filter rules while counting physical lines before filtering.
|
|
58
|
+
const start = text.length - text.trimStart().length;
|
|
59
|
+
const firstLine = text.slice(0, start).split('\n').length;
|
|
60
|
+
return text.trim().split('\n').flatMap((line, index) => {
|
|
61
|
+
if (!line)
|
|
62
|
+
return [];
|
|
63
|
+
const physicalLine = firstLine + index;
|
|
64
|
+
let record;
|
|
65
|
+
try {
|
|
66
|
+
record = JSON.parse(line);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Engine JSON errors can quote the input, including task payloads or invitations.
|
|
70
|
+
throw new SyntaxError('Invalid JSON; auditLocation=' + diagnosticJson({ line: physicalLine, verified: false }));
|
|
71
|
+
}
|
|
72
|
+
return [{ record, line: physicalLine }];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
4
75
|
export class MemoryStore {
|
|
5
76
|
#entries = [];
|
|
77
|
+
#interruption;
|
|
78
|
+
#checkpoint;
|
|
79
|
+
#closing = false;
|
|
6
80
|
read() {
|
|
7
81
|
return structuredClone(this.#entries);
|
|
8
82
|
}
|
|
9
83
|
append(entry) {
|
|
84
|
+
if (this.#closing)
|
|
85
|
+
throw new Error('LEDGER_BUSY');
|
|
86
|
+
if (this.#interruption || this.#checkpoint)
|
|
87
|
+
throw new Error('LEDGER_SEALED');
|
|
10
88
|
this.#entries.push(structuredClone(entry));
|
|
11
89
|
}
|
|
90
|
+
seal(checkpoint) {
|
|
91
|
+
if (this.#closing)
|
|
92
|
+
throw new Error('LEDGER_BUSY');
|
|
93
|
+
if (this.#interruption)
|
|
94
|
+
throw new Error('LEDGER_SEALED');
|
|
95
|
+
if (this.#checkpoint && digest(this.#checkpoint.actor.payload) !== digest(checkpoint.actor.payload))
|
|
96
|
+
throw new Error('LEDGER_SEALED');
|
|
97
|
+
this.#checkpoint = structuredClone(checkpoint);
|
|
98
|
+
}
|
|
99
|
+
closeInterrupted(bundle, beforeWrite) {
|
|
100
|
+
if (this.#closing)
|
|
101
|
+
throw new Error('LEDGER_BUSY');
|
|
102
|
+
bundle = structuredClone(bundle);
|
|
103
|
+
if (this.#checkpoint)
|
|
104
|
+
throw new Error('LEDGER_SEALED');
|
|
105
|
+
if (digest(this.#entries) !== digest(bundle.entries) ||
|
|
106
|
+
verifyAuditEvidence(bundle).status !== 'UNILATERALLY_CLOSED')
|
|
107
|
+
throw new Error('INVALID_AUDIT_INTERRUPTION');
|
|
108
|
+
if (this.#interruption && digest(this.#interruption) !== digest(bundle.interruption))
|
|
109
|
+
throw new Error('LEDGER_SEALED');
|
|
110
|
+
this.#closing = true;
|
|
111
|
+
try {
|
|
112
|
+
persistInterruptionIntent(beforeWrite);
|
|
113
|
+
this.#interruption = bundle.interruption;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
this.#closing = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
12
119
|
}
|
|
13
120
|
export class JsonlStore {
|
|
14
121
|
path;
|
|
15
122
|
constructor(path) {
|
|
16
123
|
this.path = path;
|
|
17
124
|
}
|
|
125
|
+
/** All cooperating writers use this exclusive file lock. Never share with an older writer.
|
|
126
|
+
* A lock left by process death is an explicit local recovery operation, never stolen. */
|
|
127
|
+
exclusive(operation) {
|
|
128
|
+
const path = resolve(this.path) + '.lock';
|
|
129
|
+
let fd;
|
|
130
|
+
try {
|
|
131
|
+
fd = openSync(path, 'wx', 0o600);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code === 'EEXIST')
|
|
135
|
+
throw new Error('LEDGER_BUSY');
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
return operation();
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
closeSync(fd);
|
|
143
|
+
unlinkSync(path);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
18
146
|
records() {
|
|
19
147
|
try {
|
|
20
|
-
return readFileSync(this.path, 'utf8')
|
|
21
|
-
.trim()
|
|
22
|
-
.split('\n')
|
|
23
|
-
.filter(Boolean)
|
|
24
|
-
.map((l) => JSON.parse(l));
|
|
148
|
+
return parseRecords(readFileSync(this.path, 'utf8')).map(({ record }) => record);
|
|
25
149
|
}
|
|
26
150
|
catch (e) {
|
|
27
151
|
if (e.code === 'ENOENT')
|
|
@@ -35,30 +159,93 @@ export class JsonlStore {
|
|
|
35
159
|
.map((r) => r.entry);
|
|
36
160
|
}
|
|
37
161
|
append(entry) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
162
|
+
this.exclusive(() => {
|
|
163
|
+
if (this.records().some((r) => r.checkpoint || Object.hasOwn(r, 'interruption')))
|
|
164
|
+
throw new Error('LEDGER_SEALED');
|
|
165
|
+
appendFileSync(this.path, JSON.stringify({ entry }) + '\n', { mode: 0o600 });
|
|
166
|
+
});
|
|
41
167
|
}
|
|
42
168
|
seal(checkpoint) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
169
|
+
this.exclusive(() => {
|
|
170
|
+
const records = this.records();
|
|
171
|
+
if (records.some(r => Object.hasOwn(r, 'interruption')))
|
|
46
172
|
throw new Error('LEDGER_SEALED');
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
173
|
+
const old = records.at(-1)?.checkpoint;
|
|
174
|
+
if (old) {
|
|
175
|
+
if (digest(old.actor.payload) !== digest(checkpoint.actor.payload))
|
|
176
|
+
throw new Error('LEDGER_SEALED');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
appendFileSync(this.path, JSON.stringify({ checkpoint }) + '\n', { mode: 0o600 });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
closeInterrupted(bundle, beforeWrite) {
|
|
183
|
+
bundle = structuredClone(bundle);
|
|
184
|
+
this.exclusive(() => {
|
|
185
|
+
if (verifyAuditEvidence(bundle).status !== 'UNILATERALLY_CLOSED')
|
|
186
|
+
throw new Error('INVALID_AUDIT_INTERRUPTION');
|
|
187
|
+
const text = readFileSync(this.path, 'utf8'), records = parseRecords(text);
|
|
188
|
+
const terminal = records.at(-1)?.record.interruption;
|
|
189
|
+
if (terminal) {
|
|
190
|
+
const existing = parseAuditJsonl(text);
|
|
191
|
+
if (digest(existing) !== digest(bundle))
|
|
192
|
+
throw new Error('LEDGER_SEALED');
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
if (records.some(({ record }) => !object(record) || Object.keys(record).length !== 1 || !record.entry) ||
|
|
196
|
+
digest(records.map(({ record }) => record.entry)) !== digest(bundle.entries))
|
|
197
|
+
throw new Error('AUDIT_PREFIX_MISMATCH');
|
|
198
|
+
}
|
|
199
|
+
persistInterruptionIntent(beforeWrite);
|
|
200
|
+
// Journal callbacks must not change the audit. Cooperating processes remain excluded.
|
|
201
|
+
if (readFileSync(this.path, 'utf8') !== text)
|
|
202
|
+
throw new Error('AUDIT_PREFIX_MISMATCH');
|
|
203
|
+
const fd = openSync(this.path, 'a');
|
|
204
|
+
try {
|
|
205
|
+
if (!terminal)
|
|
206
|
+
appendFileSync(fd, (text.endsWith('\n') ? '' : '\n') + JSON.stringify({ interruption: bundle.interruption }) + '\n');
|
|
207
|
+
// Includes the original acknowledged prefix, and also retries a failed sync after append.
|
|
208
|
+
fsyncSync(fd);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
closeSync(fd);
|
|
212
|
+
}
|
|
213
|
+
if (process.platform !== 'win32') {
|
|
214
|
+
const dir = openSync(dirname(this.path), 'r');
|
|
215
|
+
try {
|
|
216
|
+
fsyncSync(dir);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
closeSync(dir);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
50
223
|
}
|
|
51
224
|
}
|
|
52
225
|
export function parseJsonl(text) {
|
|
53
|
-
const records = text
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
226
|
+
const records = parseRecords(text);
|
|
227
|
+
const last = records.at(-1);
|
|
228
|
+
const misplaced = records.slice(0, -1).find(({ record }) => !object(record)?.entry || object(record)?.checkpoint || Object.hasOwn(record, 'interruption'));
|
|
229
|
+
if (!object(last?.record)?.checkpoint || object(last?.record)?.entry || Object.hasOwn(last.record, 'interruption') || misplaced) {
|
|
230
|
+
const offending = misplaced ?? last;
|
|
231
|
+
const entry = object(offending?.record)?.entry;
|
|
232
|
+
const checkpoint = object(offending?.record)?.checkpoint;
|
|
233
|
+
const payload = object(object(object(checkpoint)?.actor)?.payload);
|
|
234
|
+
const lastEntry = !entry && !checkpoint
|
|
235
|
+
? records.findLast(({ record }) => object(record)?.entry) : undefined;
|
|
236
|
+
const location = entry
|
|
237
|
+
? entryLocation(entry, !misplaced && !object(last?.record)?.checkpoint ? 'last-entry' : 'entry')
|
|
238
|
+
: { locationKind: checkpoint ? 'checkpoint' : 'record', taskId: diagnosticText(payload?.taskId),
|
|
239
|
+
head: diagnosticText(payload?.head), verified: false };
|
|
240
|
+
throw new Error('MISSING_OR_MISPLACED_CHECKPOINT; auditLocation=' + diagnosticJson({
|
|
241
|
+
...location, line: offending?.line,
|
|
242
|
+
lastEntry: lastEntry ? { ...entryLocation(lastEntry.record.entry, 'last-entry'), line: lastEntry.line } : undefined,
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
const entryRecords = records.slice(0, -1);
|
|
246
|
+
const bundle = { entries: entryRecords.map(({ record }) => record.entry), checkpoint: last.record.checkpoint };
|
|
247
|
+
jsonlLocations.set(bundle, { entryLines: entryRecords.map(({ line }) => line), checkpointLine: last.line });
|
|
248
|
+
return bundle;
|
|
62
249
|
}
|
|
63
250
|
export class BilateralLedger {
|
|
64
251
|
taskId;
|
|
@@ -224,9 +411,22 @@ function auditResult(supplied, error) {
|
|
|
224
411
|
truncationChecked: true,
|
|
225
412
|
};
|
|
226
413
|
}
|
|
414
|
+
function verificationError(error) {
|
|
415
|
+
// Retain existing verifier codes and schema explanations, without forwarding arbitrary exceptions.
|
|
416
|
+
if (error instanceof Error && (/^(checkpoint|trust anchor|entry \d+|[A-Z][A-Z0-9_]{0,79})$/.test(error.message)
|
|
417
|
+
|| error.message.startsWith('SCHEMA_VIOLATION: '))) {
|
|
418
|
+
const message = error.message;
|
|
419
|
+
const bounded = message.length > 1024 ? message.slice(0, 1024) + '...' : message;
|
|
420
|
+
return 'Error: ' + diagnosticJson(bounded).slice(1, -1);
|
|
421
|
+
}
|
|
422
|
+
return error instanceof TypeError ? 'TypeError' : 'Error';
|
|
423
|
+
}
|
|
227
424
|
export function verifyAudit(bundle, expected) {
|
|
228
425
|
const supplied = anchorSupplied(expected);
|
|
426
|
+
let location = checkpointLocation(bundle);
|
|
229
427
|
try {
|
|
428
|
+
if (object(bundle) && Object.hasOwn(bundle, 'interruption'))
|
|
429
|
+
throw new Error('checkpoint');
|
|
230
430
|
const { entries, checkpoint } = bundle, c = checkpoint.actor.payload;
|
|
231
431
|
const selfDelegated = Array.isArray(c?.actors) && c.actors.length === 2 && c.actors[0] === c.actors[1];
|
|
232
432
|
if (c.type !== 'AuditCheckpoint' ||
|
|
@@ -243,6 +443,7 @@ export function verifyAudit(bundle, expected) {
|
|
|
243
443
|
throw new Error('checkpoint');
|
|
244
444
|
assertTrustAnchor(resolveAnchor(expected), c, entries);
|
|
245
445
|
for (let i = 0; i < entries.length; i++) {
|
|
446
|
+
location = entryLocation(entries[i], 'entry', i, jsonlLocations.get(bundle)?.entryLines[i]);
|
|
246
447
|
const e = entries[i], b = e.body, a = e.counterSignature;
|
|
247
448
|
validateSchema('audit-entry', e);
|
|
248
449
|
if (b.taskId !== c.taskId ||
|
|
@@ -263,7 +464,184 @@ export function verifyAudit(bundle, expected) {
|
|
|
263
464
|
return auditResult(supplied);
|
|
264
465
|
}
|
|
265
466
|
catch (e) {
|
|
266
|
-
return auditResult(supplied, e);
|
|
467
|
+
return auditResult(supplied, verificationError(e) + '; auditLocation=' + diagnosticJson(location));
|
|
267
468
|
}
|
|
268
469
|
}
|
|
470
|
+
const interruptionLocations = new WeakMap();
|
|
471
|
+
function interruptionLocation(bundle) {
|
|
472
|
+
const signed = object(bundle?.interruption), p = object(signed?.payload);
|
|
473
|
+
const source = interruptionLocations.get(bundle);
|
|
474
|
+
const entries = Array.isArray(bundle?.entries) ? bundle.entries : [];
|
|
475
|
+
let interruptionHash;
|
|
476
|
+
try {
|
|
477
|
+
if (p)
|
|
478
|
+
interruptionHash = digest(p);
|
|
479
|
+
}
|
|
480
|
+
catch { /* Only expose a computed digest. */ }
|
|
481
|
+
return {
|
|
482
|
+
locationKind: 'interruption', taskId: diagnosticText(p?.taskId),
|
|
483
|
+
seq: typeof p?.seq === 'number' && Number.isSafeInteger(p.seq) && p.seq >= 0 ? p.seq : undefined,
|
|
484
|
+
interruptionHash, line: source?.terminalLine, verified: false,
|
|
485
|
+
lastEntry: entries.length ? entryLocation(entries.at(-1), 'last-entry', entries.length - 1, source?.entryLines.at(-1)) : undefined,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/** Read either terminal form, retaining physical JSONL locations out of band. */
|
|
489
|
+
export function parseAuditJsonl(text) {
|
|
490
|
+
const records = parseRecords(text);
|
|
491
|
+
// Keep the bilateral parser's diagnostics for old files, including missing checkpoints.
|
|
492
|
+
if (!records.some(({ record }) => object(record) && Object.hasOwn(record, 'interruption'))) {
|
|
493
|
+
const bilateral = parseJsonl(text);
|
|
494
|
+
const malformed = records.find(({ record }) => !object(record) || Object.keys(record).length !== 1 ||
|
|
495
|
+
!['entry', 'checkpoint'].includes(Object.keys(record)[0]));
|
|
496
|
+
if (!malformed)
|
|
497
|
+
return bilateral;
|
|
498
|
+
throw new Error('INVALID_AUDIT_RECORD; auditLocation=' + diagnosticJson({ line: malformed.line, verified: false }));
|
|
499
|
+
}
|
|
500
|
+
const bad = records.find(({ record }, index) => {
|
|
501
|
+
const r = object(record);
|
|
502
|
+
return !r || Object.keys(r).length !== 1 ||
|
|
503
|
+
(index === records.length - 1 ? !Object.hasOwn(r, 'interruption') : !Object.hasOwn(r, 'entry'));
|
|
504
|
+
});
|
|
505
|
+
if (bad)
|
|
506
|
+
throw new Error('MISPLACED_AUDIT_TERMINAL; auditLocation=' + diagnosticJson({ line: bad.line, verified: false }));
|
|
507
|
+
const bundle = { entries: records.slice(0, -1).map(({ record }) => record.entry), interruption: records.at(-1).record.interruption };
|
|
508
|
+
interruptionLocations.set(bundle, { entryLines: records.slice(0, -1).map(({ line }) => line), terminalLine: records.at(-1).line });
|
|
509
|
+
return bundle;
|
|
510
|
+
}
|
|
511
|
+
/** Verify a PROPOSE-only acknowledged prefix, including offline manifest roles/signature. */
|
|
512
|
+
function assertInterruptionPrefix(entries, taskId, actors) {
|
|
513
|
+
if (!Array.isArray(entries) || entries.length !== 1 || actors.length !== 2 || actors[0] === actors[1])
|
|
514
|
+
throw new Error('INTERRUPTION_SCOPE');
|
|
515
|
+
const e = entries[0];
|
|
516
|
+
validateSchema('audit-entry', e);
|
|
517
|
+
const b = e.body, a = e.counterSignature;
|
|
518
|
+
if (b.eventType !== 'PROPOSE' || b.taskId !== taskId || b.seq !== 0 || b.prevEntryHash !== null ||
|
|
519
|
+
b.actorDid !== actors[0] || b.receiverDid !== actors[1] || e.entryHash !== digest(b) ||
|
|
520
|
+
!verifySignature(b, e.actorSignature, actors[0]) || !verifySigned(a, actors[1]) ||
|
|
521
|
+
a.payload.type !== 'AckReceipt' || a.payload.taskId !== taskId || a.payload.entryHash !== e.entryHash ||
|
|
522
|
+
a.payload.actorDid !== actors[0] || a.payload.receiverDid !== actors[1])
|
|
523
|
+
throw new Error('entry 0');
|
|
524
|
+
const proposal = object(b.payload)?.proposal;
|
|
525
|
+
if (!proposal)
|
|
526
|
+
throw new Error('INTERRUPTION_MANIFEST');
|
|
527
|
+
validateManifest(proposal.payload);
|
|
528
|
+
if (!verifySigned(proposal, actors[0]) || proposal.payload.taskId !== taskId ||
|
|
529
|
+
proposal.payload.requester.ownerDid !== actors[0] || proposal.payload.worker?.ownerDid !== actors[1])
|
|
530
|
+
throw new Error('INTERRUPTION_MANIFEST');
|
|
531
|
+
// Deliberately no live expiry, retirement clock, or peer-floor policy in offline verification.
|
|
532
|
+
}
|
|
533
|
+
function evidencePins(options) {
|
|
534
|
+
const base = resolveAnchor(options);
|
|
535
|
+
for (const key of ['expectedClosureKind', 'expectedInterruptionHash']) {
|
|
536
|
+
if (!sameAnchorField(options?.[key], options?.trustAnchor?.[key]))
|
|
537
|
+
throw new Error('trust anchor');
|
|
538
|
+
}
|
|
539
|
+
return { ...base,
|
|
540
|
+
expectedClosureKind: options?.expectedClosureKind ?? options?.trustAnchor?.expectedClosureKind,
|
|
541
|
+
expectedInterruptionHash: options?.expectedInterruptionHash ?? options?.trustAnchor?.expectedInterruptionHash };
|
|
542
|
+
}
|
|
543
|
+
function assertInterruptionPins(options) {
|
|
544
|
+
if (options !== undefined && !object(options))
|
|
545
|
+
throw new Error('trust anchor');
|
|
546
|
+
if (options?.trustAnchor !== undefined && !object(options.trustAnchor))
|
|
547
|
+
throw new Error('trust anchor');
|
|
548
|
+
for (const part of [options, options?.trustAnchor]) {
|
|
549
|
+
if (!part)
|
|
550
|
+
continue;
|
|
551
|
+
for (const key of ['taskId', 'head', 'expectedLastEntryHash', 'expectedFinalState', 'expectedInterruptionHash'])
|
|
552
|
+
if (part[key] !== undefined && !hasText(part[key]))
|
|
553
|
+
throw new Error('trust anchor');
|
|
554
|
+
if (part.expectedClosureKind !== undefined && !['bilateral', 'interruption'].includes(part.expectedClosureKind))
|
|
555
|
+
throw new Error('trust anchor');
|
|
556
|
+
if (part.actors !== undefined && (!Array.isArray(part.actors) || part.actors.length !== 2 || !part.actors.every(hasText)))
|
|
557
|
+
throw new Error('trust anchor');
|
|
558
|
+
if (part.expectedEntryCount !== undefined && (!Number.isSafeInteger(part.expectedEntryCount) || part.expectedEntryCount < 0))
|
|
559
|
+
throw new Error('trust anchor');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/** Both formats are verified locally. A worker-only observation never becomes VALID. */
|
|
563
|
+
export function verifyAuditEvidence(bundle, expected) {
|
|
564
|
+
const value = object(bundle);
|
|
565
|
+
if (!value || !Object.hasOwn(value, 'interruption')) {
|
|
566
|
+
const bilateral = bundle;
|
|
567
|
+
const result = verifyAudit(bilateral, expected);
|
|
568
|
+
if (!result.valid)
|
|
569
|
+
return result;
|
|
570
|
+
try {
|
|
571
|
+
validateSchema('audit-evidence', bundle);
|
|
572
|
+
const pins = evidencePins(expected);
|
|
573
|
+
if (pins.expectedClosureKind !== undefined && pins.expectedClosureKind !== 'bilateral' ||
|
|
574
|
+
pins.expectedInterruptionHash !== undefined)
|
|
575
|
+
throw new Error('trust anchor');
|
|
576
|
+
return pins.expectedClosureKind === 'bilateral' ? { ...result, trustAnchorVerified: true } : result;
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
return auditResult(anchorSupplied(expected), verificationError(error) + '; auditLocation=' + diagnosticJson(checkpointLocation(bilateral)));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const interrupted = bundle;
|
|
583
|
+
let prefixVerified = false, terminalSignatureVerified = false, truncationChecked = false;
|
|
584
|
+
let location = interruptionLocation(interrupted);
|
|
585
|
+
try {
|
|
586
|
+
// Exclusive object shape and terminal schema before interpreting its fields.
|
|
587
|
+
if (Object.keys(value).length !== 2 || !Object.hasOwn(value, 'entries'))
|
|
588
|
+
throw new Error('INVALID_AUDIT_EVIDENCE');
|
|
589
|
+
validateSchema('audit-interruption', interrupted.interruption);
|
|
590
|
+
const { entries, interruption } = interrupted, p = interruption.payload;
|
|
591
|
+
location = entryLocation(entries?.[0], 'entry', 0, interruptionLocations.get(interrupted)?.entryLines[0]);
|
|
592
|
+
assertInterruptionPrefix(entries, p.taskId, p.actors);
|
|
593
|
+
prefixVerified = true;
|
|
594
|
+
location = interruptionLocation(interrupted);
|
|
595
|
+
const prefix = p.acknowledgedPrefix, head = entries[0].entryHash;
|
|
596
|
+
if (p.formatVersion !== 1 || p.seq !== 1 || prefix.count !== 1 || prefix.lastSeq !== 0 ||
|
|
597
|
+
p.prevEntryHash !== head || prefix.head !== head || prefix.entriesDigest !== digest(entries) ||
|
|
598
|
+
!Number.isSafeInteger(p.seq) || !Number.isSafeInteger(prefix.count) || !Number.isSafeInteger(prefix.lastSeq) ||
|
|
599
|
+
p.observedAt !== interruption.signature.signedAt || !Number.isFinite(Date.parse(p.observedAt)) ||
|
|
600
|
+
!verifySigned(interruption, p.actors[1]))
|
|
601
|
+
throw new Error('INVALID_AUDIT_INTERRUPTION');
|
|
602
|
+
terminalSignatureVerified = true;
|
|
603
|
+
assertInterruptionPins(expected);
|
|
604
|
+
const pins = evidencePins(expected);
|
|
605
|
+
const prefixCheckpoint = { type: 'AuditCheckpoint', taskId: p.taskId, actors: p.actors, count: 1, head };
|
|
606
|
+
assertTrustAnchor({ taskId: pins.taskId, actors: pins.actors }, prefixCheckpoint, entries);
|
|
607
|
+
if (pins.expectedClosureKind !== undefined && pins.expectedClosureKind !== 'interruption')
|
|
608
|
+
throw new Error('trust anchor');
|
|
609
|
+
if ([pins.head, pins.expectedLastEntryHash, pins.expectedFinalState, pins.expectedEntryCount].some(v => v !== undefined)) {
|
|
610
|
+
truncationChecked = true;
|
|
611
|
+
assertTrustAnchor(pins, prefixCheckpoint, entries);
|
|
612
|
+
}
|
|
613
|
+
if (pins.expectedInterruptionHash !== undefined) {
|
|
614
|
+
truncationChecked = true;
|
|
615
|
+
if (pins.expectedInterruptionHash !== digest(p))
|
|
616
|
+
throw new Error('trust anchor');
|
|
617
|
+
}
|
|
618
|
+
const supplied = anchorSupplied(expected) || pins.expectedClosureKind !== undefined || pins.expectedInterruptionHash !== undefined;
|
|
619
|
+
return { valid: false, status: 'UNILATERALLY_CLOSED',
|
|
620
|
+
statusLabel: 'UNILATERALLY_CLOSED (worker-only terminal; no peer terminal signature)', reason: 'WORKER_ONLY_INTERRUPTION',
|
|
621
|
+
prefixVerified, terminalSignatureVerified, bilateralCheckpointVerified: false,
|
|
622
|
+
trustAnchorVerified: supplied, truncationChecked, closureKind: 'interruption' };
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
return { valid: false, status: 'INVALID', statusLabel: 'INVALID',
|
|
626
|
+
reason: verificationError(error) + '; auditLocation=' + diagnosticJson(location),
|
|
627
|
+
prefixVerified, terminalSignatureVerified, bilateralCheckpointVerified: false,
|
|
628
|
+
trustAnchorVerified: false, truncationChecked, closureKind: 'interruption' };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/** Creates a worker observation; callers must preserve the original prefix and journal the intent before writing. */
|
|
632
|
+
export function createAuditInterruption(entries, worker, phase, reason, observedAt = new Date().toISOString()) {
|
|
633
|
+
const first = entries[0];
|
|
634
|
+
if (!first)
|
|
635
|
+
throw new Error('INTERRUPTION_SCOPE');
|
|
636
|
+
const actors = [first.body.actorDid, worker.did];
|
|
637
|
+
assertInterruptionPrefix(entries, first.body.taskId, actors);
|
|
638
|
+
const payload = { type: 'AuditInterruption', formatVersion: 1,
|
|
639
|
+
taskId: first.body.taskId, actors, seq: 1, prevEntryHash: first.entryHash,
|
|
640
|
+
acknowledgedPrefix: { count: 1, lastSeq: 0, head: first.entryHash, entriesDigest: digest(entries) },
|
|
641
|
+
phase, reason, observedAt };
|
|
642
|
+
const interruption = signPayload(payload, worker, observedAt);
|
|
643
|
+
if (verifyAuditEvidence({ entries, interruption }).status !== 'UNILATERALLY_CLOSED')
|
|
644
|
+
throw new Error('INVALID_AUDIT_INTERRUPTION');
|
|
645
|
+
return interruption;
|
|
646
|
+
}
|
|
269
647
|
//# sourceMappingURL=index.js.map
|