@dsh-enhanced/assistant-verifier 0.1.65 → 0.1.66

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.
Files changed (69) hide show
  1. package/README.md +77 -8
  2. package/lib/creation-review-store.d.ts +80 -1
  3. package/lib/creation-review-store.d.ts.map +1 -1
  4. package/lib/creation-review-store.js +520 -11
  5. package/lib/creation-review-store.js.map +1 -1
  6. package/lib/creation-review.d.ts +182 -46
  7. package/lib/creation-review.d.ts.map +1 -1
  8. package/lib/creation-review.js +283 -32
  9. package/lib/creation-review.js.map +1 -1
  10. package/lib/drivers.d.ts +20 -5
  11. package/lib/drivers.d.ts.map +1 -1
  12. package/lib/drivers.js +50 -4
  13. package/lib/drivers.js.map +1 -1
  14. package/lib/index.d.ts +2 -2
  15. package/lib/index.d.ts.map +1 -1
  16. package/lib/index.js +1 -1
  17. package/lib/index.js.map +1 -1
  18. package/lib/linux-behavior-assets.d.ts +17 -0
  19. package/lib/linux-behavior-assets.d.ts.map +1 -0
  20. package/lib/linux-behavior-assets.js +73 -0
  21. package/lib/linux-behavior-assets.js.map +1 -0
  22. package/lib/linux-behavior-runner.d.ts +10 -0
  23. package/lib/linux-behavior-runner.d.ts.map +1 -0
  24. package/lib/linux-behavior-runner.js +69 -0
  25. package/lib/linux-behavior-runner.js.map +1 -0
  26. package/lib/linux-behavior-runtime.d.ts +67 -0
  27. package/lib/linux-behavior-runtime.d.ts.map +1 -0
  28. package/lib/linux-behavior-runtime.js +500 -0
  29. package/lib/linux-behavior-runtime.js.map +1 -0
  30. package/lib/memory-review-native.d.ts +13 -1
  31. package/lib/memory-review-native.d.ts.map +1 -1
  32. package/lib/memory-review-native.js +11 -2
  33. package/lib/memory-review-native.js.map +1 -1
  34. package/lib/memory-review.d.ts +2 -0
  35. package/lib/memory-review.d.ts.map +1 -1
  36. package/lib/memory-review.js +110 -7
  37. package/lib/memory-review.js.map +1 -1
  38. package/lib/plugin-behavior-config.d.ts +10 -2
  39. package/lib/plugin-behavior-config.d.ts.map +1 -1
  40. package/lib/plugin-behavior-config.js +75 -6
  41. package/lib/plugin-behavior-config.js.map +1 -1
  42. package/lib/plugin-behavior-runner.d.ts +6 -4
  43. package/lib/plugin-behavior-runner.d.ts.map +1 -1
  44. package/lib/plugin-behavior-runner.js +41 -14
  45. package/lib/plugin-behavior-runner.js.map +1 -1
  46. package/lib/review-authority-lineage.d.ts +109 -0
  47. package/lib/review-authority-lineage.d.ts.map +1 -0
  48. package/lib/review-authority-lineage.js +135 -0
  49. package/lib/review-authority-lineage.js.map +1 -0
  50. package/lib/review-maintenance.d.ts +20 -0
  51. package/lib/review-maintenance.d.ts.map +1 -0
  52. package/lib/review-maintenance.js +19 -0
  53. package/lib/review-maintenance.js.map +1 -0
  54. package/lib/revision-regression-review.d.ts +4 -1
  55. package/lib/revision-regression-review.d.ts.map +1 -1
  56. package/lib/revision-regression-review.js +59 -7
  57. package/lib/revision-regression-review.js.map +1 -1
  58. package/lib/revision-review.d.ts +7 -0
  59. package/lib/revision-review.d.ts.map +1 -1
  60. package/lib/revision-review.js +48 -4
  61. package/lib/revision-review.js.map +1 -1
  62. package/lib/service.d.ts +2 -0
  63. package/lib/service.d.ts.map +1 -1
  64. package/lib/service.js +11 -2
  65. package/lib/service.js.map +1 -1
  66. package/lib/version.d.ts +1 -1
  67. package/lib/version.js +1 -1
  68. package/package.json +27 -14
  69. package/runtime/plugin-observer-launcher.c +246 -0
@@ -1,9 +1,24 @@
1
- import { closeSync, constants, lstatSync, mkdirSync, openSync, realpathSync } from 'node:fs';
1
+ import { createHash } from 'node:crypto';
2
+ import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, realpathSync } from 'node:fs';
2
3
  import { dirname, isAbsolute, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
3
5
  import { DatabaseSync } from 'node:sqlite';
4
6
  import { canonicalGrowthJson } from '@dsh-enhanced/assistant-growth-contract';
7
+ import { lineageFail, validateScope, validateRequest, same } from './review-authority-lineage.js';
5
8
  const SHA = /^[a-f0-9]{64}$/u;
6
9
  const ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/u;
10
+ const maintenance = Symbol('offline review grant maintenance');
11
+ const readOnlyMaintenance = Symbol('readonly offline review history');
12
+ const auditTable = 'creation_review_grant_handoffs';
13
+ const auditSchema = `CREATE TABLE ${auditTable}(transaction_id TEXT PRIMARY KEY,proof TEXT NOT NULL) STRICT`;
14
+ const auditTriggers = ['UPDATE', 'DELETE'].map(action => `CREATE TRIGGER creation_review_handoff_no_${action.toLowerCase()} BEFORE ${action} ON ${auditTable} BEGIN SELECT RAISE(ABORT,'immutable review handoff'); END`);
15
+ const hash = (value) => createHash('sha256').update(canonicalGrowthJson(value)).digest('hex');
16
+ function validScope(scope) {
17
+ return [scope.transactionId, scope.installationId, scope.profileId, scope.authority].every(value => typeof value === 'string' && ID.test(value))
18
+ && ['creation-reviews', 'revision-reviews', 'revision-regressions'].includes(scope.lane)
19
+ && typeof scope.configurationDigest === 'string' && SHA.test(scope.configurationDigest)
20
+ && typeof scope.toDigest === 'string' && SHA.test(scope.toDigest);
21
+ }
7
22
  function fail(message) { throw new Error(`creation review store: ${message}`); }
8
23
  function check(idValue, digestValue) {
9
24
  if (!ID.test(idValue) || !SHA.test(digestValue))
@@ -18,22 +33,30 @@ function privateFile(path) {
18
33
  /** Every dispatch is claimed durably first. A claimed or unknown stage is never replayed. */
19
34
  export class CreationReviewStore {
20
35
  #db;
36
+ #maintenance;
37
+ #path;
38
+ #identity;
21
39
  #closed = false;
22
- constructor(path) {
40
+ constructor(path, mode) {
41
+ this.#maintenance = mode === maintenance || mode === readOnlyMaintenance;
42
+ this.#path = path;
23
43
  if (!isAbsolute(path) || resolve(path) !== path)
24
44
  fail('invalid path');
25
45
  const parent = dirname(path);
26
- mkdirSync(parent, { recursive: true, mode: 0o700 });
46
+ if (!this.#maintenance)
47
+ mkdirSync(parent, { recursive: true, mode: 0o700 });
27
48
  const directory = lstatSync(parent);
28
49
  if (!directory.isDirectory() || directory.isSymbolicLink() || directory.mode & 0o077
29
50
  || process.getuid && directory.uid !== process.getuid() || realpathSync(parent) !== parent)
30
51
  fail('directory must be private');
31
- try {
32
- closeSync(openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | constants.O_NOFOLLOW, 0o600));
33
- }
34
- catch (error) {
35
- if (error.code !== 'EEXIST')
36
- throw error;
52
+ if (!this.#maintenance) {
53
+ try {
54
+ closeSync(openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | constants.O_NOFOLLOW, 0o600));
55
+ }
56
+ catch (error) {
57
+ if (error.code !== 'EEXIST')
58
+ throw error;
59
+ }
37
60
  }
38
61
  privateFile(path);
39
62
  for (const suffix of ['-wal', '-shm', '-journal']) {
@@ -45,9 +68,28 @@ export class CreationReviewStore {
45
68
  throw error;
46
69
  }
47
70
  }
48
- this.#db = new DatabaseSync(path);
71
+ const fd = openSync(path, (mode === readOnlyMaintenance ? constants.O_RDONLY : constants.O_RDWR) | constants.O_NOFOLLOW);
72
+ try {
73
+ const stat = fstatSync(fd);
74
+ this.#identity = { dev: stat.dev, ino: stat.ino };
75
+ if (this.#maintenance && stat.size < 100)
76
+ fail('maintenance database empty');
77
+ this.#db = mode === readOnlyMaintenance
78
+ ? new DatabaseSync(`${pathToFileURL(path).href}?mode=ro&immutable=1`, { readOnly: true })
79
+ : new DatabaseSync(this.#maintenance ? `${pathToFileURL(path).href}?mode=rw` : path);
80
+ }
81
+ finally {
82
+ closeSync(fd);
83
+ }
49
84
  try {
50
- this.#db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;
85
+ this.#checkFile();
86
+ if (this.#maintenance) {
87
+ if (mode !== readOnlyMaintenance)
88
+ this.#db.exec('PRAGMA busy_timeout=5000; PRAGMA synchronous=FULL;');
89
+ this.#maintenanceSchema();
90
+ }
91
+ else
92
+ this.#db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;
51
93
  CREATE TABLE IF NOT EXISTS grants(authority TEXT PRIMARY KEY,digest TEXT NOT NULL,maximum INTEGER NOT NULL,used INTEGER NOT NULL);
52
94
  CREATE TABLE IF NOT EXISTS verifications(plan TEXT PRIMARY KEY,binding TEXT NOT NULL,authority TEXT NOT NULL,
53
95
  state TEXT NOT NULL,data TEXT NOT NULL);`);
@@ -57,6 +99,471 @@ export class CreationReviewStore {
57
99
  throw error;
58
100
  }
59
101
  }
102
+ /** Offline private stage only. Caller must stop writers and authorize frozen configuration. */
103
+ static openExistingForMaintenance(path) {
104
+ return new CreationReviewStore(path, maintenance);
105
+ }
106
+ #checkFile() {
107
+ const parent = dirname(this.#path), directory = lstatSync(parent);
108
+ if (!directory.isDirectory() || directory.isSymbolicLink() || directory.mode & 0o077
109
+ || process.getuid && directory.uid !== process.getuid() || realpathSync(parent) !== parent)
110
+ fail('directory must be private');
111
+ privateFile(this.#path);
112
+ const stat = lstatSync(this.#path);
113
+ if (stat.dev !== this.#identity.dev || stat.ino !== this.#identity.ino)
114
+ fail('database identity changed');
115
+ for (const suffix of ['-wal', '-shm', '-journal']) {
116
+ try {
117
+ privateFile(this.#path + suffix);
118
+ }
119
+ catch (error) {
120
+ if (error.code !== 'ENOENT')
121
+ throw error;
122
+ }
123
+ }
124
+ }
125
+ #textIntegrity() {
126
+ // SQLite TEXT bindings can contain NUL/invalid UTF-8. Node 24's TEXT
127
+ // projection truncates at NUL, so never let that projection attest its own
128
+ // bytes. Check the independently returned BLOB before hashing/parsing/IDs.
129
+ const decoder = new TextDecoder('utf-8', { fatal: true });
130
+ const columns = {
131
+ sqlite_schema: ['type', 'name', 'tbl_name', 'sql'],
132
+ grants: ['authority', 'digest'], verifications: ['plan', 'binding', 'authority', 'state', 'data'],
133
+ };
134
+ if (this.#db.prepare("SELECT 1 FROM sqlite_schema WHERE name=?").get(auditTable))
135
+ columns[auditTable] = ['transaction_id', 'proof'];
136
+ for (const [table, names] of Object.entries(columns))
137
+ for (const column of names) {
138
+ const rows = this.#db.prepare(`SELECT ${column} AS text,CAST(${column} AS BLOB) AS bytes FROM ${table}`).iterate();
139
+ for (const row of rows) {
140
+ if (row.text === null && table === 'sqlite_schema' && column === 'sql')
141
+ continue;
142
+ if (typeof row.text !== 'string' || !(row.bytes instanceof Uint8Array) || row.bytes.includes(0))
143
+ lineageFail('unsafe SQLite TEXT bytes');
144
+ let decoded;
145
+ try {
146
+ decoded = decoder.decode(row.bytes);
147
+ }
148
+ catch {
149
+ lineageFail('invalid SQLite UTF-8 bytes');
150
+ }
151
+ if (decoded !== row.text || !Buffer.from(decoded, 'utf8').equals(Buffer.from(row.bytes)))
152
+ lineageFail('SQLite TEXT projection changed bytes');
153
+ }
154
+ }
155
+ }
156
+ #maintenanceSchema(allowRuntime = false) {
157
+ if (!allowRuntime && !this.#maintenance || this.#closed)
158
+ fail('requires existing-only maintenance');
159
+ this.#checkFile();
160
+ this.#textIntegrity();
161
+ if (this.#db.prepare('PRAGMA user_version').get()?.user_version !== 0
162
+ || this.#db.prepare('PRAGMA application_id').get()?.application_id !== 0)
163
+ fail('maintenance metadata changed');
164
+ const objects = this.#db.prepare("SELECT type,name,sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' ORDER BY name").all();
165
+ const audit = objects.some(row => row.name === auditTable);
166
+ const allowed = new Map([['grants', 'table'], ['verifications', 'table']]);
167
+ if (audit) {
168
+ allowed.set(auditTable, 'table');
169
+ allowed.set('creation_review_handoff_no_update', 'trigger');
170
+ allowed.set('creation_review_handoff_no_delete', 'trigger');
171
+ }
172
+ if (objects.length !== allowed.size || objects.some(row => allowed.get(String(row.name)) !== row.type))
173
+ fail('maintenance schema changed');
174
+ const normalize = (sql) => sql.replace(/\s+/gu, '').toLowerCase();
175
+ for (const sql of [
176
+ 'CREATE TABLE grants(authority TEXT PRIMARY KEY,digest TEXT NOT NULL,maximum INTEGER NOT NULL,used INTEGER NOT NULL)',
177
+ 'CREATE TABLE verifications(plan TEXT PRIMARY KEY,binding TEXT NOT NULL,authority TEXT NOT NULL,state TEXT NOT NULL,data TEXT NOT NULL)',
178
+ ]) {
179
+ if (!objects.some(row => typeof row.sql === 'string' && normalize(row.sql) === normalize(sql)))
180
+ fail('maintenance schema changed');
181
+ }
182
+ const columns = {
183
+ grants: ['authority:TEXT:0:1', 'digest:TEXT:1:0', 'maximum:INTEGER:1:0', 'used:INTEGER:1:0'],
184
+ verifications: ['plan:TEXT:0:1', 'binding:TEXT:1:0', 'authority:TEXT:1:0', 'state:TEXT:1:0', 'data:TEXT:1:0'],
185
+ };
186
+ if (audit)
187
+ columns[auditTable] = ['transaction_id:TEXT:1:1', 'proof:TEXT:1:0'];
188
+ for (const [table, expected] of Object.entries(columns)) {
189
+ const actual = this.#db.prepare(`PRAGMA table_xinfo(${table})`).all();
190
+ if (actual.some(row => row.hidden !== 0) || actual.map(row => `${row.name}:${row.type}:${row.notnull}:${row.pk}`).join() !== expected.join())
191
+ fail('maintenance columns changed');
192
+ }
193
+ if (audit && [auditSchema, ...auditTriggers].some(sql => !objects.some(row => row.sql === sql)))
194
+ fail('handoff audit schema changed');
195
+ const integrity = this.#db.prepare('PRAGMA quick_check').all();
196
+ if (integrity.length !== 1 || integrity[0]?.quick_check !== 'ok')
197
+ fail('maintenance integrity failed');
198
+ }
199
+ #snapshot(authority, overrideDigest) {
200
+ this.#maintenanceSchema();
201
+ const grants = this.#db.prepare('SELECT * FROM grants ORDER BY authority COLLATE BINARY').all();
202
+ const counts = new Map();
203
+ for (const row of grants) {
204
+ if (typeof row.authority !== 'string' || !ID.test(row.authority) || typeof row.digest !== 'string' || !SHA.test(row.digest)
205
+ || !Number.isSafeInteger(row.maximum) || row.maximum < 1 || row.maximum > 1000
206
+ || !Number.isSafeInteger(row.used) || row.used < 1 || row.used > row.maximum)
207
+ fail('grant corrupt or has no claimed history');
208
+ counts.set(row.authority, 0);
209
+ }
210
+ const grant = grants.find(row => row.authority === authority);
211
+ if (!grant)
212
+ fail('existing grant missing');
213
+ const rows = this.#db.prepare('SELECT * FROM verifications ORDER BY plan COLLATE BINARY').all();
214
+ // Hash rows independently: the whole history can exceed the canonical JSON
215
+ // helper's per-payload node limit even though every stored row is bounded.
216
+ const history = createHash('sha256').update('dsh-creation-review-history/v1\n');
217
+ for (const row of rows) {
218
+ const terminal = row.state === 'rejected' || row.state === 'unknown';
219
+ if (typeof row.plan !== 'string' || !ID.test(row.plan) || typeof row.binding !== 'string' || !SHA.test(row.binding)
220
+ || !counts.has(row.authority) || typeof row.data !== 'string' || Buffer.byteLength(row.data) > (terminal ? 300_000 : 262_144)
221
+ || !['discover-claimed', 'contract-claimed', 'contract-ready', 'case-claimed', 'cases-ready', 'review-claimed',
222
+ 'certificate', 'rejected', 'unknown'].includes(row.state))
223
+ fail('verification corrupt');
224
+ let data;
225
+ try {
226
+ data = JSON.parse(row.data);
227
+ }
228
+ catch {
229
+ fail('verification data corrupt');
230
+ }
231
+ if (!data || typeof data !== 'object' || Array.isArray(data) || canonicalGrowthJson(data) !== row.data)
232
+ fail('verification data corrupt');
233
+ const fields = data;
234
+ const keys = Object.keys(data);
235
+ if (keys.some(key => !['discovery', 'contract', 'cases', 'activeCase', 'certificate', 'reason'].includes(key))
236
+ || fields.cases !== undefined && (!fields.cases || typeof fields.cases !== 'object' || Array.isArray(fields.cases))
237
+ || fields.activeCase !== undefined && (Object.keys(fields.activeCase).sort().join(',') !== 'caseId,operationDigest'
238
+ || typeof fields.activeCase.caseId !== 'string' || !ID.test(fields.activeCase.caseId)
239
+ || typeof fields.activeCase.operationDigest !== 'string' || !SHA.test(fields.activeCase.operationDigest))
240
+ || ['rejected', 'unknown'].includes(row.state) && (typeof fields.reason !== 'string' || !fields.reason.trim() || fields.reason.length > 4096)
241
+ || !['rejected', 'unknown'].includes(row.state) && fields.reason !== undefined
242
+ || row.state !== 'certificate' && fields.certificate !== undefined
243
+ || row.state === 'certificate' && fields.certificate === undefined)
244
+ fail('verification fields corrupt');
245
+ const stageKeys = keys.filter(key => key !== 'reason').sort().join(',');
246
+ const prefixes = ['', 'discovery', 'cases,contract,discovery', 'activeCase,cases,contract,discovery'];
247
+ const expectedKeys = row.state === 'discover-claimed' ? prefixes[0]
248
+ : row.state === 'contract-claimed' ? prefixes[1]
249
+ : row.state === 'case-claimed' ? prefixes[3]
250
+ : row.state === 'certificate' ? 'cases,certificate,contract,discovery' : prefixes[2];
251
+ if (terminal ? !prefixes.includes(stageKeys) : stageKeys !== expectedKeys)
252
+ fail('verification stage corrupt');
253
+ counts.set(row.authority, counts.get(row.authority) + 1);
254
+ history.update(canonicalGrowthJson(row)).update('\n');
255
+ }
256
+ if (grants.some(row => counts.get(row.authority) !== row.used))
257
+ fail('used/history mismatch');
258
+ return { digest: overrideDigest ?? grant.digest, maximum: grant.maximum, used: grant.used,
259
+ grantsDigest: hash(grants.map(row => row.authority === authority && overrideDigest ? { ...row, digest: overrideDigest } : row)),
260
+ historyDigest: history.digest('hex'), historyCount: rows.length };
261
+ }
262
+ #auditSnapshot(excludeTransaction) {
263
+ const exists = this.#db.prepare("SELECT name FROM sqlite_schema WHERE name=?").get(auditTable);
264
+ const rows = exists ? this.#db.prepare(`SELECT transaction_id,proof FROM ${auditTable} ORDER BY transaction_id COLLATE BINARY`).all() : [];
265
+ const retained = rows.filter(row => row.transaction_id !== excludeTransaction);
266
+ return { count: retained.length, digest: hash(retained) };
267
+ }
268
+ /** Snapshot only; no schema, grant, audit or journal-mode changes. */
269
+ planGrantHandoff(scope) {
270
+ if (!validScope(scope))
271
+ fail('invalid handoff scope');
272
+ return this.#transaction(() => {
273
+ const before = this.#snapshot(scope.authority);
274
+ if (before.digest === scope.toDigest)
275
+ fail('handoff digest unchanged');
276
+ const body = { ...scope, protocol: 'dsh-review-grant-handoff/v1',
277
+ file: { path: this.#path, ...this.#identity }, before, auditBefore: this.#auditSnapshot() };
278
+ return Object.freeze({ ...body, file: Object.freeze(body.file), before: Object.freeze(before), auditBefore: Object.freeze(body.auditBefore), planDigest: hash(body) });
279
+ });
280
+ }
281
+ /** CAS and immutable audit commit together. Reuse only before any writer resumes. */
282
+ applyGrantHandoff(plan) {
283
+ if (!plan || !validScope(plan) || plan.protocol !== 'dsh-review-grant-handoff/v1')
284
+ fail('invalid handoff plan');
285
+ const { planDigest, ...body } = plan;
286
+ if (hash(body) !== planDigest || !SHA.test(planDigest) || plan.file.path !== this.#path
287
+ || plan.file.dev !== this.#identity.dev || plan.file.ino !== this.#identity.ino
288
+ || !SHA.test(plan.before.digest) || plan.before.digest === plan.toDigest
289
+ || !plan.auditBefore || !Number.isSafeInteger(plan.auditBefore.count) || plan.auditBefore.count < 0
290
+ || !SHA.test(plan.auditBefore.digest))
291
+ fail('invalid handoff plan');
292
+ return this.#transaction(() => {
293
+ const current = this.#snapshot(plan.authority);
294
+ const audit = this.#db.prepare("SELECT name FROM sqlite_schema WHERE name=?").get(auditTable);
295
+ if (audit) {
296
+ const prior = this.#db.prepare(`SELECT proof FROM ${auditTable} WHERE transaction_id=?`).get(plan.transactionId);
297
+ if (prior) {
298
+ const proof = JSON.parse(String(prior.proof));
299
+ if (prior.proof !== canonicalGrowthJson({ plan, after: current })
300
+ || canonicalGrowthJson(proof.plan) !== canonicalGrowthJson(plan)
301
+ || canonicalGrowthJson(proof.after) !== canonicalGrowthJson(current)
302
+ || canonicalGrowthJson(this.#auditSnapshot(plan.transactionId)) !== canonicalGrowthJson(plan.auditBefore)
303
+ || this.#auditSnapshot().count !== plan.auditBefore.count + 1)
304
+ fail('handoff retry conflict or drift');
305
+ return proof;
306
+ }
307
+ if (Number(this.#db.prepare(`SELECT COUNT(*) AS n FROM ${auditTable}`).get()?.n) >= 64)
308
+ fail('handoff audit limit');
309
+ }
310
+ if (canonicalGrowthJson(current) !== canonicalGrowthJson(plan.before)
311
+ || canonicalGrowthJson(this.#auditSnapshot()) !== canonicalGrowthJson(plan.auditBefore))
312
+ fail('handoff snapshot changed');
313
+ if (!audit)
314
+ this.#db.exec([auditSchema, ...auditTriggers].join(';'));
315
+ const after = this.#snapshot(plan.authority, plan.toDigest);
316
+ const result = this.#db.prepare('UPDATE grants SET digest=? WHERE authority=? AND digest=? AND maximum=? AND used=?')
317
+ .run(plan.toDigest, plan.authority, current.digest, current.maximum, current.used);
318
+ if (result.changes !== 1)
319
+ fail('handoff compare-and-swap failed');
320
+ const proof = { plan, after };
321
+ this.#db.prepare(`INSERT INTO ${auditTable}(transaction_id,proof) VALUES(?,?)`).run(plan.transactionId, canonicalGrowthJson(proof));
322
+ if (canonicalGrowthJson(this.#snapshot(plan.authority)) !== canonicalGrowthJson(after))
323
+ fail('handoff changed history');
324
+ return Object.freeze(proof);
325
+ });
326
+ }
327
+ #lineageAudits() {
328
+ const exists = this.#db.prepare('SELECT name FROM sqlite_schema WHERE name=?').get(auditTable);
329
+ if (!exists)
330
+ return [];
331
+ const rows = this.#db.prepare(`SELECT proof FROM ${auditTable} LIMIT 65`).all();
332
+ if (rows.length > 64)
333
+ lineageFail('chain limit');
334
+ return rows.map(row => {
335
+ if (typeof row.proof !== 'string' || Buffer.byteLength(row.proof) > 1_048_576)
336
+ lineageFail('proof bound');
337
+ const proof = JSON.parse(row.proof);
338
+ if (canonicalGrowthJson(proof) !== row.proof)
339
+ lineageFail('noncanonical audit proof');
340
+ this.#validateLineageProof(proof);
341
+ return proof;
342
+ });
343
+ }
344
+ #validateLineageProof(proof) {
345
+ if (!proof || Object.keys(proof).sort().join() !== 'after,plan' || !proof.plan
346
+ || proof.plan.protocol !== 'dsh-review-grant-handoff/v2')
347
+ lineageFail('v1 is not a history root');
348
+ const p = proof.plan, { planDigest, ...body } = p;
349
+ validateScope(p.scope);
350
+ if (Object.keys(p).sort().join() !== 'auditBefore,before,file,members,planDigest,protocol,scope,seals'
351
+ || hash(body) !== planDigest || !SHA.test(planDigest) || !p.file || !isAbsolute(p.file.path)
352
+ || !Number.isSafeInteger(p.file.dev) || !Number.isSafeInteger(p.file.ino)
353
+ || !p.auditBefore || !Number.isSafeInteger(p.auditBefore.count) || p.auditBefore.count < 0 || p.auditBefore.count >= 64 || !SHA.test(p.auditBefore.digest)
354
+ || !Array.isArray(p.members) || p.members.length > p.scope.oldIdentity.maximum
355
+ || p.members.some((id, i) => typeof id !== 'string' || !ID.test(id) || i > 0 && id <= p.members[i - 1])
356
+ || Buffer.byteLength(canonicalGrowthJson(p.members)) > 262_144
357
+ || !Array.isArray(p.seals) || p.seals.length > p.scope.oldIdentity.maximum || Buffer.byteLength(canonicalGrowthJson(p.seals)) > 262_144
358
+ || p.before.digest !== p.scope.oldIdentity.authority.authorityDigest || proof.after.digest !== p.scope.newIdentity.authority.authorityDigest
359
+ || p.before.maximum !== p.scope.oldIdentity.maximum || !Number.isSafeInteger(p.before.used) || p.before.used < 1 || p.before.used > p.before.maximum
360
+ || p.members.length !== p.before.used || p.seals.some(seal => !p.members.includes(seal.planId))
361
+ || p.seals.length > p.before.used || !SHA.test(p.before.historyDigest) || !SHA.test(p.before.grantsDigest)
362
+ || !SHA.test(proof.after.grantsDigest)
363
+ || !same({ ...p.before, digest: proof.after.digest, grantsDigest: proof.after.grantsDigest }, proof.after))
364
+ lineageFail('invalid v2 proof');
365
+ let previous = '';
366
+ for (const seal of p.seals) {
367
+ if (!seal || Object.keys(seal).sort().join() !== 'certificateDigest,planId,rowDigest' || !ID.test(seal.planId)
368
+ || seal.planId <= previous || !SHA.test(seal.rowDigest) || !SHA.test(seal.certificateDigest))
369
+ lineageFail('invalid seal');
370
+ previous = seal.planId;
371
+ }
372
+ }
373
+ #lineageChain(audits, headDigest, identity, scope) {
374
+ const byDigest = new Map(audits.map(proof => [hash(proof), proof]));
375
+ const chain = [], seen = new Set(), identities = new Set();
376
+ let next = headDigest, current = identity;
377
+ while (next !== null) {
378
+ if (chain.length >= 64 || seen.has(next))
379
+ lineageFail('chain cycle or bound');
380
+ const proof = byDigest.get(next);
381
+ if (!proof)
382
+ lineageFail('broken chain');
383
+ const s = proof.plan.scope;
384
+ if (!same(s.newIdentity, current) || ['installationId', 'profileId', 'lane', 'logicalDatabaseScope'].some(k => s[k] !== scope[k]))
385
+ lineageFail('chain identity or scope');
386
+ if (identities.has(s.newIdentity.authority.authorityDigest))
387
+ lineageFail('authority ABA');
388
+ identities.add(s.newIdentity.authority.authorityDigest);
389
+ seen.add(next);
390
+ chain.push(proof);
391
+ current = s.oldIdentity;
392
+ next = s.previousHeadDigest;
393
+ }
394
+ if (identities.has(current.authority.authorityDigest))
395
+ lineageFail('authority ABA');
396
+ const earlierMembers = new Set();
397
+ for (const proof of [...chain].reverse()) {
398
+ const members = new Set(proof.plan.members);
399
+ if ([...earlierMembers].some(member => !members.has(member))
400
+ || proof.plan.seals.some(seal => earlierMembers.has(seal.planId)))
401
+ lineageFail('historical member eligibility changed');
402
+ for (const member of members)
403
+ earlierMembers.add(member);
404
+ }
405
+ return chain;
406
+ }
407
+ #terminalSeals(identity, excludedMembers = new Set()) {
408
+ const rows = this.#db.prepare("SELECT * FROM verifications WHERE authority=? AND state='certificate' ORDER BY plan COLLATE BINARY LIMIT 1001")
409
+ .all(identity.authority.authorityId);
410
+ if (rows.length > identity.maximum)
411
+ lineageFail('terminal bound');
412
+ const seals = [];
413
+ for (const row of rows) {
414
+ if (excludedMembers.has(row.plan))
415
+ continue;
416
+ const data = JSON.parse(row.data);
417
+ // Previous identities remain eligible only through their earlier seals.
418
+ if (!same(data.certificate?.authority, identity.authority))
419
+ continue;
420
+ if (data.certificate?.plan?.id !== row.plan)
421
+ lineageFail('certificate plan mismatch');
422
+ seals.push({ planId: row.plan, rowDigest: hash(row), certificateDigest: hash(data.certificate) });
423
+ }
424
+ if (Buffer.byteLength(canonicalGrowthJson(seals)) > 262_144)
425
+ lineageFail('seal bytes bound');
426
+ return seals;
427
+ }
428
+ #members(authority) {
429
+ const rows = this.#db.prepare('SELECT plan FROM verifications WHERE authority=? ORDER BY plan COLLATE BINARY LIMIT 1001').all(authority);
430
+ if (rows.length > 1000)
431
+ lineageFail('membership limit');
432
+ return rows.map(row => String(row.plan));
433
+ }
434
+ #priorMembers(audits) { return new Set(audits.flatMap(proof => proof.plan.members)); }
435
+ #checkPrevious(scope, audits) {
436
+ if (scope.previousHeadDigest === null) {
437
+ if (audits.length)
438
+ lineageFail('existing history requires previous head');
439
+ return;
440
+ }
441
+ const chain = this.#lineageChain(audits, scope.previousHeadDigest, scope.oldIdentity, scope);
442
+ const currentMembers = new Set(this.#members(scope.oldIdentity.authority.authorityId));
443
+ if (chain.some(proof => proof.plan.members.some(member => !currentMembers.has(member))))
444
+ lineageFail('historical membership removed');
445
+ if (chain.length !== audits.length || chain.some(p => p.plan.scope.oldIdentity.authority.authorityDigest === scope.newIdentity.authority.authorityDigest
446
+ || p.plan.scope.newIdentity.authority.authorityDigest === scope.newIdentity.authority.authorityDigest))
447
+ lineageFail('branch or authority ABA');
448
+ for (const proof of chain)
449
+ for (const seal of proof.plan.seals) {
450
+ const row = this.#db.prepare('SELECT * FROM verifications WHERE plan=?').get(seal.planId);
451
+ if (!row || row.state !== 'certificate' || hash(row) !== seal.rowDigest
452
+ || hash(JSON.parse(row.data).certificate) !== seal.certificateDigest)
453
+ lineageFail('inherited seal changed');
454
+ }
455
+ }
456
+ planGrantHandoffV2(scope) {
457
+ validateScope(scope);
458
+ return this.#transaction(() => {
459
+ const before = this.#snapshot(scope.oldIdentity.authority.authorityId);
460
+ if (before.digest !== scope.oldIdentity.authority.authorityDigest || before.maximum !== scope.oldIdentity.maximum)
461
+ lineageFail('old grant identity');
462
+ const audits = this.#lineageAudits();
463
+ this.#checkPrevious(scope, audits);
464
+ const body = { protocol: 'dsh-review-grant-handoff/v2', scope: structuredClone(scope),
465
+ file: { path: this.#path, ...this.#identity }, before, auditBefore: this.#auditSnapshot(), members: this.#members(scope.oldIdentity.authority.authorityId), seals: this.#terminalSeals(scope.oldIdentity, this.#priorMembers(audits)) };
466
+ return structuredClone({ ...body, planDigest: hash(body) });
467
+ });
468
+ }
469
+ applyGrantHandoffV2(plan) {
470
+ // Validate via a synthetic after snapshot before any database mutation.
471
+ const { planDigest: _, ...body } = plan;
472
+ validateScope(plan.scope);
473
+ if (hash(body) !== plan.planDigest || !same(plan.file, { path: this.#path, ...this.#identity }))
474
+ lineageFail('plan or file changed');
475
+ return this.#transaction(() => {
476
+ const current = this.#snapshot(plan.scope.oldIdentity.authority.authorityId), audits = this.#lineageAudits();
477
+ const prior = audits.find(p => p.plan.scope.transactionId === plan.scope.transactionId);
478
+ if (prior) {
479
+ if (!same(prior.plan, plan) || !same(prior.after, current) || !same(this.#auditSnapshot(plan.scope.transactionId), plan.auditBefore)
480
+ || this.#auditSnapshot().count !== plan.auditBefore.count + 1 || !same(this.#members(plan.scope.oldIdentity.authority.authorityId), plan.members)
481
+ || !same(this.#terminalSeals(plan.scope.oldIdentity, this.#priorMembers(audits.filter(p => p !== prior))), plan.seals))
482
+ lineageFail('retry drift');
483
+ return prior;
484
+ }
485
+ this.#checkPrevious(plan.scope, audits);
486
+ if (audits.length >= 64 || !same(current, plan.before) || !same(this.#auditSnapshot(), plan.auditBefore)
487
+ || !same(this.#members(plan.scope.oldIdentity.authority.authorityId), plan.members)
488
+ || !same(this.#terminalSeals(plan.scope.oldIdentity, this.#priorMembers(audits)), plan.seals))
489
+ lineageFail('snapshot or seals changed');
490
+ const after = this.#snapshot(plan.scope.oldIdentity.authority.authorityId, plan.scope.newIdentity.authority.authorityDigest);
491
+ const proof = { plan, after };
492
+ this.#validateLineageProof(proof);
493
+ if (!this.#db.prepare('SELECT name FROM sqlite_schema WHERE name=?').get(auditTable))
494
+ this.#db.exec([auditSchema, ...auditTriggers].join(';'));
495
+ const changed = this.#db.prepare('UPDATE grants SET digest=? WHERE authority=? AND digest=? AND maximum=? AND used=?')
496
+ .run(after.digest, plan.scope.oldIdentity.authority.authorityId, current.digest, current.maximum, current.used);
497
+ if (changed.changes !== 1)
498
+ lineageFail('CAS failed');
499
+ this.#db.prepare(`INSERT INTO ${auditTable}(transaction_id,proof) VALUES(?,?)`).run(plan.scope.transactionId, canonicalGrowthJson(proof));
500
+ if (!same(this.#snapshot(plan.scope.oldIdentity.authority.authorityId), after))
501
+ lineageFail('history changed');
502
+ return structuredClone(proof);
503
+ });
504
+ }
505
+ /** Same existing connection, one read transaction. Does not claim, migrate, or change quota. */
506
+ resolveHistoricalCertificate(request) {
507
+ validateRequest(request);
508
+ this.#checkFile();
509
+ if (!same(request.currentFileIdentity, this.#identity))
510
+ lineageFail('current file identity');
511
+ if (this.#closed)
512
+ lineageFail('closed');
513
+ this.#db.exec('BEGIN');
514
+ try {
515
+ this.#maintenanceSchema(true);
516
+ const audits = this.#lineageAudits(), chain = this.#lineageChain(audits, request.headDigest, request.currentIdentity, request);
517
+ if (chain.length !== audits.length || !same(chain[0]?.plan.file, { path: chain[0]?.plan.file.path, ...request.currentFileIdentity }))
518
+ lineageFail('current head file or branch');
519
+ const currentMembers = new Set(this.#members(request.currentIdentity.authority.authorityId));
520
+ if (chain.some(proof => proof.plan.members.some(member => !currentMembers.has(member))))
521
+ lineageFail('historical membership removed');
522
+ const grant = this.#db.prepare('SELECT * FROM grants WHERE authority=?').get(request.currentIdentity.authority.authorityId);
523
+ const count = Number(this.#db.prepare('SELECT COUNT(*) AS n FROM verifications WHERE authority=?').get(request.currentIdentity.authority.authorityId)?.n);
524
+ if (!grant || grant.digest !== request.currentIdentity.authority.authorityDigest || grant.maximum !== request.currentIdentity.maximum
525
+ || !Number.isSafeInteger(grant.used) || grant.used !== count || grant.used < chain[0].after.used || grant.used > grant.maximum)
526
+ lineageFail('current grant changed');
527
+ const row = this.#db.prepare('SELECT * FROM verifications WHERE plan=?').get(request.planId);
528
+ if (!row || row.state !== 'certificate' || row.authority !== grant.authority || Buffer.byteLength(row.data) > 262_144)
529
+ lineageFail('exact terminal row required');
530
+ const data = JSON.parse(row.data);
531
+ if (canonicalGrowthJson(data) !== row.data || !data.certificate || data.certificate.plan?.id !== row.plan)
532
+ lineageFail('terminal data');
533
+ const rowDigest = hash(row), certificateDigest = hash(data.certificate);
534
+ const sealed = chain.find(proof => proof.plan.seals.some(seal => seal.planId === row.plan && seal.rowDigest === rowDigest && seal.certificateDigest === certificateDigest));
535
+ if (!sealed || !same(data.certificate.authority, sealed.plan.scope.oldIdentity.authority))
536
+ lineageFail('unsealed terminal row');
537
+ this.#checkFile();
538
+ this.#db.exec('COMMIT');
539
+ return structuredClone({ certificate: data.certificate, identity: sealed.plan.scope.oldIdentity });
540
+ }
541
+ catch (error) {
542
+ this.#db.exec('ROLLBACK');
543
+ throw error;
544
+ }
545
+ }
546
+ /** Offline checkpointed adapter only. Live runtime should use its already-open store method above. */
547
+ static resolveHistoricalCertificate(path, request) {
548
+ validateRequest(request);
549
+ for (const suffix of ['-wal', '-journal']) {
550
+ try {
551
+ if (lstatSync(path + suffix).size)
552
+ lineageFail('offline checkpoint required');
553
+ }
554
+ catch (error) {
555
+ if (error.code !== 'ENOENT')
556
+ throw error;
557
+ }
558
+ }
559
+ const store = new CreationReviewStore(path, readOnlyMaintenance);
560
+ try {
561
+ return store.resolveHistoricalCertificate(request);
562
+ }
563
+ finally {
564
+ store.close();
565
+ }
566
+ }
60
567
  close() { if (!this.#closed) {
61
568
  this.#closed = true;
62
569
  this.#db.close();
@@ -64,6 +571,8 @@ export class CreationReviewStore {
64
571
  #transaction(callback) {
65
572
  if (this.#closed)
66
573
  fail('closed');
574
+ if (this.#maintenance)
575
+ this.#checkFile();
67
576
  this.#db.exec('BEGIN IMMEDIATE');
68
577
  try {
69
578
  const result = callback();