@noy-db/hub 0.6.0-pre.19 → 0.6.0-pre.20

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 (33) hide show
  1. package/dist/broker/index.js +3 -3
  2. package/dist/cargo/index.js +4 -4
  3. package/dist/{chunk-HGAR7H5M.js → chunk-6TXUVKPG.js} +4 -4
  4. package/dist/{chunk-LAFP5H5U.js → chunk-F5AJZC3U.js} +2 -2
  5. package/dist/{chunk-37P5VB4N.js → chunk-PNAIDIP7.js} +2 -2
  6. package/dist/{chunk-E6WANBD6.js → chunk-RKDH6L53.js} +34 -6
  7. package/dist/{chunk-E6WANBD6.js.map → chunk-RKDH6L53.js.map} +1 -1
  8. package/dist/{chunk-YEZW6UJA.js → chunk-RQY5UWEL.js} +3 -3
  9. package/dist/{chunk-LBDTQM3R.js → chunk-VEX2BQVX.js} +2 -2
  10. package/dist/{chunk-HUYNPLTT.js → chunk-XGQGWZ6D.js} +3 -3
  11. package/dist/{chunk-I26TE27S.js → chunk-ZXW3X3DQ.js} +3 -3
  12. package/dist/custody/index.js +2 -2
  13. package/dist/index.js +8 -8
  14. package/dist/{liberate-7QY7TULB.js → liberate-I6AOLWXW.js} +3 -3
  15. package/dist/{noydb-VZIVPDXG.js → noydb-SS6EN7TK.js} +5 -5
  16. package/dist/pod/index.js +5 -5
  17. package/dist/{seed-2KZDTFS4.js → seed-SGIF3EN4.js} +2 -2
  18. package/dist/team/index.js +3 -3
  19. package/dist/vault-head/index.js +5 -1
  20. package/dist/vault-head/index.js.map +1 -1
  21. package/dist/with-commit/vault-head/index.d.ts +1 -1
  22. package/dist/with-commit/vault-head/verify.d.ts +42 -2
  23. package/package.json +3 -3
  24. /package/dist/{chunk-HGAR7H5M.js.map → chunk-6TXUVKPG.js.map} +0 -0
  25. /package/dist/{chunk-LAFP5H5U.js.map → chunk-F5AJZC3U.js.map} +0 -0
  26. /package/dist/{chunk-37P5VB4N.js.map → chunk-PNAIDIP7.js.map} +0 -0
  27. /package/dist/{chunk-YEZW6UJA.js.map → chunk-RQY5UWEL.js.map} +0 -0
  28. /package/dist/{chunk-LBDTQM3R.js.map → chunk-VEX2BQVX.js.map} +0 -0
  29. /package/dist/{chunk-HUYNPLTT.js.map → chunk-XGQGWZ6D.js.map} +0 -0
  30. /package/dist/{chunk-I26TE27S.js.map → chunk-ZXW3X3DQ.js.map} +0 -0
  31. /package/dist/{liberate-7QY7TULB.js.map → liberate-I6AOLWXW.js.map} +0 -0
  32. /package/dist/{noydb-VZIVPDXG.js.map → noydb-SS6EN7TK.js.map} +0 -0
  33. /package/dist/{seed-2KZDTFS4.js.map → seed-SGIF3EN4.js.map} +0 -0
@@ -11,7 +11,7 @@ import {
11
11
  } from "./chunk-2IK6AC6A.js";
12
12
  import {
13
13
  createOwnerKeyring
14
- } from "./chunk-E6WANBD6.js";
14
+ } from "./chunk-RKDH6L53.js";
15
15
  import {
16
16
  diff
17
17
  } from "./chunk-SVLR4POP.js";
@@ -404,7 +404,7 @@ async function createOwnerOnAdoptedPartition(store, vaultName, opts) {
404
404
  }
405
405
  }
406
406
  if (isManaged(opts)) {
407
- const { createNoydb } = await import("./noydb-VZIVPDXG.js");
407
+ const { createNoydb } = await import("./noydb-SS6EN7TK.js");
408
408
  const db = await createNoydb({
409
409
  store,
410
410
  user: userId,
@@ -460,4 +460,4 @@ export {
460
460
  withCargo,
461
461
  describeExtraction
462
462
  };
463
- //# sourceMappingURL=chunk-YEZW6UJA.js.map
463
+ //# sourceMappingURL=chunk-RQY5UWEL.js.map
@@ -8,7 +8,7 @@ import {
8
8
  rotateKeys,
9
9
  verifyPrompt,
10
10
  verifyTypedEcho
11
- } from "./chunk-E6WANBD6.js";
11
+ } from "./chunk-RKDH6L53.js";
12
12
  import {
13
13
  NoAccessError,
14
14
  NoydbError,
@@ -114,4 +114,4 @@ export {
114
114
  MemoryDeviceSealProvider,
115
115
  withTeam
116
116
  };
117
- //# sourceMappingURL=chunk-LBDTQM3R.js.map
117
+ //# sourceMappingURL=chunk-VEX2BQVX.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  grant,
3
3
  revoke
4
- } from "./chunk-E6WANBD6.js";
4
+ } from "./chunk-RKDH6L53.js";
5
5
  import {
6
6
  CustodyNotEnabledError
7
7
  } from "./chunk-P7L3OQLL.js";
@@ -16,7 +16,7 @@ function withCustody() {
16
16
  return host._revokeCustodianImpl(revoke, vault, options, factors);
17
17
  },
18
18
  async liberate(vault, opts) {
19
- const { liberateVault } = await import("./liberate-7QY7TULB.js");
19
+ const { liberateVault } = await import("./liberate-I6AOLWXW.js");
20
20
  return liberateVault(vault, opts);
21
21
  }
22
22
  };
@@ -74,4 +74,4 @@ export {
74
74
  withCustody,
75
75
  CustodyApi
76
76
  };
77
- //# sourceMappingURL=chunk-HUYNPLTT.js.map
77
+ //# sourceMappingURL=chunk-XGQGWZ6D.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createNoydb
3
- } from "./chunk-HGAR7H5M.js";
3
+ } from "./chunk-6TXUVKPG.js";
4
4
  import {
5
5
  deriveSchemaManifest
6
6
  } from "./chunk-E6VDPBE3.js";
@@ -16,7 +16,7 @@ import {
16
16
  } from "./chunk-2IK6AC6A.js";
17
17
  import {
18
18
  loadKeyring
19
- } from "./chunk-E6WANBD6.js";
19
+ } from "./chunk-RKDH6L53.js";
20
20
  import {
21
21
  ConflictError,
22
22
  MigrationRequiredError,
@@ -286,4 +286,4 @@ export {
286
286
  followRedirects,
287
287
  open
288
288
  };
289
- //# sourceMappingURL=chunk-I26TE27S.js.map
289
+ //# sourceMappingURL=chunk-ZXW3X3DQ.js.map
@@ -2,8 +2,8 @@ import {
2
2
  CustodyApi,
3
3
  NO_CUSTODY,
4
4
  withCustody
5
- } from "../chunk-HUYNPLTT.js";
6
- import "../chunk-E6WANBD6.js";
5
+ } from "../chunk-XGQGWZ6D.js";
6
+ import "../chunk-RKDH6L53.js";
7
7
  import "../chunk-JHO6FGZX.js";
8
8
  import "../chunk-D5UDZANO.js";
9
9
  import "../chunk-ZVAFJQZB.js";
package/dist/index.js CHANGED
@@ -57,14 +57,14 @@ import {
57
57
  decryptExtractedPartition,
58
58
  diffVault,
59
59
  withCargo
60
- } from "./chunk-YEZW6UJA.js";
60
+ } from "./chunk-RQY5UWEL.js";
61
61
  import "./chunk-CV7WXCZD.js";
62
62
  import {
63
63
  createDeedOwner,
64
64
  isDeedVault,
65
65
  liberateVault,
66
66
  loadDeedMarker
67
- } from "./chunk-37P5VB4N.js";
67
+ } from "./chunk-PNAIDIP7.js";
68
68
  import {
69
69
  withdrawAccessibleData
70
70
  } from "./chunk-TC6MBEGH.js";
@@ -121,7 +121,7 @@ import {
121
121
  signRedirect,
122
122
  verifyRedirect,
123
123
  wrapPodStore
124
- } from "./chunk-I26TE27S.js";
124
+ } from "./chunk-ZXW3X3DQ.js";
125
125
  import {
126
126
  Collection,
127
127
  ELEVATION_AUDIT_COLLECTION,
@@ -140,7 +140,7 @@ import {
140
140
  refArray,
141
141
  via,
142
142
  withArchive
143
- } from "./chunk-HGAR7H5M.js";
143
+ } from "./chunk-6TXUVKPG.js";
144
144
  import {
145
145
  LinkIntegrityError,
146
146
  isLinkCollectionName
@@ -201,7 +201,7 @@ import {
201
201
  CustodyApi,
202
202
  NO_CUSTODY,
203
203
  withCustody
204
- } from "./chunk-HUYNPLTT.js";
204
+ } from "./chunk-XGQGWZ6D.js";
205
205
  import "./chunk-RSFG7XHM.js";
206
206
  import {
207
207
  buildLookupSnapshot
@@ -366,7 +366,7 @@ import {
366
366
  MemoryDeviceSealProvider,
367
367
  beginEchoUnlock,
368
368
  withTeam
369
- } from "./chunk-LBDTQM3R.js";
369
+ } from "./chunk-VEX2BQVX.js";
370
370
  import {
371
371
  MAGIC_LINK_GRANTS_COLLECTION,
372
372
  NO_TEAM,
@@ -395,7 +395,7 @@ import {
395
395
  unwrapDeksFromPaperEntry,
396
396
  unwrapMagicLinkGrant,
397
397
  writeMagicLinkGrant
398
- } from "./chunk-LAFP5H5U.js";
398
+ } from "./chunk-F5AJZC3U.js";
399
399
  import {
400
400
  META_COLLECTION,
401
401
  POLICY_RECORD_ID,
@@ -424,7 +424,7 @@ import {
424
424
  validateSecret,
425
425
  verifyPrompt,
426
426
  verifyTypedEcho
427
- } from "./chunk-E6WANBD6.js";
427
+ } from "./chunk-RKDH6L53.js";
428
428
  import {
429
429
  DIRECTORY_RECORD_ID,
430
430
  VISIBILITY_RECORD_PREFIX,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  liberateVault
3
- } from "./chunk-37P5VB4N.js";
3
+ } from "./chunk-PNAIDIP7.js";
4
4
  import "./chunk-TC6MBEGH.js";
5
5
  import "./chunk-ZPMXYPBR.js";
6
6
  import "./chunk-NRNTXBHT.js";
7
7
  import "./chunk-2IK6AC6A.js";
8
8
  import "./chunk-DYASJSKI.js";
9
- import "./chunk-E6WANBD6.js";
9
+ import "./chunk-RKDH6L53.js";
10
10
  import "./chunk-JHO6FGZX.js";
11
11
  import "./chunk-D5UDZANO.js";
12
12
  import "./chunk-ZVAFJQZB.js";
@@ -21,4 +21,4 @@ import "./chunk-PZ5AY32C.js";
21
21
  export {
22
22
  liberateVault
23
23
  };
24
- //# sourceMappingURL=liberate-7QY7TULB.js.map
24
+ //# sourceMappingURL=liberate-I6AOLWXW.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Noydb,
3
3
  createNoydb
4
- } from "./chunk-HGAR7H5M.js";
4
+ } from "./chunk-6TXUVKPG.js";
5
5
  import "./chunk-KBOPY6W2.js";
6
6
  import "./chunk-C7WJNVJG.js";
7
7
  import "./chunk-AQF7JSQR.js";
@@ -21,7 +21,7 @@ import "./chunk-A3NTKHDB.js";
21
21
  import "./chunk-NRNTXBHT.js";
22
22
  import "./chunk-YVTADFTC.js";
23
23
  import "./chunk-PZSS7MUM.js";
24
- import "./chunk-HUYNPLTT.js";
24
+ import "./chunk-XGQGWZ6D.js";
25
25
  import "./chunk-RSFG7XHM.js";
26
26
  import "./chunk-ICCRAPWD.js";
27
27
  import "./chunk-W6NFONOV.js";
@@ -52,9 +52,9 @@ import "./chunk-PG6PFMDC.js";
52
52
  import "./chunk-GYUCRDAU.js";
53
53
  import "./chunk-437FDIHO.js";
54
54
  import "./chunk-KDX6VEYV.js";
55
- import "./chunk-LAFP5H5U.js";
55
+ import "./chunk-F5AJZC3U.js";
56
56
  import "./chunk-DKYEVHZH.js";
57
- import "./chunk-E6WANBD6.js";
57
+ import "./chunk-RKDH6L53.js";
58
58
  import "./chunk-JHO6FGZX.js";
59
59
  import "./chunk-HTMQBBF3.js";
60
60
  import "./chunk-KM6YR5XF.js";
@@ -81,4 +81,4 @@ export {
81
81
  Noydb,
82
82
  createNoydb
83
83
  };
84
- //# sourceMappingURL=noydb-VZIVPDXG.js.map
84
+ //# sourceMappingURL=noydb-SS6EN7TK.js.map
package/dist/pod/index.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  signRedirect,
6
6
  verifyRedirect,
7
7
  wrapPodStore
8
- } from "../chunk-I26TE27S.js";
9
- import "../chunk-HGAR7H5M.js";
8
+ } from "../chunk-ZXW3X3DQ.js";
9
+ import "../chunk-6TXUVKPG.js";
10
10
  import "../chunk-KBOPY6W2.js";
11
11
  import "../chunk-C7WJNVJG.js";
12
12
  import "../chunk-AQF7JSQR.js";
@@ -33,7 +33,7 @@ import "../chunk-A3NTKHDB.js";
33
33
  import "../chunk-NRNTXBHT.js";
34
34
  import "../chunk-YVTADFTC.js";
35
35
  import "../chunk-PZSS7MUM.js";
36
- import "../chunk-HUYNPLTT.js";
36
+ import "../chunk-XGQGWZ6D.js";
37
37
  import "../chunk-RSFG7XHM.js";
38
38
  import "../chunk-ICCRAPWD.js";
39
39
  import "../chunk-W6NFONOV.js";
@@ -89,9 +89,9 @@ import "../chunk-PG6PFMDC.js";
89
89
  import "../chunk-GYUCRDAU.js";
90
90
  import "../chunk-437FDIHO.js";
91
91
  import "../chunk-KDX6VEYV.js";
92
- import "../chunk-LAFP5H5U.js";
92
+ import "../chunk-F5AJZC3U.js";
93
93
  import "../chunk-DKYEVHZH.js";
94
- import "../chunk-E6WANBD6.js";
94
+ import "../chunk-RKDH6L53.js";
95
95
  import "../chunk-JHO6FGZX.js";
96
96
  import "../chunk-HTMQBBF3.js";
97
97
  import "../chunk-KM6YR5XF.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BROKER_COLLECTION,
3
3
  ensureCollectionDEK
4
- } from "./chunk-E6WANBD6.js";
4
+ } from "./chunk-RKDH6L53.js";
5
5
  import "./chunk-JHO6FGZX.js";
6
6
  import {
7
7
  computeBrokerProof,
@@ -225,4 +225,4 @@ export {
225
225
  mintStoreCredentials,
226
226
  rotateSeed
227
227
  };
228
- //# sourceMappingURL=seed-2KZDTFS4.js.map
228
+ //# sourceMappingURL=seed-SGIF3EN4.js.map
@@ -2,7 +2,7 @@ import {
2
2
  MemoryDeviceSealProvider,
3
3
  beginEchoUnlock,
4
4
  withTeam
5
- } from "../chunk-LBDTQM3R.js";
5
+ } from "../chunk-VEX2BQVX.js";
6
6
  import {
7
7
  NO_TEAM,
8
8
  burnPaperRecoveryEntry,
@@ -31,7 +31,7 @@ import {
31
31
  unwrapMagicLinkGrant,
32
32
  updateAuthenticator,
33
33
  writeMagicLinkGrant
34
- } from "../chunk-LAFP5H5U.js";
34
+ } from "../chunk-F5AJZC3U.js";
35
35
  import "../chunk-DKYEVHZH.js";
36
36
  import {
37
37
  buildEchoBlock,
@@ -53,7 +53,7 @@ import {
53
53
  updateKeyringIdentity,
54
54
  verifyPrompt,
55
55
  verifyTypedEcho
56
- } from "../chunk-E6WANBD6.js";
56
+ } from "../chunk-RKDH6L53.js";
57
57
  import "../chunk-JHO6FGZX.js";
58
58
  import "../chunk-HTMQBBF3.js";
59
59
  import "../chunk-47ZFULEZ.js";
@@ -89,6 +89,9 @@ function withVaultHead(opts = {}) {
89
89
  async function verifyVaultHead(head, store, vault, getDEK, collection) {
90
90
  const known = await head.knownIn(store, vault, getDEK, collection);
91
91
  const discrepancies = [];
92
+ const because = [];
93
+ if (known.size === 0) because.push("no-expectations");
94
+ if (store.capabilities?.casAtomic !== true) because.push("store-cannot-cas");
92
95
  for (const [id, expected] of known) {
93
96
  const env = await store.get(vault, collection, id);
94
97
  if (env === null) {
@@ -99,7 +102,8 @@ async function verifyVaultHead(head, store, vault, getDEK, collection) {
99
102
  discrepancies.push({ collection, id, expected, actual: env._v, kind: "rolled-back" });
100
103
  }
101
104
  }
102
- return { checked: known.size, discrepancies, clean: discrepancies.length === 0 };
105
+ const verdict = discrepancies.length > 0 ? "tampered" : because.length > 0 ? "unverifiable" : "verified";
106
+ return { verdict, checked: known.size, discrepancies, because };
103
107
  }
104
108
  export {
105
109
  DEFAULT_HEAD_BUCKETS,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/with-commit/vault-head/head.ts","../../src/with-commit/vault-head/active.ts","../../src/with-commit/vault-head/verify.ts"],"sourcesContent":["/**\n * The vault head's storage layer (#1044).\n *\n * A bucket is an ordinary encrypted record at `_head/<collection>::<n>`, so it\n * inherits #1041's identity binding for free: the store cannot relocate a\n * bucket, re-author it, or serve one bucket's bytes as another's. What it can\n * still do is serve an OLD bucket, which is why {@link readBucket} enforces\n * monotonicity against the caller's own record of what it last wrote.\n *\n * @packageDocumentation\n */\nimport { encrypt, buildRecordAad, openEnvelopeJson, buildRecordEnvelope, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { NoydbStore } from '../../kernel/types.js'\nimport { VAULT_HEAD_COLLECTION } from './strategy.js'\n\n/** `{ recordId → version }` for one bucket. */\nexport type BucketBody = Record<string, number>\n\n/**\n * FNV-1a over `collection/id`. A hash, not a cipher: the bucket a record lands\n * in is not secret — the store already sees which bucket was written, and it\n * already saw the record's own address. Choosing something stronger would buy\n * nothing and cost every write.\n */\nexport function bucketIndex(collection: string, id: string, buckets: number): number {\n let h = 0x811c9dc5\n const s = `${collection}/${id}`\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193) >>> 0\n }\n return h % buckets\n}\n\nexport function bucketId(collection: string, id: string, buckets: number): string {\n return `${collection}::${String(bucketIndex(collection, id, buckets))}`\n}\n\nexport async function readBucket(\n store: NoydbStore,\n vault: string,\n dek: EnclaveKey,\n bucket: string,\n): Promise<{ body: BucketBody; version: number }> {\n const env = await store.get(vault, VAULT_HEAD_COLLECTION, bucket)\n if (!env) return { body: {}, version: 0 }\n const json = await openEnvelopeJson({ collection: VAULT_HEAD_COLLECTION, id: bucket }, env, dek)\n return { body: JSON.parse(json) as BucketBody, version: env._v }\n}\n\nexport async function writeBucket(\n store: NoydbStore,\n vault: string,\n dek: EnclaveKey,\n bucket: string,\n body: BucketBody,\n expectedVersion: number,\n): Promise<void> {\n const identity = { collection: VAULT_HEAD_COLLECTION, id: bucket, version: expectedVersion + 1 }\n const json = JSON.stringify(body)\n const { iv, data } = await encrypt(json, dek, buildRecordAad(identity))\n const env = buildRecordEnvelope(identity, { iv, data })\n // CAS: two writers racing on one bucket must not silently lose an entry.\n // A lost head entry is a record the sweep stops expecting — a false clean.\n await store.put(vault, VAULT_HEAD_COLLECTION, bucket, env, expectedVersion || undefined)\n}\n","/**\n * The real {@link VaultHeadStrategy} (#1044).\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport {\n DEFAULT_HEAD_BUCKETS,\n VAULT_HEAD_COLLECTION,\n type HeadEntry,\n type VaultHeadStrategy,\n type WithVaultHeadOptions,\n} from './strategy.js'\nimport { bucketId, readBucket, writeBucket } from './head.js'\n\nexport function withVaultHead(opts: WithVaultHeadOptions = {}): VaultHeadStrategy {\n const buckets = opts.buckets ?? DEFAULT_HEAD_BUCKETS\n\n const dekFor = (getDEK: (c: string) => Promise<EnclaveKey>) => getDEK(VAULT_HEAD_COLLECTION)\n\n return {\n enabled: true,\n bucketFor: (collection, id) => bucketId(collection, id, buckets),\n\n async note(store: NoydbStore, vault, getDEK, entry: HeadEntry) {\n const dek = await dekFor(getDEK)\n const bucket = bucketId(entry.collection, entry.id, buckets)\n const { body, version } = await readBucket(store, vault, dek, bucket)\n const key = `${entry.collection}/${entry.id}`\n // Monotonic: a head entry never moves BACKWARD. A write that would lower\n // it is either a stale retry or a rollback being laundered through the\n // client's own head, and neither should be recorded.\n if ((body[key] ?? -1) >= entry.version) return\n body[key] = entry.version\n await writeBucket(store, vault, dek, bucket, body, version)\n },\n\n async expected(store, vault, getDEK, collection, id) {\n const dek = await dekFor(getDEK)\n const { body } = await readBucket(store, vault, dek, bucketId(collection, id, buckets))\n return body[`${collection}/${id}`] ?? null\n },\n\n async knownIn(store, vault, getDEK, collection) {\n const dek = await dekFor(getDEK)\n const out = new Map<string, number>()\n const prefix = `${collection}/`\n // Sweep every bucket: a record's bucket is a function of its id, so the\n // set for one collection is spread across all of them.\n for (const bucket of await store.list(vault, VAULT_HEAD_COLLECTION)) {\n if (!bucket.startsWith(`${collection}::`)) continue\n const { body } = await readBucket(store, vault, dek, bucket)\n for (const [k, v] of Object.entries(body)) {\n if (k.startsWith(prefix)) out.set(k.slice(prefix.length), v)\n }\n }\n return out\n },\n }\n}\n","/**\n * The sweep — comparing what the head expects against what the store serves (#1044).\n *\n * This is the only place absence becomes visible. #1041 and #1042 can reject a\n * record that lies about itself; neither can notice one that never arrived, or\n * one served at an older version than the client wrote. Both are indistinguishable\n * from the truth without an independent record of what should be there.\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { VaultHeadStrategy } from './strategy.js'\n\n/** One record the store is not serving faithfully. */\nexport interface HeadDiscrepancy {\n readonly collection: string\n readonly id: string\n /** What the client last committed. */\n readonly expected: number\n /**\n * What the store serves now — or `null` when it serves nothing.\n *\n * The two are different attacks wearing the same result. `null` is\n * **suppression**: the record is gone. A lower number is **rollback**: an\n * authentic older version served in place of the current one, which is the\n * case no amount of per-envelope authentication can see.\n */\n readonly actual: number | null\n readonly kind: 'withheld' | 'rolled-back'\n}\n\nexport interface HeadVerifyResult {\n readonly checked: number\n readonly discrepancies: readonly HeadDiscrepancy[]\n /** `true` when every record the head knows about is served at or above its expected version. */\n readonly clean: boolean\n}\n\n/**\n * Sweep one collection.\n *\n * **A record the store serves that the head does NOT know is not reported.**\n * That is not an oversight: the head can be enabled on an existing vault, and\n * every pre-existing record would otherwise read as an anomaly. The head's\n * claim is one-directional — *what I wrote must still be there* — and an\n * unknown record simply falls outside it.\n *\n * **A HIGHER version than expected is also not reported**, for the same reason:\n * another device legitimately advanced it. The head detects going backwards, not\n * moving forwards.\n */\nexport async function verifyVaultHead(\n head: VaultHeadStrategy,\n store: NoydbStore,\n vault: string,\n getDEK: (collection: string) => Promise<EnclaveKey>,\n collection: string,\n): Promise<HeadVerifyResult> {\n const known = await head.knownIn(store, vault, getDEK, collection)\n const discrepancies: HeadDiscrepancy[] = []\n\n for (const [id, expected] of known) {\n const env = await store.get(vault, collection, id)\n if (env === null) {\n discrepancies.push({ collection, id, expected, actual: null, kind: 'withheld' })\n continue\n }\n if (env._v < expected) {\n discrepancies.push({ collection, id, expected, actual: env._v, kind: 'rolled-back' })\n }\n }\n\n return { checked: known.size, discrepancies, clean: discrepancies.length === 0 }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/with-commit/vault-head/head.ts","../../src/with-commit/vault-head/active.ts","../../src/with-commit/vault-head/verify.ts"],"sourcesContent":["/**\n * The vault head's storage layer (#1044).\n *\n * A bucket is an ordinary encrypted record at `_head/<collection>::<n>`, so it\n * inherits #1041's identity binding for free: the store cannot relocate a\n * bucket, re-author it, or serve one bucket's bytes as another's. What it can\n * still do is serve an OLD bucket, which is why {@link readBucket} enforces\n * monotonicity against the caller's own record of what it last wrote.\n *\n * @packageDocumentation\n */\nimport { encrypt, buildRecordAad, openEnvelopeJson, buildRecordEnvelope, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { NoydbStore } from '../../kernel/types.js'\nimport { VAULT_HEAD_COLLECTION } from './strategy.js'\n\n/** `{ recordId → version }` for one bucket. */\nexport type BucketBody = Record<string, number>\n\n/**\n * FNV-1a over `collection/id`. A hash, not a cipher: the bucket a record lands\n * in is not secret — the store already sees which bucket was written, and it\n * already saw the record's own address. Choosing something stronger would buy\n * nothing and cost every write.\n */\nexport function bucketIndex(collection: string, id: string, buckets: number): number {\n let h = 0x811c9dc5\n const s = `${collection}/${id}`\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193) >>> 0\n }\n return h % buckets\n}\n\nexport function bucketId(collection: string, id: string, buckets: number): string {\n return `${collection}::${String(bucketIndex(collection, id, buckets))}`\n}\n\nexport async function readBucket(\n store: NoydbStore,\n vault: string,\n dek: EnclaveKey,\n bucket: string,\n): Promise<{ body: BucketBody; version: number }> {\n const env = await store.get(vault, VAULT_HEAD_COLLECTION, bucket)\n if (!env) return { body: {}, version: 0 }\n const json = await openEnvelopeJson({ collection: VAULT_HEAD_COLLECTION, id: bucket }, env, dek)\n return { body: JSON.parse(json) as BucketBody, version: env._v }\n}\n\nexport async function writeBucket(\n store: NoydbStore,\n vault: string,\n dek: EnclaveKey,\n bucket: string,\n body: BucketBody,\n expectedVersion: number,\n): Promise<void> {\n const identity = { collection: VAULT_HEAD_COLLECTION, id: bucket, version: expectedVersion + 1 }\n const json = JSON.stringify(body)\n const { iv, data } = await encrypt(json, dek, buildRecordAad(identity))\n const env = buildRecordEnvelope(identity, { iv, data })\n // CAS: two writers racing on one bucket must not silently lose an entry.\n // A lost head entry is a record the sweep stops expecting — a false clean.\n await store.put(vault, VAULT_HEAD_COLLECTION, bucket, env, expectedVersion || undefined)\n}\n","/**\n * The real {@link VaultHeadStrategy} (#1044).\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport {\n DEFAULT_HEAD_BUCKETS,\n VAULT_HEAD_COLLECTION,\n type HeadEntry,\n type VaultHeadStrategy,\n type WithVaultHeadOptions,\n} from './strategy.js'\nimport { bucketId, readBucket, writeBucket } from './head.js'\n\nexport function withVaultHead(opts: WithVaultHeadOptions = {}): VaultHeadStrategy {\n const buckets = opts.buckets ?? DEFAULT_HEAD_BUCKETS\n\n const dekFor = (getDEK: (c: string) => Promise<EnclaveKey>) => getDEK(VAULT_HEAD_COLLECTION)\n\n return {\n enabled: true,\n bucketFor: (collection, id) => bucketId(collection, id, buckets),\n\n async note(store: NoydbStore, vault, getDEK, entry: HeadEntry) {\n const dek = await dekFor(getDEK)\n const bucket = bucketId(entry.collection, entry.id, buckets)\n const { body, version } = await readBucket(store, vault, dek, bucket)\n const key = `${entry.collection}/${entry.id}`\n // Monotonic: a head entry never moves BACKWARD. A write that would lower\n // it is either a stale retry or a rollback being laundered through the\n // client's own head, and neither should be recorded.\n if ((body[key] ?? -1) >= entry.version) return\n body[key] = entry.version\n await writeBucket(store, vault, dek, bucket, body, version)\n },\n\n async expected(store, vault, getDEK, collection, id) {\n const dek = await dekFor(getDEK)\n const { body } = await readBucket(store, vault, dek, bucketId(collection, id, buckets))\n return body[`${collection}/${id}`] ?? null\n },\n\n async knownIn(store, vault, getDEK, collection) {\n const dek = await dekFor(getDEK)\n const out = new Map<string, number>()\n const prefix = `${collection}/`\n // Sweep every bucket: a record's bucket is a function of its id, so the\n // set for one collection is spread across all of them.\n for (const bucket of await store.list(vault, VAULT_HEAD_COLLECTION)) {\n if (!bucket.startsWith(`${collection}::`)) continue\n const { body } = await readBucket(store, vault, dek, bucket)\n for (const [k, v] of Object.entries(body)) {\n if (k.startsWith(prefix)) out.set(k.slice(prefix.length), v)\n }\n }\n return out\n },\n }\n}\n","/**\n * The sweep — comparing what the head expects against what the store serves (#1044).\n *\n * This is the only place absence becomes visible. #1041 and #1042 can reject a\n * record that lies about itself; neither can notice one that never arrived, or\n * one served at an older version than the client wrote. Both are indistinguishable\n * from the truth without an independent record of what should be there.\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { VaultHeadStrategy } from './strategy.js'\n\n/** One record the store is not serving faithfully. */\nexport interface HeadDiscrepancy {\n readonly collection: string\n readonly id: string\n /** What the client last committed. */\n readonly expected: number\n /**\n * What the store serves now — or `null` when it serves nothing.\n *\n * The two are different attacks wearing the same result. `null` is\n * **suppression**: the record is gone. A lower number is **rollback**: an\n * authentic older version served in place of the current one, which is the\n * case no amount of per-envelope authentication can see.\n */\n readonly actual: number | null\n readonly kind: 'withheld' | 'rolled-back'\n}\n\n/**\n * Why a sweep could not reach a conclusion.\n *\n * - `'no-expectations'` — the head holds nothing for this collection. A fresh\n * vault, a head switched on after the fact, or a **restore from a snapshot**:\n * `_head` is `_`-prefixed, so `loadAll` excludes it and a restored vault has\n * no head to compare against.\n * - `'store-cannot-cas'` — the store does not advertise `capabilities.casAtomic`,\n * so two writers racing on one bucket can silently lose an entry. A lost entry\n * is a record the sweep stops expecting, which is a false clean.\n */\nexport type HeadUnverifiableReason = 'no-expectations' | 'store-cannot-cas'\n\n/**\n * ## Three-way, deliberately (#1101)\n *\n * - `'verified'` — the head held expectations, every one was met, and the store\n * can serialize head writes.\n * - `'unverifiable'` — the sweep could not conclude. See `because`.\n * - `'tampered'` — at least one discrepancy. Positive evidence, so it wins over\n * any `unverifiable` reason present at the same time.\n *\n * **The middle value is the point, and collapsing it is wrong in a different\n * direction each way:** folded into \"clean\" it hides withholding, which is the\n * one thing this service exists to catch; folded into \"tampered\" it cries wolf\n * on a vault that is merely unexamined.\n *\n * That is not hypothetical. #1044's own first bug was a head that recorded\n * nothing and swept **perfectly clean** — the degraded state rendered\n * identically to a healthy one, which is why its test asserts `checked` before\n * anything else. A two-way verdict makes that state a supported outcome.\n */\nexport type HeadVerdict = 'verified' | 'unverifiable' | 'tampered'\n\nexport interface HeadVerifyResult {\n readonly verdict: HeadVerdict\n readonly checked: number\n readonly discrepancies: readonly HeadDiscrepancy[]\n /**\n * Non-empty only when `verdict === 'unverifiable'`.\n *\n * There is deliberately no `clean: boolean`. It could not distinguish \"no\n * expectations\" from \"all expectations met\" — both rendered `true` — and that\n * indistinguishability *was* the defect. Replacing it rather than keeping it\n * alongside forces every caller to face the three-way.\n */\n readonly because: readonly HeadUnverifiableReason[]\n}\n\n/**\n * Sweep one collection.\n *\n * **A record the store serves that the head does NOT know is not reported.**\n * That is not an oversight: the head can be enabled on an existing vault, and\n * every pre-existing record would otherwise read as an anomaly. The head's\n * claim is one-directional — *what I wrote must still be there* — and an\n * unknown record simply falls outside it.\n *\n * **A HIGHER version than expected is also not reported**, for the same reason:\n * another device legitimately advanced it. The head detects going backwards, not\n * moving forwards.\n */\nexport async function verifyVaultHead(\n head: VaultHeadStrategy,\n store: NoydbStore,\n vault: string,\n getDEK: (collection: string) => Promise<EnclaveKey>,\n collection: string,\n): Promise<HeadVerifyResult> {\n const known = await head.knownIn(store, vault, getDEK, collection)\n const discrepancies: HeadDiscrepancy[] = []\n\n const because: HeadUnverifiableReason[] = []\n if (known.size === 0) because.push('no-expectations')\n // Read the store's OWN declared capability rather than probing: a store that\n // cannot serialize head writes may have silently dropped an entry, and a\n // dropped entry is invisible by construction — there is nothing left to find.\n if (store.capabilities?.casAtomic !== true) because.push('store-cannot-cas')\n\n for (const [id, expected] of known) {\n const env = await store.get(vault, collection, id)\n if (env === null) {\n discrepancies.push({ collection, id, expected, actual: null, kind: 'withheld' })\n continue\n }\n if (env._v < expected) {\n discrepancies.push({ collection, id, expected, actual: env._v, kind: 'rolled-back' })\n }\n }\n\n // A discrepancy is positive evidence, so it outranks any reason the sweep was\n // also incomplete: a rolled-back record found by a head that may be missing\n // OTHER entries is still a rolled-back record.\n const verdict: HeadVerdict = discrepancies.length > 0\n ? 'tampered'\n : because.length > 0 ? 'unverifiable' : 'verified'\n\n return { verdict, checked: known.size, discrepancies, because }\n}\n"],"mappings":"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","names":[]}
@@ -4,7 +4,7 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
  export { withVaultHead } from './active.js';
7
- export { verifyVaultHead, type HeadDiscrepancy, type HeadVerifyResult } from './verify.js';
7
+ export { verifyVaultHead, type HeadDiscrepancy, type HeadVerifyResult, type HeadVerdict, type HeadUnverifiableReason } from './verify.js';
8
8
  export { NO_VAULT_HEAD, VAULT_HEAD_COLLECTION, DEFAULT_HEAD_BUCKETS, type VaultHeadStrategy, type WithVaultHeadOptions, type HeadEntry, } from './strategy.js';
9
9
  export type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js';
10
10
  export type { EnclaveKey } from '../../kernel/enclave/index.js';
@@ -28,11 +28,51 @@ export interface HeadDiscrepancy {
28
28
  readonly actual: number | null;
29
29
  readonly kind: 'withheld' | 'rolled-back';
30
30
  }
31
+ /**
32
+ * Why a sweep could not reach a conclusion.
33
+ *
34
+ * - `'no-expectations'` — the head holds nothing for this collection. A fresh
35
+ * vault, a head switched on after the fact, or a **restore from a snapshot**:
36
+ * `_head` is `_`-prefixed, so `loadAll` excludes it and a restored vault has
37
+ * no head to compare against.
38
+ * - `'store-cannot-cas'` — the store does not advertise `capabilities.casAtomic`,
39
+ * so two writers racing on one bucket can silently lose an entry. A lost entry
40
+ * is a record the sweep stops expecting, which is a false clean.
41
+ */
42
+ export type HeadUnverifiableReason = 'no-expectations' | 'store-cannot-cas';
43
+ /**
44
+ * ## Three-way, deliberately (#1101)
45
+ *
46
+ * - `'verified'` — the head held expectations, every one was met, and the store
47
+ * can serialize head writes.
48
+ * - `'unverifiable'` — the sweep could not conclude. See `because`.
49
+ * - `'tampered'` — at least one discrepancy. Positive evidence, so it wins over
50
+ * any `unverifiable` reason present at the same time.
51
+ *
52
+ * **The middle value is the point, and collapsing it is wrong in a different
53
+ * direction each way:** folded into "clean" it hides withholding, which is the
54
+ * one thing this service exists to catch; folded into "tampered" it cries wolf
55
+ * on a vault that is merely unexamined.
56
+ *
57
+ * That is not hypothetical. #1044's own first bug was a head that recorded
58
+ * nothing and swept **perfectly clean** — the degraded state rendered
59
+ * identically to a healthy one, which is why its test asserts `checked` before
60
+ * anything else. A two-way verdict makes that state a supported outcome.
61
+ */
62
+ export type HeadVerdict = 'verified' | 'unverifiable' | 'tampered';
31
63
  export interface HeadVerifyResult {
64
+ readonly verdict: HeadVerdict;
32
65
  readonly checked: number;
33
66
  readonly discrepancies: readonly HeadDiscrepancy[];
34
- /** `true` when every record the head knows about is served at or above its expected version. */
35
- readonly clean: boolean;
67
+ /**
68
+ * Non-empty only when `verdict === 'unverifiable'`.
69
+ *
70
+ * There is deliberately no `clean: boolean`. It could not distinguish "no
71
+ * expectations" from "all expectations met" — both rendered `true` — and that
72
+ * indistinguishability *was* the defect. Replacing it rather than keeping it
73
+ * alongside forces every caller to face the three-way.
74
+ */
75
+ readonly because: readonly HeadUnverifiableReason[];
36
76
  }
37
77
  /**
38
78
  * Sweep one collection.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/hub",
3
- "version": "0.6.0-pre.19",
3
+ "version": "0.6.0-pre.20",
4
4
  "description": "Zero-knowledge, offline-first, encrypted document store — core library with AES-256-GCM, PBKDF2, multi-user keyring, and sync engine",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -215,14 +215,14 @@
215
215
  "node": ">=22.0.0"
216
216
  },
217
217
  "dependencies": {
218
- "@noy-db/attestation": "0.6.0-pre.19"
218
+ "@noy-db/attestation": "0.6.0-pre.20"
219
219
  },
220
220
  "devDependencies": {
221
221
  "@types/node": "^22.0.0",
222
222
  "esbuild": "^0.25.0",
223
223
  "zod": "^4.0.0",
224
224
  "zod-to-json-schema": "^3.25.2",
225
- "@noy-db/on-shamir": "0.6.0-pre.19"
225
+ "@noy-db/on-shamir": "0.6.0-pre.20"
226
226
  },
227
227
  "peerDependencies": {
228
228
  "zod-to-json-schema": "^3.25.0"