@cello-protocol/daemon 0.0.3 → 0.0.4
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/agent-loader.d.ts +41 -0
- package/dist/agent-loader.d.ts.map +1 -0
- package/dist/agent-loader.js +94 -0
- package/dist/agent-loader.js.map +1 -0
- package/dist/bin/cello-daemon.d.ts +13 -0
- package/dist/bin/cello-daemon.d.ts.map +1 -0
- package/dist/bin/cello-daemon.js +170 -0
- package/dist/bin/cello-daemon.js.map +1 -0
- package/dist/cello-node-transport-dialer.d.ts +59 -0
- package/dist/cello-node-transport-dialer.d.ts.map +1 -0
- package/dist/cello-node-transport-dialer.js +108 -0
- package/dist/cello-node-transport-dialer.js.map +1 -0
- package/dist/challenge-verifier.d.ts +12 -0
- package/dist/challenge-verifier.d.ts.map +1 -0
- package/dist/challenge-verifier.js +11 -0
- package/dist/challenge-verifier.js.map +1 -0
- package/dist/connect-or-start.d.ts +25 -0
- package/dist/connect-or-start.d.ts.map +1 -0
- package/dist/connect-or-start.js +117 -0
- package/dist/connect-or-start.js.map +1 -0
- package/dist/content-park-client.d.ts +49 -0
- package/dist/content-park-client.d.ts.map +1 -0
- package/dist/content-park-client.js +196 -0
- package/dist/content-park-client.js.map +1 -0
- package/dist/daemon.d.ts +65 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +3202 -0
- package/dist/daemon.js.map +1 -0
- package/dist/directory-bootstrap.d.ts +55 -0
- package/dist/directory-bootstrap.d.ts.map +1 -0
- package/dist/directory-bootstrap.js +102 -0
- package/dist/directory-bootstrap.js.map +1 -0
- package/dist/file-manifest-provider.d.ts +18 -0
- package/dist/file-manifest-provider.d.ts.map +1 -0
- package/dist/file-manifest-provider.js +72 -0
- package/dist/file-manifest-provider.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc-client.d.ts +31 -0
- package/dist/ipc-client.d.ts.map +1 -0
- package/dist/ipc-client.js +112 -0
- package/dist/ipc-client.js.map +1 -0
- package/dist/ipc-server.d.ts +49 -0
- package/dist/ipc-server.d.ts.map +1 -0
- package/dist/ipc-server.js +268 -0
- package/dist/ipc-server.js.map +1 -0
- package/dist/lock-file.d.ts +27 -0
- package/dist/lock-file.d.ts.map +1 -0
- package/dist/lock-file.js +84 -0
- package/dist/lock-file.js.map +1 -0
- package/dist/manifest-loader.d.ts +33 -0
- package/dist/manifest-loader.d.ts.map +1 -0
- package/dist/manifest-loader.js +70 -0
- package/dist/manifest-loader.js.map +1 -0
- package/dist/manifest-poll-scheduler.d.ts +31 -0
- package/dist/manifest-poll-scheduler.d.ts.map +1 -0
- package/dist/manifest-poll-scheduler.js +59 -0
- package/dist/manifest-poll-scheduler.js.map +1 -0
- package/dist/manifest-version-store-file.d.ts +18 -0
- package/dist/manifest-version-store-file.d.ts.map +1 -0
- package/dist/manifest-version-store-file.js +40 -0
- package/dist/manifest-version-store-file.js.map +1 -0
- package/dist/manifest-version-store.d.ts +14 -0
- package/dist/manifest-version-store.d.ts.map +1 -0
- package/dist/manifest-version-store.js +13 -0
- package/dist/manifest-version-store.js.map +1 -0
- package/dist/network-directory-node.d.ts +94 -0
- package/dist/network-directory-node.d.ts.map +1 -0
- package/dist/network-directory-node.js +626 -0
- package/dist/network-directory-node.js.map +1 -0
- package/dist/nonce-dedup.d.ts +68 -0
- package/dist/nonce-dedup.d.ts.map +1 -0
- package/dist/nonce-dedup.js +204 -0
- package/dist/nonce-dedup.js.map +1 -0
- package/dist/notification-dispatcher.d.ts +65 -0
- package/dist/notification-dispatcher.d.ts.map +1 -0
- package/dist/notification-dispatcher.js +138 -0
- package/dist/notification-dispatcher.js.map +1 -0
- package/dist/registration-context.d.ts +69 -0
- package/dist/registration-context.d.ts.map +1 -0
- package/dist/registration-context.js +118 -0
- package/dist/registration-context.js.map +1 -0
- package/dist/registration-manager.d.ts +72 -0
- package/dist/registration-manager.d.ts.map +1 -0
- package/dist/registration-manager.js +267 -0
- package/dist/registration-manager.js.map +1 -0
- package/dist/registration-persistence.d.ts +131 -0
- package/dist/registration-persistence.d.ts.map +1 -0
- package/dist/registration-persistence.js +233 -0
- package/dist/registration-persistence.js.map +1 -0
- package/dist/retry-queue.d.ts +144 -0
- package/dist/retry-queue.d.ts.map +1 -0
- package/dist/retry-queue.js +444 -0
- package/dist/retry-queue.js.map +1 -0
- package/dist/seal-frontier-verify.d.ts +58 -0
- package/dist/seal-frontier-verify.d.ts.map +1 -0
- package/dist/seal-frontier-verify.js +87 -0
- package/dist/seal-frontier-verify.js.map +1 -0
- package/dist/seal-legibility-tbs.d.ts +25 -0
- package/dist/seal-legibility-tbs.d.ts.map +1 -0
- package/dist/seal-legibility-tbs.js +78 -0
- package/dist/seal-legibility-tbs.js.map +1 -0
- package/dist/seal-upgrade.d.ts +90 -0
- package/dist/seal-upgrade.d.ts.map +1 -0
- package/dist/seal-upgrade.js +178 -0
- package/dist/seal-upgrade.js.map +1 -0
- package/dist/session-assignment-parser.d.ts +22 -0
- package/dist/session-assignment-parser.d.ts.map +1 -0
- package/dist/session-assignment-parser.js +139 -0
- package/dist/session-assignment-parser.js.map +1 -0
- package/dist/session-ceremony.d.ts +156 -0
- package/dist/session-ceremony.d.ts.map +1 -0
- package/dist/session-ceremony.js +447 -0
- package/dist/session-ceremony.js.map +1 -0
- package/dist/session-connection-gater.d.ts +91 -0
- package/dist/session-connection-gater.d.ts.map +1 -0
- package/dist/session-connection-gater.js +146 -0
- package/dist/session-connection-gater.js.map +1 -0
- package/dist/session-node-manager.d.ts +585 -0
- package/dist/session-node-manager.d.ts.map +1 -0
- package/dist/session-node-manager.js +2609 -0
- package/dist/session-node-manager.js.map +1 -0
- package/dist/session-relay-client.d.ts +101 -0
- package/dist/session-relay-client.d.ts.map +1 -0
- package/dist/session-relay-client.js +520 -0
- package/dist/session-relay-client.js.map +1 -0
- package/dist/session-tree.d.ts +80 -0
- package/dist/session-tree.d.ts.map +1 -0
- package/dist/session-tree.js +123 -0
- package/dist/session-tree.js.map +1 -0
- package/dist/signaling-connect.d.ts +83 -0
- package/dist/signaling-connect.d.ts.map +1 -0
- package/dist/signaling-connect.js +266 -0
- package/dist/signaling-connect.js.map +1 -0
- package/dist/transcript-cipher.d.ts +31 -0
- package/dist/transcript-cipher.d.ts.map +1 -0
- package/dist/transcript-cipher.js +74 -0
- package/dist/transcript-cipher.js.map +1 -0
- package/dist/transport-composition.d.ts +31 -0
- package/dist/transport-composition.d.ts.map +1 -0
- package/dist/transport-composition.js +55 -0
- package/dist/transport-composition.js.map +1 -0
- package/dist/transport-selector.d.ts +189 -0
- package/dist/transport-selector.d.ts.map +1 -0
- package/dist/transport-selector.js +195 -0
- package/dist/transport-selector.js.map +1 -0
- package/dist/types.d.ts +265 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +33 -0
- package/dist/types.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CELLO Daemon — RetryQueue
|
|
3
|
+
*
|
|
4
|
+
* Per-session FIFO queue of messages awaiting delivery. When a send over a
|
|
5
|
+
* session node stream fails, the message envelope is added to the queue. When
|
|
6
|
+
* the peer reconnects, the daemon drains the queue in FIFO order.
|
|
7
|
+
*
|
|
8
|
+
* Persistence: SQLCipher table `retry_queue`.
|
|
9
|
+
* Cap: 1,000 messages per session. On overflow the OLDEST is evicted.
|
|
10
|
+
*
|
|
11
|
+
* Pseudocode (SPARC Phase P):
|
|
12
|
+
*
|
|
13
|
+
* 1. constructor(db, logger):
|
|
14
|
+
* - Store db handle and logger reference
|
|
15
|
+
* - Initialize empty per-session Map<sessionId, RetryQueueEntry[]>
|
|
16
|
+
* - Create retry_queue table IF NOT EXISTS
|
|
17
|
+
* - Create index on (session_id, position ASC)
|
|
18
|
+
*
|
|
19
|
+
* 2. loadFromDb():
|
|
20
|
+
* - SELECT all rows from retry_queue ordered by session_id, position ASC
|
|
21
|
+
* - Group into per-session arrays of RetryQueueEntry
|
|
22
|
+
* - Store in in-memory Map
|
|
23
|
+
* - Track per-session position counters (MAX position per session)
|
|
24
|
+
*
|
|
25
|
+
* 3. enqueue(sessionId, nonce: Uint8Array, contentBlob: Uint8Array):
|
|
26
|
+
* - Convert nonce to hex via Buffer.from(nonce).toString('hex')
|
|
27
|
+
* - If session queue size >= 1000: evict oldest (lowest position)
|
|
28
|
+
* - DELETE from retry_queue WHERE session_id AND position = min
|
|
29
|
+
* - Log message.retry.evicted WARN with evicted nonce hex
|
|
30
|
+
* - Remove from in-memory array
|
|
31
|
+
* - Compute next position: max(existing positions) + 1
|
|
32
|
+
* - INSERT into retry_queue (session_id, nonce_hex, content_blob, queued_at, attempts, position)
|
|
33
|
+
* - Add to in-memory array
|
|
34
|
+
* - Log message.retry.queued INFO
|
|
35
|
+
* - Return queueDepth
|
|
36
|
+
*
|
|
37
|
+
* 4. drainSession(sessionId, sendFn):
|
|
38
|
+
* - Get entries for session in FIFO order (ascending position)
|
|
39
|
+
* - For each entry:
|
|
40
|
+
* a. Call sendFn(entry.contentBlob) → await result
|
|
41
|
+
* b. If success: DELETE from retry_queue, remove from memory,
|
|
42
|
+
* log message.retry.delivered INFO with attemptsTotal
|
|
43
|
+
* c. If failure: HALT immediately. Do not attempt remaining entries.
|
|
44
|
+
* FIFO invariant: M3 must not arrive before M2.
|
|
45
|
+
* - Return count of successfully delivered messages
|
|
46
|
+
*
|
|
47
|
+
* 5. getTotalDepth():
|
|
48
|
+
* - Sum sizes of all per-session arrays
|
|
49
|
+
* - Return integer >= 0
|
|
50
|
+
*
|
|
51
|
+
* 6. getSessionDepth(sessionId):
|
|
52
|
+
* - Return length of session's array (0 if absent)
|
|
53
|
+
*/
|
|
54
|
+
/** Cap per outline.md Resource Caps. */
|
|
55
|
+
export const RETRY_QUEUE_CAP = 1000;
|
|
56
|
+
export class RetryQueue {
|
|
57
|
+
#db;
|
|
58
|
+
#logger;
|
|
59
|
+
// DOD-LOG-1: when provided, content_blob is AES-256-GCM encrypted at rest with the SAME key as
|
|
60
|
+
// the transcript — closing the gap where queued message plaintext sat in cleartext in the DB.
|
|
61
|
+
// Optional for backward-compat with direct-construction tests (they store plaintext, as before).
|
|
62
|
+
#cipher;
|
|
63
|
+
/** Per-session FIFO arrays of DIRECT-resend entries, ordered by position ascending. */
|
|
64
|
+
#queues = new Map();
|
|
65
|
+
/** Per-session position counter for monotonic increment. */
|
|
66
|
+
#positionCounters = new Map();
|
|
67
|
+
/** CELLO-M7-MSG-001: per-session FIFO arrays of awaiting-ACK content (park target). */
|
|
68
|
+
#awaiting = new Map();
|
|
69
|
+
constructor(db, logger, cipher) {
|
|
70
|
+
this.#db = db;
|
|
71
|
+
this.#logger = logger;
|
|
72
|
+
this.#cipher = cipher;
|
|
73
|
+
// Create table + index if not exists (inline migration — not Flyway)
|
|
74
|
+
this.#db.exec(`
|
|
75
|
+
CREATE TABLE IF NOT EXISTS retry_queue (
|
|
76
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
session_id TEXT NOT NULL,
|
|
78
|
+
nonce_hex TEXT NOT NULL,
|
|
79
|
+
content_blob BLOB NOT NULL,
|
|
80
|
+
queued_at INTEGER NOT NULL,
|
|
81
|
+
attempts INTEGER NOT NULL DEFAULT 1,
|
|
82
|
+
position INTEGER NOT NULL,
|
|
83
|
+
UNIQUE(session_id, nonce_hex)
|
|
84
|
+
)
|
|
85
|
+
`);
|
|
86
|
+
this.#db.exec(`
|
|
87
|
+
CREATE INDEX IF NOT EXISTS retry_queue_by_session_position
|
|
88
|
+
ON retry_queue(session_id, position ASC)
|
|
89
|
+
`);
|
|
90
|
+
// CELLO-M7-MSG-001 (AC-005): extend the SAME table with awaiting-ACK content state.
|
|
91
|
+
// No new table — guarded ALTERs make this idempotent across client upgrades.
|
|
92
|
+
const cols = this.#db.prepare("PRAGMA table_info(retry_queue)").all();
|
|
93
|
+
const hasCol = (n) => cols.some((c) => c.name === n);
|
|
94
|
+
if (!hasCol("awaiting_ack")) {
|
|
95
|
+
this.#db.exec("ALTER TABLE retry_queue ADD COLUMN awaiting_ack INTEGER NOT NULL DEFAULT 0");
|
|
96
|
+
}
|
|
97
|
+
if (!hasCol("content_hash_hex")) {
|
|
98
|
+
this.#db.exec("ALTER TABLE retry_queue ADD COLUMN content_hash_hex TEXT");
|
|
99
|
+
}
|
|
100
|
+
// DOD-LOOP-1: the OWNING agent for awaiting-ACK content, so two of the operator's agents can
|
|
101
|
+
// hold awaiting content for the SAME session_id on one daemon without colliding. NULL for
|
|
102
|
+
// legacy direct-retry rows; awaiting rows always set it.
|
|
103
|
+
if (!hasCol("agent_name")) {
|
|
104
|
+
this.#db.exec("ALTER TABLE retry_queue ADD COLUMN agent_name TEXT");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** DOD-LOOP-1: composite key for the awaiting-ACK map — (agentName, sessionId). */
|
|
108
|
+
#ak(agentName, sessionId) {
|
|
109
|
+
return `${agentName}\x1f${sessionId}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Load all retry queue entries from SQLCipher into memory.
|
|
113
|
+
* Must complete BEFORE the IPC socket opens (AC-007).
|
|
114
|
+
*/
|
|
115
|
+
loadFromDb() {
|
|
116
|
+
const rows = this.#db
|
|
117
|
+
.prepare("SELECT session_id, agent_name, nonce_hex, content_blob, queued_at, attempts, position, awaiting_ack, content_hash_hex FROM retry_queue ORDER BY session_id, position ASC")
|
|
118
|
+
.all();
|
|
119
|
+
this.#queues.clear();
|
|
120
|
+
this.#positionCounters.clear();
|
|
121
|
+
this.#awaiting.clear();
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
// Track max position per session across BOTH direct and awaiting entries.
|
|
124
|
+
const currentMax = this.#positionCounters.get(row.session_id) ?? 0;
|
|
125
|
+
if (row.position > currentMax)
|
|
126
|
+
this.#positionCounters.set(row.session_id, row.position);
|
|
127
|
+
if (row.awaiting_ack === 1) {
|
|
128
|
+
// CELLO-M7-MSG-001 (AC-004): un-acked content to re-park at startup. DOD-LOOP-1: keyed by
|
|
129
|
+
// the OWNING agent (legacy rows with no agent_name fall back to "" — single-agent behavior).
|
|
130
|
+
const agentName = row.agent_name ?? "";
|
|
131
|
+
const entry = {
|
|
132
|
+
agentName,
|
|
133
|
+
sessionId: row.session_id,
|
|
134
|
+
contentHashHex: row.content_hash_hex ?? row.nonce_hex,
|
|
135
|
+
contentBlob: this.#openBlob(Uint8Array.from(row.content_blob)),
|
|
136
|
+
queuedAt: row.queued_at,
|
|
137
|
+
position: row.position,
|
|
138
|
+
};
|
|
139
|
+
const ak = this.#ak(agentName, entry.sessionId);
|
|
140
|
+
let q = this.#awaiting.get(ak);
|
|
141
|
+
if (!q) {
|
|
142
|
+
q = [];
|
|
143
|
+
this.#awaiting.set(ak, q);
|
|
144
|
+
}
|
|
145
|
+
q.push(entry);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const entry = {
|
|
149
|
+
sessionId: row.session_id,
|
|
150
|
+
nonceHex: row.nonce_hex,
|
|
151
|
+
contentBlob: this.#openBlob(Uint8Array.from(row.content_blob)),
|
|
152
|
+
queuedAt: row.queued_at,
|
|
153
|
+
attempts: row.attempts,
|
|
154
|
+
position: row.position,
|
|
155
|
+
};
|
|
156
|
+
let sessionQueue = this.#queues.get(entry.sessionId);
|
|
157
|
+
if (!sessionQueue) {
|
|
158
|
+
sessionQueue = [];
|
|
159
|
+
this.#queues.set(entry.sessionId, sessionQueue);
|
|
160
|
+
}
|
|
161
|
+
sessionQueue.push(entry);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Enqueue a failed message for later retry.
|
|
166
|
+
* Evicts the oldest entry if cap is reached.
|
|
167
|
+
*/
|
|
168
|
+
/** DOD-LOG-1: encrypt the content blob at rest when a cipher is configured (else passthrough). */
|
|
169
|
+
#sealBlob(b) {
|
|
170
|
+
return this.#cipher ? this.#cipher.encrypt(b) : b;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* DOD-LOG-1: decrypt a stored content blob. If no cipher is configured, or the blob is a legacy
|
|
174
|
+
* PLAINTEXT row written before at-rest encryption (decrypt returns null), fall back to the raw
|
|
175
|
+
* bytes — so an upgrade reads old rows without crashing while new rows are encrypted.
|
|
176
|
+
*/
|
|
177
|
+
#openBlob(b) {
|
|
178
|
+
if (!this.#cipher)
|
|
179
|
+
return b;
|
|
180
|
+
return this.#cipher.decrypt(b) ?? b;
|
|
181
|
+
}
|
|
182
|
+
enqueue(sessionId, nonce, contentBlob) {
|
|
183
|
+
const nonceHex = Buffer.from(nonce).toString("hex");
|
|
184
|
+
let sessionQueue = this.#queues.get(sessionId);
|
|
185
|
+
if (!sessionQueue) {
|
|
186
|
+
sessionQueue = [];
|
|
187
|
+
this.#queues.set(sessionId, sessionQueue);
|
|
188
|
+
}
|
|
189
|
+
// Cap enforcement: evict oldest if at limit
|
|
190
|
+
if (sessionQueue.length >= RETRY_QUEUE_CAP) {
|
|
191
|
+
const oldest = sessionQueue[0];
|
|
192
|
+
// Delete from DB
|
|
193
|
+
try {
|
|
194
|
+
this.#db
|
|
195
|
+
.prepare("DELETE FROM retry_queue WHERE session_id = ? AND position = ?")
|
|
196
|
+
.run(sessionId, oldest.position);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
this.#logger.error("message.retry.persist.failed", {
|
|
200
|
+
sessionId,
|
|
201
|
+
nonce: oldest.nonceHex,
|
|
202
|
+
error: err instanceof Error ? err.message : String(err),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Log eviction BEFORE the new entry is logged (per story spec)
|
|
206
|
+
this.#logger.warn("message.retry.evicted", {
|
|
207
|
+
sessionId,
|
|
208
|
+
nonce: oldest.nonceHex,
|
|
209
|
+
queueDepth: RETRY_QUEUE_CAP,
|
|
210
|
+
});
|
|
211
|
+
// Remove from in-memory
|
|
212
|
+
sessionQueue.shift();
|
|
213
|
+
}
|
|
214
|
+
// Compute next position
|
|
215
|
+
const currentMax = this.#positionCounters.get(sessionId) ?? 0;
|
|
216
|
+
const nextPosition = currentMax + 1;
|
|
217
|
+
this.#positionCounters.set(sessionId, nextPosition);
|
|
218
|
+
const queuedAt = Date.now();
|
|
219
|
+
const entry = {
|
|
220
|
+
sessionId,
|
|
221
|
+
nonceHex,
|
|
222
|
+
contentBlob,
|
|
223
|
+
queuedAt,
|
|
224
|
+
attempts: 1,
|
|
225
|
+
position: nextPosition,
|
|
226
|
+
};
|
|
227
|
+
// Persist to SQLCipher
|
|
228
|
+
try {
|
|
229
|
+
this.#db
|
|
230
|
+
.prepare(`INSERT INTO retry_queue (session_id, nonce_hex, content_blob, queued_at, attempts, position)
|
|
231
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
232
|
+
.run(sessionId, nonceHex, Buffer.from(this.#sealBlob(contentBlob)), queuedAt, 1, nextPosition);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
this.#logger.error("message.retry.persist.failed", {
|
|
236
|
+
sessionId,
|
|
237
|
+
nonce: nonceHex,
|
|
238
|
+
error: err instanceof Error ? err.message : String(err),
|
|
239
|
+
});
|
|
240
|
+
// Message stays in memory only (DB-001 fallback)
|
|
241
|
+
}
|
|
242
|
+
sessionQueue.push(entry);
|
|
243
|
+
this.#logger.info("message.retry.queued", {
|
|
244
|
+
sessionId,
|
|
245
|
+
nonce: nonceHex,
|
|
246
|
+
queueDepth: sessionQueue.length,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Drain the session's retry queue in FIFO order.
|
|
251
|
+
* Halts immediately on first failure (FIFO invariant: no out-of-order delivery).
|
|
252
|
+
* Returns the number of successfully delivered messages.
|
|
253
|
+
*/
|
|
254
|
+
async drainSession(sessionId, sendFn) {
|
|
255
|
+
const sessionQueue = this.#queues.get(sessionId);
|
|
256
|
+
if (!sessionQueue || sessionQueue.length === 0) {
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
let delivered = 0;
|
|
260
|
+
// Drain in FIFO order (array is already sorted by position ascending)
|
|
261
|
+
while (sessionQueue.length > 0) {
|
|
262
|
+
const entry = sessionQueue[0];
|
|
263
|
+
const result = await sendFn(entry.contentBlob);
|
|
264
|
+
if (!result.delivered) {
|
|
265
|
+
// Increment attempts on failure so attemptsTotal reflects actual tries
|
|
266
|
+
entry.attempts++;
|
|
267
|
+
try {
|
|
268
|
+
this.#db
|
|
269
|
+
.prepare("UPDATE retry_queue SET attempts = ? WHERE session_id = ? AND nonce_hex = ?")
|
|
270
|
+
.run(entry.attempts, sessionId, entry.nonceHex);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
this.#logger.error("message.retry.persist.failed", {
|
|
274
|
+
sessionId,
|
|
275
|
+
nonce: entry.nonceHex,
|
|
276
|
+
error: err instanceof Error ? err.message : String(err),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
// Halt immediately — FIFO invariant (AC-015)
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
// Success: delete from DB
|
|
283
|
+
try {
|
|
284
|
+
this.#db
|
|
285
|
+
.prepare("DELETE FROM retry_queue WHERE session_id = ? AND nonce_hex = ?")
|
|
286
|
+
.run(sessionId, entry.nonceHex);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
this.#logger.error("message.retry.persist.failed", {
|
|
290
|
+
sessionId,
|
|
291
|
+
nonce: entry.nonceHex,
|
|
292
|
+
error: err instanceof Error ? err.message : String(err),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// Remove from memory
|
|
296
|
+
sessionQueue.shift();
|
|
297
|
+
// Increment attempts for logging
|
|
298
|
+
const attemptsTotal = entry.attempts + 1;
|
|
299
|
+
this.#logger.info("message.retry.delivered", {
|
|
300
|
+
sessionId,
|
|
301
|
+
nonce: entry.nonceHex,
|
|
302
|
+
attemptsTotal,
|
|
303
|
+
});
|
|
304
|
+
delivered++;
|
|
305
|
+
}
|
|
306
|
+
// Clean up empty queue entry
|
|
307
|
+
if (sessionQueue.length === 0) {
|
|
308
|
+
this.#queues.delete(sessionId);
|
|
309
|
+
}
|
|
310
|
+
return delivered;
|
|
311
|
+
}
|
|
312
|
+
/** Total retry queue depth across all sessions. */
|
|
313
|
+
getTotalDepth() {
|
|
314
|
+
let total = 0;
|
|
315
|
+
for (const queue of this.#queues.values()) {
|
|
316
|
+
total += queue.length;
|
|
317
|
+
}
|
|
318
|
+
return total;
|
|
319
|
+
}
|
|
320
|
+
/** Retry queue depth for a specific session. */
|
|
321
|
+
getSessionDepth(sessionId) {
|
|
322
|
+
return this.#queues.get(sessionId)?.length ?? 0;
|
|
323
|
+
}
|
|
324
|
+
/** Get all entries for a session (FIFO ordered, defensive copy). Used by drain_session IPC. */
|
|
325
|
+
getSessionEntries(sessionId) {
|
|
326
|
+
const queue = this.#queues.get(sessionId);
|
|
327
|
+
return queue ? [...queue] : [];
|
|
328
|
+
}
|
|
329
|
+
// ─── CELLO-M7-MSG-001 (AC-004/AC-005/AC-019): awaiting-ACK content (park target) ──
|
|
330
|
+
/**
|
|
331
|
+
* Record un-acked content awaiting a `persisted` delivery ACK (TTF-trigger path,
|
|
332
|
+
* AC-005). Persisted to the SAME retry_queue table (awaiting_ack = 1) so a crash
|
|
333
|
+
* before the relay park confirms is recoverable at startup (AC-004). Idempotent on
|
|
334
|
+
* (sessionId, contentHash).
|
|
335
|
+
*/
|
|
336
|
+
enqueueAwaitingContent(agentName, sessionId, contentHash, contentBlob) {
|
|
337
|
+
const contentHashHex = Buffer.from(contentHash).toString("hex");
|
|
338
|
+
const ak = this.#ak(agentName, sessionId);
|
|
339
|
+
let q = this.#awaiting.get(ak);
|
|
340
|
+
if (!q) {
|
|
341
|
+
q = [];
|
|
342
|
+
this.#awaiting.set(ak, q);
|
|
343
|
+
}
|
|
344
|
+
if (q.some((e) => e.contentHashHex === contentHashHex))
|
|
345
|
+
return; // idempotent
|
|
346
|
+
const currentMax = this.#positionCounters.get(ak) ?? 0;
|
|
347
|
+
const position = currentMax + 1;
|
|
348
|
+
this.#positionCounters.set(ak, position);
|
|
349
|
+
const queuedAt = Date.now();
|
|
350
|
+
try {
|
|
351
|
+
this.#db
|
|
352
|
+
.prepare(`INSERT INTO retry_queue (session_id, agent_name, nonce_hex, content_blob, queued_at, attempts, position, awaiting_ack, content_hash_hex)
|
|
353
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)`)
|
|
354
|
+
.run(sessionId, agentName, contentHashHex, Buffer.from(this.#sealBlob(contentBlob)), queuedAt, 1, position, contentHashHex);
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
this.#logger.error("message.retry.persist.failed", {
|
|
358
|
+
sessionId,
|
|
359
|
+
nonce: contentHashHex,
|
|
360
|
+
error: err instanceof Error ? err.message : String(err),
|
|
361
|
+
});
|
|
362
|
+
// In-memory only (DB-001 fallback) — still re-parkable this run.
|
|
363
|
+
}
|
|
364
|
+
q.push({ agentName, sessionId, contentHashHex, contentBlob, queuedAt, position });
|
|
365
|
+
}
|
|
366
|
+
/** Remove an awaiting-ACK entry once its `persisted` ACK arrives. */
|
|
367
|
+
markContentAcked(agentName, sessionId, contentHash) {
|
|
368
|
+
const contentHashHex = Buffer.from(contentHash).toString("hex");
|
|
369
|
+
const ak = this.#ak(agentName, sessionId);
|
|
370
|
+
const q = this.#awaiting.get(ak);
|
|
371
|
+
if (q) {
|
|
372
|
+
const idx = q.findIndex((e) => e.contentHashHex === contentHashHex);
|
|
373
|
+
if (idx !== -1)
|
|
374
|
+
q.splice(idx, 1);
|
|
375
|
+
if (q.length === 0)
|
|
376
|
+
this.#awaiting.delete(ak);
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
this.#db
|
|
380
|
+
.prepare("DELETE FROM retry_queue WHERE agent_name = ? AND session_id = ? AND content_hash_hex = ? AND awaiting_ack = 1")
|
|
381
|
+
.run(agentName, sessionId, contentHashHex);
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
this.#logger.error("message.retry.persist.failed", {
|
|
385
|
+
sessionId, nonce: contentHashHex, error: err instanceof Error ? err.message : String(err),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** Awaiting-ACK depth for a session (un-acked content count). */
|
|
390
|
+
getAwaitingDepth(agentName, sessionId) {
|
|
391
|
+
return this.#awaiting.get(this.#ak(agentName, sessionId))?.length ?? 0;
|
|
392
|
+
}
|
|
393
|
+
/** All (agent, session) pairs that currently hold awaiting-ACK content (for the startup flush). */
|
|
394
|
+
getAwaitingSessions() {
|
|
395
|
+
const out = [];
|
|
396
|
+
for (const q of this.#awaiting.values()) {
|
|
397
|
+
if (q.length > 0)
|
|
398
|
+
out.push({ agentName: q[0].agentName, sessionId: q[0].sessionId });
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Drain a session's awaiting-ACK content to the relay park target (AC-005). Each item
|
|
404
|
+
* is independent — unlike the direct FIFO resend, a park failure does NOT halt the
|
|
405
|
+
* rest; the failed item stays queued for the next reconnect / startup flush (DB-001,
|
|
406
|
+
* AC-019). Returns the number of entries successfully parked.
|
|
407
|
+
*/
|
|
408
|
+
async drainAwaitingToPark(agentName, sessionId, parkFn) {
|
|
409
|
+
const ak = this.#ak(agentName, sessionId);
|
|
410
|
+
const q = this.#awaiting.get(ak);
|
|
411
|
+
if (!q || q.length === 0)
|
|
412
|
+
return 0;
|
|
413
|
+
let parked = 0;
|
|
414
|
+
for (const entry of [...q]) {
|
|
415
|
+
let result;
|
|
416
|
+
try {
|
|
417
|
+
result = await parkFn(entry);
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
result = { parked: false, error: err instanceof Error ? err.message : String(err) };
|
|
421
|
+
}
|
|
422
|
+
if (!result.parked)
|
|
423
|
+
continue; // keep for next reconnect / startup flush (AC-019)
|
|
424
|
+
const idx = q.indexOf(entry);
|
|
425
|
+
if (idx !== -1)
|
|
426
|
+
q.splice(idx, 1);
|
|
427
|
+
try {
|
|
428
|
+
this.#db
|
|
429
|
+
.prepare("DELETE FROM retry_queue WHERE agent_name = ? AND session_id = ? AND content_hash_hex = ? AND awaiting_ack = 1")
|
|
430
|
+
.run(agentName, sessionId, entry.contentHashHex);
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
this.#logger.error("message.retry.persist.failed", {
|
|
434
|
+
sessionId, nonce: entry.contentHashHex, error: err instanceof Error ? err.message : String(err),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
parked += 1;
|
|
438
|
+
}
|
|
439
|
+
if (q.length === 0)
|
|
440
|
+
this.#awaiting.delete(ak);
|
|
441
|
+
return parked;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
//# sourceMappingURL=retry-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-queue.js","sourceRoot":"","sources":["../src/retry-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-LEG-2 (SESSION-004 SI-002) — client-side content-frontier re-derivation.
|
|
3
|
+
*
|
|
4
|
+
* The directory builds + signs the seal legibility, including each party's
|
|
5
|
+
* `content_frontier_seq` ("the highest message the party provably received"). The client
|
|
6
|
+
* must NOT trust that published value: a buggy or malicious directory could inflate it.
|
|
7
|
+
* The client independently re-derives each party's frontier from the SIGNED leaves the
|
|
8
|
+
* directory ships on the session_sealed frame, and rejects the certificate
|
|
9
|
+
* (`certificate_frontier_unverifiable`) if any published frontier EXCEEDS what the signed
|
|
10
|
+
* leaves support.
|
|
11
|
+
*
|
|
12
|
+
* Why signature-verification alone is sufficient to catch inflation: the directory cannot
|
|
13
|
+
* forge a leaf signed by a participant (it holds no participant key), so it can only ship
|
|
14
|
+
* REAL signed leaves. The maximum real signed last_seen_seq for a party IS that party's
|
|
15
|
+
* honest frontier — there is no way to fabricate a higher one. (A malicious directory could
|
|
16
|
+
* OMIT leaves, lowering the re-derived value and DoS-ing its own seal, but that is a refusal
|
|
17
|
+
* to seal, not an inflation attack — and Merkle-binding the shipped leaves to the sealed_root
|
|
18
|
+
* is a further hardening tracked separately.)
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the directory's buildSealLegibility frontier derivation
|
|
21
|
+
* (trustless-cello/packages/directory/src/seal-legibility.ts): max signed last_seen_seq
|
|
22
|
+
* (Structure 1 index 4) per sender, over that sender's OWN signed leaves.
|
|
23
|
+
*/
|
|
24
|
+
export interface SealFrontierLeaf {
|
|
25
|
+
structure1_cbor: Uint8Array;
|
|
26
|
+
sender_pubkey: Uint8Array;
|
|
27
|
+
sender_signature: Uint8Array;
|
|
28
|
+
}
|
|
29
|
+
export type ReDeriveResult = {
|
|
30
|
+
ok: true;
|
|
31
|
+
frontiers: Map<string, number>;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: "leaf_signature_invalid" | "leaf_malformed" | "leaf_session_mismatch";
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Re-derive each party's content_frontier_seq from the signed leaves. Verifies every leaf's
|
|
38
|
+
* Ed25519 signature over its Structure 1 bytes AND that the leaf's SIGNED session_id matches
|
|
39
|
+
* the session being sealed — otherwise a malicious directory could replay a party's genuinely
|
|
40
|
+
* signed leaves from a DIFFERENT session (where it reached a higher frontier) to inflate this
|
|
41
|
+
* session's frontier. An unverifiable or wrong-session leaf fails the whole re-derivation.
|
|
42
|
+
* Returns a map of lowercase-hex sender pubkey → max signed last_seen_seq.
|
|
43
|
+
*/
|
|
44
|
+
export declare function reDeriveFrontiers(leaves: SealFrontierLeaf[], expectedSessionId: Uint8Array): ReDeriveResult;
|
|
45
|
+
/**
|
|
46
|
+
* Compare each published per-party content_frontier_seq against the re-derived value. Returns
|
|
47
|
+
* the first party whose PUBLISHED frontier exceeds what its signed leaves support (inflation —
|
|
48
|
+
* the certificate_frontier_unverifiable case), or null when every published frontier is honest.
|
|
49
|
+
*/
|
|
50
|
+
export declare function findInflatedFrontier(participants: ReadonlyArray<{
|
|
51
|
+
pubkey: string;
|
|
52
|
+
content_frontier_seq: number;
|
|
53
|
+
}>, derived: Map<string, number>): {
|
|
54
|
+
party: string;
|
|
55
|
+
publishedFrontier: number;
|
|
56
|
+
derivedFrontier: number;
|
|
57
|
+
} | null;
|
|
58
|
+
//# sourceMappingURL=seal-frontier-verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seal-frontier-verify.d.ts","sourceRoot":"","sources":["../src/seal-frontier-verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,UAAU,CAAC;IAC5B,aAAa,EAAE,UAAU,CAAC;IAC1B,gBAAgB,EAAE,UAAU,CAAC;CAC9B;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAC5C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,wBAAwB,GAAG,gBAAgB,GAAG,uBAAuB,CAAA;CAAE,CAAC;AAQjG;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,EAAE,EAAE,iBAAiB,EAAE,UAAU,GAAG,cAAc,CA0B3G;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,aAAa,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,oBAAoB,EAAE,MAAM,CAAA;CAAE,CAAC,EAC7E,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQ9E"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-LEG-2 (SESSION-004 SI-002) — client-side content-frontier re-derivation.
|
|
3
|
+
*
|
|
4
|
+
* The directory builds + signs the seal legibility, including each party's
|
|
5
|
+
* `content_frontier_seq` ("the highest message the party provably received"). The client
|
|
6
|
+
* must NOT trust that published value: a buggy or malicious directory could inflate it.
|
|
7
|
+
* The client independently re-derives each party's frontier from the SIGNED leaves the
|
|
8
|
+
* directory ships on the session_sealed frame, and rejects the certificate
|
|
9
|
+
* (`certificate_frontier_unverifiable`) if any published frontier EXCEEDS what the signed
|
|
10
|
+
* leaves support.
|
|
11
|
+
*
|
|
12
|
+
* Why signature-verification alone is sufficient to catch inflation: the directory cannot
|
|
13
|
+
* forge a leaf signed by a participant (it holds no participant key), so it can only ship
|
|
14
|
+
* REAL signed leaves. The maximum real signed last_seen_seq for a party IS that party's
|
|
15
|
+
* honest frontier — there is no way to fabricate a higher one. (A malicious directory could
|
|
16
|
+
* OMIT leaves, lowering the re-derived value and DoS-ing its own seal, but that is a refusal
|
|
17
|
+
* to seal, not an inflation attack — and Merkle-binding the shipped leaves to the sealed_root
|
|
18
|
+
* is a further hardening tracked separately.)
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the directory's buildSealLegibility frontier derivation
|
|
21
|
+
* (trustless-cello/packages/directory/src/seal-legibility.ts): max signed last_seen_seq
|
|
22
|
+
* (Structure 1 index 4) per sender, over that sender's OWN signed leaves.
|
|
23
|
+
*/
|
|
24
|
+
import { verify } from "@cello-protocol/crypto";
|
|
25
|
+
import { decode } from "cbor-x";
|
|
26
|
+
function bytesEqual(a, b) {
|
|
27
|
+
if (a.length !== b.length)
|
|
28
|
+
return false;
|
|
29
|
+
for (let i = 0; i < a.length; i++)
|
|
30
|
+
if (a[i] !== b[i])
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Re-derive each party's content_frontier_seq from the signed leaves. Verifies every leaf's
|
|
36
|
+
* Ed25519 signature over its Structure 1 bytes AND that the leaf's SIGNED session_id matches
|
|
37
|
+
* the session being sealed — otherwise a malicious directory could replay a party's genuinely
|
|
38
|
+
* signed leaves from a DIFFERENT session (where it reached a higher frontier) to inflate this
|
|
39
|
+
* session's frontier. An unverifiable or wrong-session leaf fails the whole re-derivation.
|
|
40
|
+
* Returns a map of lowercase-hex sender pubkey → max signed last_seen_seq.
|
|
41
|
+
*/
|
|
42
|
+
export function reDeriveFrontiers(leaves, expectedSessionId) {
|
|
43
|
+
const frontiers = new Map();
|
|
44
|
+
for (const leaf of leaves) {
|
|
45
|
+
if (!verify(leaf.sender_pubkey, leaf.structure1_cbor, leaf.sender_signature)) {
|
|
46
|
+
return { ok: false, reason: "leaf_signature_invalid" };
|
|
47
|
+
}
|
|
48
|
+
let arr;
|
|
49
|
+
try {
|
|
50
|
+
arr = decode(leaf.structure1_cbor);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { ok: false, reason: "leaf_malformed" };
|
|
54
|
+
}
|
|
55
|
+
// Structure 1 = [1, content_hash(32), sender_pubkey(32), session_id(16), last_seen_seq, ts]
|
|
56
|
+
if (!Array.isArray(arr) || arr.length < 5)
|
|
57
|
+
return { ok: false, reason: "leaf_malformed" };
|
|
58
|
+
const sid = arr[3];
|
|
59
|
+
if (!(sid instanceof Uint8Array) || !bytesEqual(sid, expectedSessionId)) {
|
|
60
|
+
return { ok: false, reason: "leaf_session_mismatch" };
|
|
61
|
+
}
|
|
62
|
+
const raw = arr[4];
|
|
63
|
+
const lss = typeof raw === "bigint" ? Number(raw) : raw;
|
|
64
|
+
if (typeof lss !== "number" || !Number.isFinite(lss))
|
|
65
|
+
continue; // leaf carries no signed last_seen
|
|
66
|
+
const senderHex = Buffer.from(leaf.sender_pubkey).toString("hex").toLowerCase();
|
|
67
|
+
const cur = frontiers.get(senderHex) ?? 0;
|
|
68
|
+
if (lss > cur)
|
|
69
|
+
frontiers.set(senderHex, lss);
|
|
70
|
+
}
|
|
71
|
+
return { ok: true, frontiers };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Compare each published per-party content_frontier_seq against the re-derived value. Returns
|
|
75
|
+
* the first party whose PUBLISHED frontier exceeds what its signed leaves support (inflation —
|
|
76
|
+
* the certificate_frontier_unverifiable case), or null when every published frontier is honest.
|
|
77
|
+
*/
|
|
78
|
+
export function findInflatedFrontier(participants, derived) {
|
|
79
|
+
for (const p of participants) {
|
|
80
|
+
const d = derived.get(p.pubkey.toLowerCase()) ?? 0;
|
|
81
|
+
if (p.content_frontier_seq > d) {
|
|
82
|
+
return { party: p.pubkey, publishedFrontier: p.content_frontier_seq, derivedFrontier: d };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=seal-frontier-verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seal-frontier-verify.js","sourceRoot":"","sources":["../src/seal-frontier-verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAYhC,SAAS,UAAU,CAAC,CAAa,EAAE,CAAa;IAC9C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IACnE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAA0B,EAAE,iBAA6B;IACzF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAC7E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAC;QACzD,CAAC;QACD,IAAI,GAAY,CAAC;QACjB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QACjD,CAAC;QACD,4FAA4F;QAC5F,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QAC1F,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACnB,IAAI,CAAC,CAAC,GAAG,YAAY,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,CAAC;YACxE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC;QACxD,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACnB,MAAM,GAAG,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QACxD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,SAAS,CAAC,mCAAmC;QACnG,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QAChF,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,GAAG,GAAG,GAAG;YAAE,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,YAA6E,EAC7E,OAA4B;IAE5B,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,CAAC,oBAAoB,GAAG,CAAC,EAAE,CAAC;YAC/B,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAC,oBAAoB,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC;QAC5F,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The legibility shape this hash reads — pubkeys may be Uint8Array or Buffer (CBOR-decoded). */
|
|
2
|
+
export interface LegibilityForHash {
|
|
3
|
+
participants: Array<{
|
|
4
|
+
pubkey: unknown;
|
|
5
|
+
content_frontier_seq: unknown;
|
|
6
|
+
last_authored_seq: unknown;
|
|
7
|
+
attestation_mode: unknown;
|
|
8
|
+
}>;
|
|
9
|
+
final_message: {
|
|
10
|
+
sender_pubkey: unknown;
|
|
11
|
+
seq: unknown;
|
|
12
|
+
answered: unknown;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare function canonicalLegibilityBytes(legibility: LegibilityForHash): Buffer;
|
|
16
|
+
/** SHA-256 of the canonical legibility bytes (32 bytes). */
|
|
17
|
+
export declare function canonicalLegibilityHash(legibility: LegibilityForHash): Uint8Array;
|
|
18
|
+
/**
|
|
19
|
+
* Build the seal TBS bytes WITH the legibility binding: the plain seal TBS concatenated with
|
|
20
|
+
* the 32-byte legibility hash. When `legibility` is absent (the unilateral seal carries none),
|
|
21
|
+
* returns the plain TBS unchanged — so the unilateral path keeps signing/verifying the plain TBS
|
|
22
|
+
* and only the bilateral path (which always carries legibility) is bound.
|
|
23
|
+
*/
|
|
24
|
+
export declare function bindLegibilityToTbs(tbs: Uint8Array, legibility: LegibilityForHash | null | undefined): Uint8Array;
|
|
25
|
+
//# sourceMappingURL=seal-legibility-tbs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seal-legibility-tbs.d.ts","sourceRoot":"","sources":["../src/seal-legibility-tbs.ts"],"names":[],"mappings":"AAuBA,iGAAiG;AACjG,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,KAAK,CAAC;QAClB,MAAM,EAAE,OAAO,CAAC;QAChB,oBAAoB,EAAE,OAAO,CAAC;QAC9B,iBAAiB,EAAE,OAAO,CAAC;QAC3B,gBAAgB,EAAE,OAAO,CAAC;KAC3B,CAAC,CAAC;IACH,aAAa,EAAE;QACb,aAAa,EAAE,OAAO,CAAC;QACvB,GAAG,EAAE,OAAO,CAAC;QACb,QAAQ,EAAE,OAAO,CAAC;KACnB,CAAC;CACH;AA0BD,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,iBAAiB,GAAG,MAAM,CAY9E;AAED,4DAA4D;AAC5D,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,iBAAiB,GAAG,UAAU,CAEjF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,EAAE,UAAU,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,CAOjH"}
|