@c956180462/awbs 0.0.1

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 (50) hide show
  1. package/AWBS_CORE_DESIGN.md +983 -0
  2. package/AWBS_CURRENT_FEATURES.md +463 -0
  3. package/LICENSE +21 -0
  4. package/README.md +265 -0
  5. package/TASK_001_VIEW_AUTHORITY.md +446 -0
  6. package/TASK_003_AUTHORITY_LEDGER_AND_DB_AUDIT.md +268 -0
  7. package/TASK_004_TRUSTED_AUTHORITY_LAYER.md +547 -0
  8. package/TASK_005_AUTHORITY_SESSION.md +218 -0
  9. package/TASK_006_TRUST_BOUNDARY_HARDENING.md +381 -0
  10. package/TASK_007_TRUSTED_OPERATION_ENTRY.md +129 -0
  11. package/bin/awbs.js +2 -0
  12. package/docs/DEVELOPMENT_LEARNING.md +319 -0
  13. package/docs/FULL_CHAIN.md +295 -0
  14. package/docs/PRODUCT.md +188 -0
  15. package/docs/USAGE.md +294 -0
  16. package/package.json +45 -0
  17. package/src/adapters/file-summary-store.ts +88 -0
  18. package/src/adapters/git-cli.ts +107 -0
  19. package/src/adapters/local-authority-session.ts +606 -0
  20. package/src/adapters/local-file-database.ts +199 -0
  21. package/src/adapters/sealed-authority.ts +725 -0
  22. package/src/adapters/session-authority-client.ts +176 -0
  23. package/src/adapters/sqlite-index-store.ts +176 -0
  24. package/src/cli.ts +491 -0
  25. package/src/domain/authority-types.ts +194 -0
  26. package/src/domain/constants.ts +11 -0
  27. package/src/domain/errors.ts +6 -0
  28. package/src/domain/hash.ts +27 -0
  29. package/src/domain/path-policy.ts +36 -0
  30. package/src/domain/paths.ts +65 -0
  31. package/src/domain/session-proof.ts +140 -0
  32. package/src/domain/session-types.ts +101 -0
  33. package/src/domain/types.ts +94 -0
  34. package/src/ports/authority-session.ts +8 -0
  35. package/src/ports/authority.ts +26 -0
  36. package/src/ports/file-database.ts +18 -0
  37. package/src/ports/git.ts +23 -0
  38. package/src/ports/index-store.ts +7 -0
  39. package/src/ports/summary-store.ts +16 -0
  40. package/src/runtime.ts +56 -0
  41. package/src/session-entry.ts +1 -0
  42. package/src/usecases/authority.ts +53 -0
  43. package/src/usecases/changeset.ts +437 -0
  44. package/src/usecases/db.ts +192 -0
  45. package/src/usecases/index.ts +136 -0
  46. package/src/usecases/init.ts +48 -0
  47. package/src/usecases/ledger.ts +146 -0
  48. package/src/usecases/session.ts +48 -0
  49. package/src/usecases/trusted-chain.ts +56 -0
  50. package/src/usecases/view.ts +166 -0
@@ -0,0 +1,725 @@
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, scryptSync } from "node:crypto";
2
+ import { join, resolve } from "node:path";
3
+ import { AwbsError } from "../domain/errors.ts";
4
+ import type {
5
+ AuthorityCatalog,
6
+ AuthorityChangesetApplyOperation,
7
+ AuthorityChangesetReceipt,
8
+ AuthorityEvent,
9
+ AuthorityLedger,
10
+ AuthorityLedgerEntry,
11
+ AuthorityLocal,
12
+ AuthorityPayloadType,
13
+ AuthorityReceipt,
14
+ AuthorityRepairReport,
15
+ AuthorityRepo,
16
+ AuthorityResource,
17
+ AuthorityVerifyReport,
18
+ AuthorityViewContract,
19
+ AuthorityViewSource,
20
+ SealEnvelope
21
+ } from "../domain/authority-types.ts";
22
+ import type { FileDatabasePort } from "../ports/file-database.ts";
23
+ import type { AuthorityPort } from "../ports/authority.ts";
24
+ import type { ChangesetManifest } from "../domain/types.ts";
25
+
26
+ const RUNTIME_PEPPER = "awbs-authority-runtime-context-v1";
27
+
28
+ export class SealedAuthorityAdapter implements AuthorityPort {
29
+ private readonly files: FileDatabasePort;
30
+ private readonly memoryLocal: AuthorityLocal | null;
31
+
32
+ constructor(files: FileDatabasePort, options: { memoryLocal?: AuthorityLocal } = {}) {
33
+ this.files = files;
34
+ this.memoryLocal = options.memoryLocal ?? null;
35
+ }
36
+
37
+ ensureInitialized(root: string): void {
38
+ this.files.ensureDir(join(root, ".awbs", "authority", "views"));
39
+ this.files.ensureDir(join(root, ".awbs", "private"));
40
+
41
+ const repoPath = this.repoPath(root);
42
+ const localPath = this.localPath(root);
43
+ const eventsPath = this.eventsPath(root);
44
+ const ledgerEventsPath = this.ledgerEventsPath(root);
45
+
46
+ if (!this.files.pathExists(repoPath)) {
47
+ const repo: AuthorityRepo = {
48
+ schemaVersion: 1,
49
+ repoId: randomUUID(),
50
+ authoritySalt: randomBytes(24).toString("base64"),
51
+ algorithm: "AWBS-AES-256-GCM-v1",
52
+ kdf: "scrypt-repo-local-runtime-v1",
53
+ trustMode: "ephemeral-local-key-v1",
54
+ createdAt: new Date().toISOString()
55
+ };
56
+ this.files.writeJson(repoPath, repo);
57
+ } else {
58
+ this.assertRepoTrustMode(root);
59
+ }
60
+
61
+ if (!this.memoryLocal && !this.files.pathExists(localPath)) {
62
+ const local: AuthorityLocal = {
63
+ schemaVersion: 1,
64
+ installationId: randomUUID(),
65
+ localSealSeed: randomBytes(32).toString("base64"),
66
+ createdAt: new Date().toISOString()
67
+ };
68
+ this.files.writeJson(localPath, local);
69
+ }
70
+
71
+ if (!this.files.pathExists(eventsPath)) {
72
+ this.files.writeText(eventsPath, "");
73
+ }
74
+ if (!this.files.pathExists(ledgerEventsPath)) {
75
+ this.files.writeText(ledgerEventsPath, "");
76
+ }
77
+
78
+ const catalogSealPath = this.catalogSealPath(root);
79
+ if (!this.files.pathExists(catalogSealPath)) {
80
+ const now = new Date().toISOString();
81
+ const repo = this.readRepo(root);
82
+ const catalog: AuthorityCatalog = {
83
+ schemaVersion: 1,
84
+ repoId: repo.repoId,
85
+ catalogVersion: 1,
86
+ createdAt: now,
87
+ updatedAt: now,
88
+ resources: [],
89
+ views: [],
90
+ ext: {}
91
+ };
92
+ this.writeCatalog(root, catalog);
93
+ this.appendEvent(root, {
94
+ schemaVersion: 1,
95
+ event: "AUTHORITY_INITIALIZED",
96
+ eventId: randomUUID(),
97
+ createdAt: now,
98
+ details: { repoId: repo.repoId }
99
+ });
100
+ } else {
101
+ this.readCatalog(root);
102
+ }
103
+ }
104
+
105
+ createView(root: string, contract: AuthorityViewContract): AuthorityViewContract {
106
+ this.ensureInitialized(root);
107
+ const viewDir = this.viewDir(root, contract.viewId);
108
+ if (this.files.pathExists(join(viewDir, "contract.seal.json"))) {
109
+ throw new AwbsError(`View already exists: ${contract.viewId}`);
110
+ }
111
+
112
+ this.files.ensureDir(viewDir);
113
+ this.writeViewContract(root, contract);
114
+
115
+ const catalog = this.readCatalog(root);
116
+ const nextCatalog: AuthorityCatalog = {
117
+ ...catalog,
118
+ catalogVersion: catalog.catalogVersion + 1,
119
+ updatedAt: new Date().toISOString(),
120
+ resources: mergeResources(catalog.resources, contract.sources),
121
+ views: [
122
+ ...catalog.views,
123
+ {
124
+ viewId: contract.viewId,
125
+ status: "active",
126
+ baseCommit: contract.baseCommit,
127
+ readPaths: contract.readPaths,
128
+ writePaths: contract.writePaths,
129
+ createdAt: contract.createdAt,
130
+ ext: {}
131
+ }
132
+ ]
133
+ };
134
+ this.writeCatalog(root, nextCatalog);
135
+ this.appendEvent(root, {
136
+ schemaVersion: 1,
137
+ event: "VIEW_CREATED",
138
+ eventId: randomUUID(),
139
+ createdAt: new Date().toISOString(),
140
+ viewId: contract.viewId,
141
+ details: { readPaths: contract.readPaths, writePaths: contract.writePaths }
142
+ });
143
+ return contract;
144
+ }
145
+
146
+ getViewContract(root: string, viewId: string, options: { allowRevoked?: boolean } = {}): AuthorityViewContract {
147
+ this.ensureInitialized(root);
148
+ const catalog = this.readCatalog(root);
149
+ const catalogView = catalog.views.find((view) => view.viewId === viewId);
150
+ if (!catalogView) {
151
+ throw new AwbsError(`View is not registered in authority catalog: ${viewId}`);
152
+ }
153
+ if (catalogView.status === "revoked" && !options.allowRevoked) {
154
+ throw new AwbsError(`View has been revoked: ${viewId}`);
155
+ }
156
+
157
+ const contract = this.openViewContract(root, viewId);
158
+ if (contract.viewId !== viewId) {
159
+ throw new AwbsError(`View contract id mismatch for ${viewId}`);
160
+ }
161
+ return contract;
162
+ }
163
+
164
+ revokeView(root: string, viewId: string): AuthorityViewContract {
165
+ const contract = this.getViewContract(root, viewId, { allowRevoked: true });
166
+ const catalog = this.readCatalog(root);
167
+ const existing = catalog.views.find((view) => view.viewId === viewId);
168
+ if (!existing) {
169
+ throw new AwbsError(`View is not registered in authority catalog: ${viewId}`);
170
+ }
171
+ if (existing.status === "revoked") {
172
+ return contract;
173
+ }
174
+
175
+ const now = new Date().toISOString();
176
+ const nextCatalog: AuthorityCatalog = {
177
+ ...catalog,
178
+ catalogVersion: catalog.catalogVersion + 1,
179
+ updatedAt: now,
180
+ views: catalog.views.map((view) => (view.viewId === viewId ? { ...view, status: "revoked", revokedAt: now } : view))
181
+ };
182
+ this.writeCatalog(root, nextCatalog);
183
+ this.appendEvent(root, {
184
+ schemaVersion: 1,
185
+ event: "VIEW_REVOKED",
186
+ eventId: randomUUID(),
187
+ createdAt: now,
188
+ viewId
189
+ });
190
+ return contract;
191
+ }
192
+
193
+ verify(root: string): AuthorityVerifyReport {
194
+ const errors: string[] = [];
195
+ const mirrorMismatches: string[] = [];
196
+ let catalog: AuthorityCatalog | null = null;
197
+
198
+ try {
199
+ this.ensureInitializedNoCatalogRead(root);
200
+ catalog = this.openSeal<AuthorityCatalog>(root, this.catalogSealPath(root), "authority.catalog");
201
+ if (this.readOptionalText(this.catalogMirrorPath(root)) !== mirrorText(catalog)) {
202
+ mirrorMismatches.push("catalog.mirror.json");
203
+ }
204
+ } catch (error) {
205
+ errors.push(error instanceof Error ? error.message : String(error));
206
+ }
207
+
208
+ if (catalog) {
209
+ for (const view of catalog.views) {
210
+ try {
211
+ const contract = this.openSeal<AuthorityViewContract>(root, this.viewSealPath(root, view.viewId), "authority.viewContract");
212
+ if (this.readOptionalText(this.viewMirrorPath(root, view.viewId)) !== mirrorText(contract)) {
213
+ mirrorMismatches.push(`views/${view.viewId}/mirror.json`);
214
+ }
215
+ } catch (error) {
216
+ errors.push(error instanceof Error ? error.message : String(error));
217
+ }
218
+ }
219
+ }
220
+
221
+ if (this.hasLedger(root)) {
222
+ try {
223
+ const ledger = this.openSeal<AuthorityLedger>(root, this.ledgerSealPath(root), "authority.ledger");
224
+ if (this.readOptionalText(this.ledgerMirrorPath(root)) !== mirrorText(ledger)) {
225
+ mirrorMismatches.push("ledger.mirror.json");
226
+ }
227
+ } catch (error) {
228
+ errors.push(error instanceof Error ? error.message : String(error));
229
+ }
230
+ }
231
+
232
+ return {
233
+ ok: errors.length === 0 && mirrorMismatches.length === 0,
234
+ mirrorMismatches,
235
+ errors,
236
+ catalog: {
237
+ views: catalog?.views.length ?? 0,
238
+ resources: catalog?.resources.length ?? 0
239
+ }
240
+ };
241
+ }
242
+
243
+ repairMirrors(root: string): AuthorityRepairReport {
244
+ this.ensureInitialized(root);
245
+ const repairedMirrors: string[] = [];
246
+
247
+ const catalogBefore = this.readOptionalText(this.catalogMirrorPath(root));
248
+ const catalog = this.openSeal<AuthorityCatalog>(root, this.catalogSealPath(root), "authority.catalog");
249
+ this.writeMirror(this.catalogMirrorPath(root), catalog);
250
+ if (catalogBefore !== this.readOptionalText(this.catalogMirrorPath(root))) {
251
+ repairedMirrors.push("catalog.mirror.json");
252
+ }
253
+
254
+ for (const view of catalog.views) {
255
+ const mirrorPath = this.viewMirrorPath(root, view.viewId);
256
+ const before = this.readOptionalText(mirrorPath);
257
+ const contract = this.openSeal<AuthorityViewContract>(root, this.viewSealPath(root, view.viewId), "authority.viewContract");
258
+ this.writeMirror(mirrorPath, contract);
259
+ if (before !== this.readOptionalText(mirrorPath)) {
260
+ repairedMirrors.push(`views/${view.viewId}/mirror.json`);
261
+ }
262
+ }
263
+
264
+ if (this.hasLedger(root)) {
265
+ const ledgerBefore = this.readOptionalText(this.ledgerMirrorPath(root));
266
+ const ledger = this.openSeal<AuthorityLedger>(root, this.ledgerSealPath(root), "authority.ledger");
267
+ this.writeMirror(this.ledgerMirrorPath(root), ledger);
268
+ if (ledgerBefore !== this.readOptionalText(this.ledgerMirrorPath(root))) {
269
+ repairedMirrors.push("ledger.mirror.json");
270
+ }
271
+ }
272
+
273
+ if (repairedMirrors.length > 0) {
274
+ this.appendEvent(root, {
275
+ schemaVersion: 1,
276
+ event: "MIRROR_REBUILT",
277
+ eventId: randomUUID(),
278
+ createdAt: new Date().toISOString(),
279
+ details: { repairedMirrors }
280
+ });
281
+ }
282
+
283
+ return { repairedMirrors };
284
+ }
285
+
286
+ readCatalog(root: string): AuthorityCatalog {
287
+ this.ensureInitializedNoCatalogRead(root);
288
+ return this.openSeal<AuthorityCatalog>(root, this.catalogSealPath(root), "authority.catalog");
289
+ }
290
+
291
+ hasLedger(root: string): boolean {
292
+ return this.files.pathExists(this.ledgerSealPath(root));
293
+ }
294
+
295
+ bootstrapLedger(root: string, parentTrustedCommit: string): AuthorityLedger {
296
+ this.ensureInitialized(root);
297
+ if (this.hasLedger(root)) {
298
+ throw new AwbsError("Trusted ledger is already bootstrapped.");
299
+ }
300
+ const now = new Date().toISOString();
301
+ const repo = this.readRepo(root);
302
+ const entryBase: Omit<AuthorityLedgerEntry, "entryHash"> = {
303
+ schemaVersion: 1,
304
+ entryId: randomUUID(),
305
+ kind: "bootstrap",
306
+ previousEntryHash: null,
307
+ parentTrustedCommit,
308
+ baseCommit: parentTrustedCommit,
309
+ changesetId: null,
310
+ viewId: null,
311
+ createdAt: now,
312
+ appliedPaths: [],
313
+ changesetManifestHash: null,
314
+ changesetPayloadHash: null,
315
+ authorityContractHash: null,
316
+ operationHash: contentHash({
317
+ kind: "bootstrap",
318
+ parentTrustedCommit,
319
+ repoId: repo.repoId,
320
+ createdAt: now
321
+ }),
322
+ ext: {}
323
+ };
324
+ const entry: AuthorityLedgerEntry = {
325
+ ...entryBase,
326
+ entryHash: ledgerEntryHash(entryBase)
327
+ };
328
+ const ledger: AuthorityLedger = {
329
+ schemaVersion: 1,
330
+ repoId: repo.repoId,
331
+ ledgerVersion: 1,
332
+ createdAt: now,
333
+ updatedAt: now,
334
+ headEntryId: entry.entryId,
335
+ entries: [entry],
336
+ ext: {}
337
+ };
338
+ this.writeLedger(root, ledger);
339
+ this.appendLedgerEvent(root, {
340
+ schemaVersion: 1,
341
+ event: "LEDGER_BOOTSTRAPPED",
342
+ eventId: randomUUID(),
343
+ createdAt: now,
344
+ details: { parentTrustedCommit, entryId: entry.entryId }
345
+ });
346
+ return ledger;
347
+ }
348
+
349
+ readLedger(root: string): AuthorityLedger {
350
+ this.ensureInitializedNoCatalogRead(root);
351
+ return this.openSeal<AuthorityLedger>(root, this.ledgerSealPath(root), "authority.ledger");
352
+ }
353
+
354
+ recordChangesetApply(root: string, operation: AuthorityChangesetApplyOperation): AuthorityLedgerEntry {
355
+ const ledger = this.readLedger(root);
356
+ if (operation.schemaVersion !== 1) {
357
+ throw new AwbsError("Invalid changeset apply operation schema.");
358
+ }
359
+ const entryId = randomUUID();
360
+ const createdAt = operation.createdAt ?? new Date().toISOString();
361
+ const headEntry = ledger.entries.find((existing) => existing.entryId === ledger.headEntryId);
362
+ if (!headEntry) {
363
+ throw new AwbsError("Trusted ledger head entry is missing.");
364
+ }
365
+ const entryBase: Omit<AuthorityLedgerEntry, "entryHash"> = {
366
+ schemaVersion: 1,
367
+ entryId,
368
+ kind: "changeset",
369
+ previousEntryHash: headEntry.entryHash,
370
+ parentTrustedCommit: operation.parentTrustedCommit,
371
+ baseCommit: operation.baseCommit,
372
+ changesetId: operation.changesetId,
373
+ viewId: operation.viewId,
374
+ createdAt,
375
+ appliedPaths: operation.appliedPaths,
376
+ changesetManifestHash: operation.changesetManifestHash,
377
+ changesetPayloadHash: operation.changesetPayloadHash,
378
+ authorityContractHash: operation.authorityContractHash,
379
+ operationHash: contentHash({
380
+ kind: "changeset",
381
+ parentTrustedCommit: operation.parentTrustedCommit,
382
+ baseCommit: operation.baseCommit,
383
+ changesetId: operation.changesetId,
384
+ viewId: operation.viewId,
385
+ appliedPaths: operation.appliedPaths,
386
+ changesetManifestHash: operation.changesetManifestHash,
387
+ changesetPayloadHash: operation.changesetPayloadHash,
388
+ authorityContractHash: operation.authorityContractHash
389
+ }),
390
+ ext: operation.ext
391
+ };
392
+ const entry: AuthorityLedgerEntry = {
393
+ ...entryBase,
394
+ entryHash: ledgerEntryHash(entryBase)
395
+ };
396
+ const now = new Date().toISOString();
397
+ const nextLedger: AuthorityLedger = {
398
+ ...ledger,
399
+ ledgerVersion: ledger.ledgerVersion + 1,
400
+ updatedAt: now,
401
+ headEntryId: entry.entryId,
402
+ entries: [...ledger.entries, entry]
403
+ };
404
+ this.writeLedger(root, nextLedger);
405
+ this.appendLedgerEvent(root, {
406
+ schemaVersion: 1,
407
+ event: "LEDGER_ENTRY_APPENDED",
408
+ eventId: randomUUID(),
409
+ createdAt: now,
410
+ viewId: entry.viewId ?? undefined,
411
+ details: {
412
+ entryId: entry.entryId,
413
+ changesetId: entry.changesetId,
414
+ parentTrustedCommit: entry.parentTrustedCommit
415
+ }
416
+ });
417
+ return entry;
418
+ }
419
+
420
+ sealChangesetReceipt(root: string, changesetRoot: string, receipt: AuthorityChangesetReceipt): AuthorityChangesetReceipt {
421
+ this.ensureInitializedNoCatalogRead(root);
422
+ this.assertChangesetReceiptScope(root, changesetRoot, receipt);
423
+ const receiptPath = this.changesetReceiptPath(changesetRoot);
424
+ this.writeSeal(root, receiptPath, "authority.changesetReceipt", receipt, {
425
+ changesetId: receipt.changesetId,
426
+ viewId: receipt.viewId,
427
+ baseCommit: receipt.baseCommit
428
+ });
429
+ return receipt;
430
+ }
431
+
432
+ openChangesetReceipt(root: string, changesetRoot: string): AuthorityChangesetReceipt {
433
+ this.ensureInitializedNoCatalogRead(root);
434
+ const manifest = this.files.readJson<ChangesetManifest>(join(changesetRoot, "manifest.json"));
435
+ const receipt = this.openSeal<AuthorityChangesetReceipt>(root, this.changesetReceiptPath(changesetRoot), "authority.changesetReceipt");
436
+ this.assertChangesetReceiptScope(root, changesetRoot, receipt, manifest);
437
+ return receipt;
438
+ }
439
+
440
+ private ensureInitializedNoCatalogRead(root: string): void {
441
+ this.files.ensureDir(join(root, ".awbs", "authority", "views"));
442
+ this.files.ensureDir(join(root, ".awbs", "private"));
443
+ if (!this.files.pathExists(this.repoPath(root))) {
444
+ throw new AwbsError("Authority repo is not initialized. Run `awbs init` first.");
445
+ }
446
+ if (!this.hasLocalMaterial(root)) {
447
+ throw new AwbsError("Authority local material is missing. Run `awbs init` or restore .awbs/private/local.json.");
448
+ }
449
+ }
450
+
451
+ private writeCatalog(root: string, catalog: AuthorityCatalog): void {
452
+ this.writeSeal(root, this.catalogSealPath(root), "authority.catalog", catalog, {
453
+ repoId: catalog.repoId,
454
+ schemaVersion: catalog.schemaVersion,
455
+ catalogVersion: catalog.catalogVersion
456
+ });
457
+ this.writeMirror(this.catalogMirrorPath(root), catalog);
458
+ }
459
+
460
+ private writeLedger(root: string, ledger: AuthorityLedger): void {
461
+ this.writeSeal(root, this.ledgerSealPath(root), "authority.ledger", ledger, {
462
+ repoId: ledger.repoId,
463
+ schemaVersion: ledger.schemaVersion,
464
+ ledgerVersion: ledger.ledgerVersion,
465
+ headEntryId: ledger.headEntryId
466
+ });
467
+ this.writeMirror(this.ledgerMirrorPath(root), ledger);
468
+ }
469
+
470
+ private writeViewContract(root: string, contract: AuthorityViewContract): void {
471
+ this.writeSeal(root, this.viewSealPath(root, contract.viewId), "authority.viewContract", contract, {
472
+ viewId: contract.viewId,
473
+ schemaVersion: contract.schemaVersion,
474
+ baseCommit: contract.baseCommit
475
+ });
476
+ this.writeMirror(this.viewMirrorPath(root, contract.viewId), contract);
477
+ this.files.writeJson(this.viewReceiptPath(root, contract.viewId), {
478
+ schemaVersion: 1,
479
+ viewId: contract.viewId,
480
+ payloadType: "authority.viewContract",
481
+ algorithm: "AWBS-AES-256-GCM-v1",
482
+ contentHash: contentHash(contract),
483
+ createdAt: contract.createdAt,
484
+ ext: {}
485
+ } satisfies AuthorityReceipt);
486
+ }
487
+
488
+ private openViewContract(root: string, viewId: string): AuthorityViewContract {
489
+ return this.openSeal<AuthorityViewContract>(root, this.viewSealPath(root, viewId), "authority.viewContract");
490
+ }
491
+
492
+ private writeSeal(root: string, path: string, payloadType: "authority.catalog", payload: AuthorityCatalog, aad: Record<string, unknown>): void;
493
+ private writeSeal(root: string, path: string, payloadType: "authority.viewContract", payload: AuthorityViewContract, aad: Record<string, unknown>): void;
494
+ private writeSeal(root: string, path: string, payloadType: "authority.ledger", payload: AuthorityLedger, aad: Record<string, unknown>): void;
495
+ private writeSeal(root: string, path: string, payloadType: "authority.changesetReceipt", payload: AuthorityChangesetReceipt, aad: Record<string, unknown>): void;
496
+ private writeSeal(
497
+ root: string,
498
+ path: string,
499
+ payloadType: AuthorityPayloadType,
500
+ payload: AuthorityCatalog | AuthorityViewContract | AuthorityLedger | AuthorityChangesetReceipt,
501
+ aad: Record<string, unknown>
502
+ ): void {
503
+ const key = this.deriveKey(root);
504
+ const nonce = randomBytes(12);
505
+ const plaintext = Buffer.from(canonicalJson(payload), "utf8");
506
+ const aadText = canonicalJson({ payloadType, ...aad });
507
+ const cipher = createCipheriv("aes-256-gcm", key, nonce);
508
+ cipher.setAAD(Buffer.from(aadText, "utf8"));
509
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
510
+ const envelope: SealEnvelope = {
511
+ schemaVersion: 1,
512
+ sealType: "awbs.seal.v1",
513
+ payloadType,
514
+ aad: { payloadType, ...aad },
515
+ nonce: nonce.toString("base64"),
516
+ ciphertext: ciphertext.toString("base64"),
517
+ tag: cipher.getAuthTag().toString("base64"),
518
+ contentHash: sha256String(plaintext.toString("utf8"))
519
+ };
520
+ this.files.writeJson(path, envelope);
521
+ }
522
+
523
+ private openSeal<T>(root: string, path: string, expectedPayloadType: AuthorityPayloadType): T {
524
+ if (!this.files.pathExists(path)) {
525
+ throw new AwbsError(`Sealed authority payload not found: ${path}`);
526
+ }
527
+ const envelope = this.files.readJson<SealEnvelope>(path);
528
+ if (envelope.sealType !== "awbs.seal.v1" || envelope.payloadType !== expectedPayloadType) {
529
+ throw new AwbsError(`Invalid authority seal envelope: ${path}`);
530
+ }
531
+
532
+ try {
533
+ const key = this.deriveKey(root);
534
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(envelope.nonce, "base64"));
535
+ decipher.setAAD(Buffer.from(canonicalJson(envelope.aad), "utf8"));
536
+ decipher.setAuthTag(Buffer.from(envelope.tag, "base64"));
537
+ const plaintext = Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext, "base64")), decipher.final()]).toString("utf8");
538
+ if (sha256String(plaintext) !== envelope.contentHash) {
539
+ throw new AwbsError(`Authority content hash mismatch: ${path}`);
540
+ }
541
+ return JSON.parse(plaintext) as T;
542
+ } catch (error) {
543
+ if (error instanceof AwbsError) {
544
+ throw error;
545
+ }
546
+ throw new AwbsError(`Failed to open sealed authority payload: ${path}`);
547
+ }
548
+ }
549
+
550
+ private deriveKey(root: string): Buffer {
551
+ const repo = this.readRepo(root);
552
+ const local = this.readLocal(root);
553
+ const password = `${local.localSealSeed}:${local.installationId}:${repo.repoId}:${RUNTIME_PEPPER}`;
554
+ return scryptSync(password, Buffer.from(repo.authoritySalt, "base64"), 32, { N: 16384, r: 8, p: 1 });
555
+ }
556
+
557
+ private readRepo(root: string): AuthorityRepo {
558
+ return this.files.readJson<AuthorityRepo>(this.repoPath(root));
559
+ }
560
+
561
+ private assertRepoTrustMode(root: string): void {
562
+ const repo = this.readRepo(root);
563
+ if (repo.trustMode !== "ephemeral-local-key-v1") {
564
+ throw new AwbsError("Authority repo trustMode is not ephemeral-local-key-v1. Reinitialize this development database with the current AWBS version.");
565
+ }
566
+ }
567
+
568
+ private readLocal(root: string): AuthorityLocal {
569
+ if (this.memoryLocal) {
570
+ return this.memoryLocal;
571
+ }
572
+ return this.files.readJson<AuthorityLocal>(this.localPath(root));
573
+ }
574
+
575
+ private hasLocalMaterial(root: string): boolean {
576
+ return Boolean(this.memoryLocal) || this.files.pathExists(this.localPath(root));
577
+ }
578
+
579
+ private appendEvent(root: string, event: AuthorityEvent): void {
580
+ const path = this.eventsPath(root);
581
+ const existing = this.files.pathExists(path) ? this.files.readText(path) : "";
582
+ this.files.writeText(path, `${existing}${JSON.stringify(event)}\n`);
583
+ }
584
+
585
+ private appendLedgerEvent(root: string, event: AuthorityEvent): void {
586
+ const path = this.ledgerEventsPath(root);
587
+ const existing = this.files.pathExists(path) ? this.files.readText(path) : "";
588
+ this.files.writeText(path, `${existing}${JSON.stringify(event)}\n`);
589
+ }
590
+
591
+ private writeMirror(path: string, value: unknown): void {
592
+ this.files.writeJson(path, value);
593
+ }
594
+
595
+ private readOptionalText(path: string): string | null {
596
+ return this.files.pathExists(path) ? this.files.readText(path) : null;
597
+ }
598
+
599
+ private repoPath(root: string): string {
600
+ return join(root, ".awbs", "authority", "repo.json");
601
+ }
602
+
603
+ private localPath(root: string): string {
604
+ return join(root, ".awbs", "private", "local.json");
605
+ }
606
+
607
+ private eventsPath(root: string): string {
608
+ return join(root, ".awbs", "authority", "view-events.jsonl");
609
+ }
610
+
611
+ private ledgerEventsPath(root: string): string {
612
+ return join(root, ".awbs", "authority", "ledger-events.jsonl");
613
+ }
614
+
615
+ private catalogSealPath(root: string): string {
616
+ return join(root, ".awbs", "authority", "catalog.seal.json");
617
+ }
618
+
619
+ private catalogMirrorPath(root: string): string {
620
+ return join(root, ".awbs", "authority", "catalog.mirror.json");
621
+ }
622
+
623
+ private ledgerSealPath(root: string): string {
624
+ return join(root, ".awbs", "authority", "ledger.seal.json");
625
+ }
626
+
627
+ private ledgerMirrorPath(root: string): string {
628
+ return join(root, ".awbs", "authority", "ledger.mirror.json");
629
+ }
630
+
631
+ private viewDir(root: string, viewId: string): string {
632
+ return join(root, ".awbs", "authority", "views", viewId);
633
+ }
634
+
635
+ private viewSealPath(root: string, viewId: string): string {
636
+ return join(this.viewDir(root, viewId), "contract.seal.json");
637
+ }
638
+
639
+ private viewMirrorPath(root: string, viewId: string): string {
640
+ return join(this.viewDir(root, viewId), "mirror.json");
641
+ }
642
+
643
+ private viewReceiptPath(root: string, viewId: string): string {
644
+ return join(this.viewDir(root, viewId), "receipt.json");
645
+ }
646
+
647
+ private changesetReceiptPath(changesetRoot: string): string {
648
+ return join(changesetRoot, "receipt.seal.json");
649
+ }
650
+
651
+ private assertChangesetReceiptScope(root: string, changesetRoot: string, receipt: AuthorityChangesetReceipt, manifest?: ChangesetManifest): void {
652
+ const expectedRoot = resolve(root, ".awbs", "changesets", receipt.changesetId);
653
+ if (resolve(changesetRoot) !== expectedRoot) {
654
+ throw new AwbsError(`Changeset receipt path does not match changeset id: ${receipt.changesetId}`);
655
+ }
656
+ const actualManifest = manifest ?? this.files.readJson<ChangesetManifest>(join(changesetRoot, "manifest.json"));
657
+ if (
658
+ actualManifest.changesetId !== receipt.changesetId ||
659
+ actualManifest.viewId !== receipt.viewId ||
660
+ actualManifest.baseCommit !== receipt.baseCommit ||
661
+ actualManifest.payloadHash !== receipt.payloadHash ||
662
+ actualManifest.operationHash !== receipt.operationHash ||
663
+ contentHash(actualManifest) !== receipt.manifestHash
664
+ ) {
665
+ throw new AwbsError(`Changeset receipt does not match manifest: ${receipt.changesetId}`);
666
+ }
667
+ }
668
+ }
669
+
670
+ function mergeResources(existing: AuthorityResource[], sources: AuthorityViewSource[]): AuthorityResource[] {
671
+ const byPath = new Map(existing.map((resource) => [resource.path, resource]));
672
+ for (const source of sources) {
673
+ if (!byPath.has(source.path)) {
674
+ byPath.set(source.path, {
675
+ resourceId: `res_${sha256String(source.path).slice("sha256:".length, "sha256:".length + 12)}`,
676
+ path: source.path,
677
+ kind: source.kind,
678
+ parent: parentPath(source.path),
679
+ defaultMode: "read",
680
+ ext: {}
681
+ });
682
+ }
683
+ }
684
+ return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
685
+ }
686
+
687
+ function parentPath(path: string): string | null {
688
+ const index = path.lastIndexOf("/");
689
+ return index <= 0 ? null : path.slice(0, index);
690
+ }
691
+
692
+ function contentHash(value: unknown): string {
693
+ return sha256String(canonicalJson(value));
694
+ }
695
+
696
+ function ledgerEntryHash(entry: AuthorityLedgerEntry | Omit<AuthorityLedgerEntry, "entryHash">): string {
697
+ const { entryHash: _entryHash, ...hashable } = entry as AuthorityLedgerEntry;
698
+ return contentHash(hashable);
699
+ }
700
+
701
+ function mirrorText(value: unknown): string {
702
+ return `${JSON.stringify(value, null, 2)}\n`;
703
+ }
704
+
705
+ function sha256String(value: string): string {
706
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
707
+ }
708
+
709
+ function canonicalJson(value: unknown): string {
710
+ return JSON.stringify(sortForCanonicalJson(value));
711
+ }
712
+
713
+ function sortForCanonicalJson(value: unknown): unknown {
714
+ if (Array.isArray(value)) {
715
+ return value.map(sortForCanonicalJson);
716
+ }
717
+ if (value && typeof value === "object") {
718
+ const result: Record<string, unknown> = {};
719
+ for (const key of Object.keys(value).sort()) {
720
+ result[key] = sortForCanonicalJson((value as Record<string, unknown>)[key]);
721
+ }
722
+ return result;
723
+ }
724
+ return value;
725
+ }