@noy-db/hub 0.4.0-pre.3 → 0.4.0-pre.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/cargo/index.js +2 -2
  2. package/dist/{chunk-GU5AYA2Q.js → chunk-4X6DJZHY.js} +2 -2
  3. package/dist/{chunk-N4A3DCBT.js → chunk-5KI3REO3.js} +2 -2
  4. package/dist/{chunk-OWLPX2GI.js → chunk-BLB7P7LS.js} +2 -2
  5. package/dist/{chunk-J6U4BOKP.js → chunk-JW3XCZA2.js} +9 -9
  6. package/dist/{chunk-UBF7HTOR.js → chunk-QUIG6BZT.js} +15 -15
  7. package/dist/{chunk-UBF7HTOR.js.map → chunk-QUIG6BZT.js.map} +1 -1
  8. package/dist/{chunk-SN75YVM5.js → chunk-U73LHVPF.js} +15 -15
  9. package/dist/{chunk-SN75YVM5.js.map → chunk-U73LHVPF.js.map} +1 -1
  10. package/dist/{chunk-LUH2MPEF.js → chunk-W7TCXZWU.js} +423 -467
  11. package/dist/chunk-W7TCXZWU.js.map +1 -0
  12. package/dist/{collection-facade-ENLTJZBT.js → collection-facade-CMZGQ6IS.js} +2 -2
  13. package/dist/index.js +7 -7
  14. package/dist/kernel/collection-config.d.ts +11 -69
  15. package/dist/kernel/collection.d.ts +10 -21
  16. package/dist/kernel/noydb.d.ts +11 -12
  17. package/dist/kernel/vault.d.ts +21 -68
  18. package/dist/{noydb-KUX2TJMX.js → noydb-N5IW6OFZ.js} +7 -7
  19. package/dist/port/with/archive-strategy.d.ts +31 -0
  20. package/dist/port/with/strategies.d.ts +134 -0
  21. package/dist/query/index.js +1 -1
  22. package/dist/team/index.js +1 -1
  23. package/dist/tiers/index.js +1 -1
  24. package/package.json +3 -3
  25. package/dist/chunk-LUH2MPEF.js.map +0 -1
  26. /package/dist/{chunk-GU5AYA2Q.js.map → chunk-4X6DJZHY.js.map} +0 -0
  27. /package/dist/{chunk-N4A3DCBT.js.map → chunk-5KI3REO3.js.map} +0 -0
  28. /package/dist/{chunk-OWLPX2GI.js.map → chunk-BLB7P7LS.js.map} +0 -0
  29. /package/dist/{chunk-J6U4BOKP.js.map → chunk-JW3XCZA2.js.map} +0 -0
  30. /package/dist/{collection-facade-ENLTJZBT.js.map → collection-facade-CMZGQ6IS.js.map} +0 -0
  31. /package/dist/{noydb-KUX2TJMX.js.map → noydb-N5IW6OFZ.js.map} +0 -0
@@ -31,7 +31,7 @@ import {
31
31
  buildPersistedIndexCallbacks,
32
32
  encodeVecId,
33
33
  syncTierSearch
34
- } from "./chunk-J6U4BOKP.js";
34
+ } from "./chunk-JW3XCZA2.js";
35
35
  import {
36
36
  derivePersistedSchema,
37
37
  isZod4Schema
@@ -44,7 +44,7 @@ import {
44
44
  CustodyApi,
45
45
  NO_CARGO,
46
46
  NO_CUSTODY
47
- } from "./chunk-SN75YVM5.js";
47
+ } from "./chunk-U73LHVPF.js";
48
48
  import {
49
49
  resolveManagedSecret,
50
50
  saveSealedPassphrase
@@ -72,7 +72,7 @@ import {
72
72
  NO_TIERS,
73
73
  classifySealedShred,
74
74
  syncDerivedOutputs
75
- } from "./chunk-N4A3DCBT.js";
75
+ } from "./chunk-5KI3REO3.js";
76
76
  import {
77
77
  NO_PORTABILITY
78
78
  } from "./chunk-GYB2QK5H.js";
@@ -115,7 +115,7 @@ import {
115
115
  NO_AGGREGATE,
116
116
  Query,
117
117
  ScanBuilder
118
- } from "./chunk-GU5AYA2Q.js";
118
+ } from "./chunk-4X6DJZHY.js";
119
119
  import {
120
120
  EXPORT_AUDIT_COLLECTION,
121
121
  createExportBlobsHandle,
@@ -163,7 +163,7 @@ import {
163
163
  rotatePassphrase,
164
164
  updateAuthenticator,
165
165
  writeMagicLinkGrant
166
- } from "./chunk-UBF7HTOR.js";
166
+ } from "./chunk-QUIG6BZT.js";
167
167
  import {
168
168
  assertKeyringOpenAllowed,
169
169
  buildRecipientKeyringFile,
@@ -293,6 +293,284 @@ import {
293
293
  ValidationError
294
294
  } from "./chunk-IGM563N5.js";
295
295
 
296
+ // src/via/blob/binding.ts
297
+ async function eraseBlobs(ctx, cfg) {
298
+ if (cfg.purgeBlobsForRecord) return cfg.purgeBlobsForRecord(ctx.id);
299
+ return { shredded: 0, residue: [] };
300
+ }
301
+ function buildBlobDescribeFragment(fields) {
302
+ return {
303
+ blobFields: Object.fromEntries(
304
+ Object.entries(fields).map(([field, p]) => [field, {
305
+ ...p.retainDays !== void 0 ? { retainDays: p.retainDays } : {},
306
+ ...p.evictWhen !== void 0 ? { evictWhen: true } : {},
307
+ ...p.legalHold !== void 0 ? { legalHold: true } : {},
308
+ ...p.retainUntil !== void 0 ? { retainUntil: true } : {},
309
+ ...p.external === true ? { external: true } : {},
310
+ ...p.public === true ? { public: true } : {},
311
+ ...p.backlink !== void 0 ? { backlink: p.backlink } : {}
312
+ }])
313
+ )
314
+ };
315
+ }
316
+ function blobBinding(cfg) {
317
+ return {
318
+ brand: "blob",
319
+ // encryptedAtRest: 'envelope' — blob chunks are AEAD-encrypted envelopes
320
+ // in `_blob_chunks` (service-side), not sealed record fields; queryable:
321
+ // 'none' — nothing indexes blob content; exportable — vault.exportBlobs()
322
+ // is a first-class door; forgettable — shredAllForRecord participates in
323
+ // vault.forget().
324
+ posture: { encryptedAtRest: "envelope", queryable: "none", exportable: true, forgettable: true },
325
+ covers: (field) => field in cfg.fields,
326
+ erase: (ctx) => eraseBlobs(ctx, cfg),
327
+ describeFragment: () => buildBlobDescribeFragment(cfg.fields)
328
+ };
329
+ }
330
+ function linkBlobVia() {
331
+ installViaBinder("blob", (c) => blobBinding(c));
332
+ }
333
+
334
+ // src/port/with/blob-strategy.ts
335
+ linkBlobVia();
336
+ var NO_BLOBS = {
337
+ openSlot() {
338
+ throw new Error(
339
+ 'Blob storage is not enabled on this Noydb instance. Import `{ withBlobs }` from "@noy-db/hub/blobs" and pass `withBlobs()` to `createNoydb({ blobStrategy: withBlobs() })`.'
340
+ );
341
+ }
342
+ };
343
+
344
+ // src/with-audit/consent/strategy.ts
345
+ var NO_CONSENT = {
346
+ async write() {
347
+ },
348
+ async read() {
349
+ return [];
350
+ }
351
+ };
352
+
353
+ // src/with-commit/crdt/strategy.ts
354
+ var NOT_ENABLED = new Error(
355
+ 'CRDT mode requires the CRDT strategy. Import `{ withCrdt }` from "@noy-db/hub/crdt" and pass it to `createNoydb({ crdtStrategy: withCrdt() })`.'
356
+ );
357
+ var NO_CRDT = {
358
+ buildLwwMapState() {
359
+ throw NOT_ENABLED;
360
+ },
361
+ buildRgaState() {
362
+ throw NOT_ENABLED;
363
+ },
364
+ mergeCrdtStates() {
365
+ throw NOT_ENABLED;
366
+ },
367
+ resolveCrdtSnapshot() {
368
+ throw NOT_ENABLED;
369
+ }
370
+ };
371
+
372
+ // src/with-commit/history/strategy.ts
373
+ function notEnabled(op) {
374
+ return new Error(
375
+ `${op} requires the history strategy. Import \`{ withHistory }\` from "@noy-db/hub/history" and pass it to \`createNoydb({ historyStrategy: withHistory() })\`.`
376
+ );
377
+ }
378
+ var NO_HISTORY = {
379
+ async saveHistory() {
380
+ },
381
+ async getHistoryEntries() {
382
+ throw notEnabled("collection.history()");
383
+ },
384
+ async getVersionEnvelope() {
385
+ throw notEnabled("collection.getVersion()");
386
+ },
387
+ async pruneHistory() {
388
+ return 0;
389
+ },
390
+ async clearHistory() {
391
+ return 0;
392
+ },
393
+ async tombstoneHistory() {
394
+ return 0;
395
+ },
396
+ async rewrapHistory() {
397
+ },
398
+ async envelopePayloadHash() {
399
+ return "";
400
+ },
401
+ computePatch() {
402
+ return [];
403
+ },
404
+ diff() {
405
+ throw notEnabled("collection.diff()");
406
+ },
407
+ buildLedger() {
408
+ return null;
409
+ },
410
+ buildVaultInstant() {
411
+ throw notEnabled("vault.at() / vault.timeMachine()");
412
+ }
413
+ };
414
+
415
+ // src/with-lookup/indexing/strategy.ts
416
+ var NO_INDEXING = {
417
+ createState() {
418
+ return DISABLED_STATE;
419
+ }
420
+ };
421
+ var DISABLED_STATE = {
422
+ isEnabled: false,
423
+ getEagerIndexes: () => null,
424
+ getPersistedIndexes: () => null
425
+ };
426
+
427
+ // src/with-audit/periods/strategy.ts
428
+ var NOT_ENABLED2 = new Error(
429
+ 'Accounting periods require the periods strategy. Import `{ withPeriods }` from "@noy-db/hub/periods" and pass it to `createNoydb({ periodsStrategy: withPeriods() })`.'
430
+ );
431
+ var NO_PERIODS = {
432
+ async loadPeriods() {
433
+ return [];
434
+ },
435
+ async chainAnchor() {
436
+ return { priorPeriodHash: "" };
437
+ },
438
+ assertTsWritable() {
439
+ },
440
+ validatePeriodName() {
441
+ throw NOT_ENABLED2;
442
+ },
443
+ async appendPeriodLedgerEntry() {
444
+ throw NOT_ENABLED2;
445
+ }
446
+ };
447
+
448
+ // src/with-party/session/strategy.ts
449
+ function notEnabled2(op) {
450
+ return new Error(
451
+ `${op} requires the session strategy. Import \`{ withSession }\` from "@noy-db/hub/session" and pass it to \`createNoydb({ sessionStrategy: withSession() })\`.`
452
+ );
453
+ }
454
+ var NO_SESSION = {
455
+ validateSessionPolicy() {
456
+ throw notEnabled2("sessionPolicy");
457
+ },
458
+ createEnforcer() {
459
+ throw notEnabled2("session policy enforcement");
460
+ },
461
+ revokeAllSessions() {
462
+ }
463
+ };
464
+
465
+ // src/with-fork/shadow/strategy.ts
466
+ var NOT_ENABLED3 = new Error(
467
+ 'VaultFrame requires the shadow strategy. Import `{ withShadow }` from "@noy-db/hub/shadow" and pass it to `createNoydb({ shadowStrategy: withShadow() })`.'
468
+ );
469
+ var NO_SHADOW = {
470
+ buildFrame() {
471
+ throw NOT_ENABLED3;
472
+ }
473
+ };
474
+
475
+ // src/with-fork/snapshots/strategy.ts
476
+ var NOT_ENABLED4 = new Error(
477
+ 'Snapshots require the snapshot strategy. Import `{ withSnapshots }` from "@noy-db/hub/snapshots" and pass it to `createNoydb({ snapshotStrategy: withSnapshots({ store }) })`.'
478
+ );
479
+ var NO_SNAPSHOTS = {
480
+ async snapshot() {
481
+ throw NOT_ENABLED4;
482
+ },
483
+ async listSnapshots() {
484
+ throw NOT_ENABLED4;
485
+ },
486
+ async restoreSnapshot() {
487
+ throw NOT_ENABLED4;
488
+ },
489
+ async autoSnapshot() {
490
+ throw NOT_ENABLED4;
491
+ }
492
+ };
493
+
494
+ // src/with-party/team/sync-strategy.ts
495
+ function notEnabled3(op) {
496
+ return new Error(
497
+ `${op} requires the sync strategy. Import \`{ withSync }\` from "@noy-db/hub/sync" and pass it to \`createNoydb({ syncStrategy: withSync() })\`.`
498
+ );
499
+ }
500
+ var NO_SYNC = {
501
+ buildSyncEngine() {
502
+ throw notEnabled3("SyncEngine");
503
+ },
504
+ buildSyncTransaction() {
505
+ throw notEnabled3("SyncTransaction");
506
+ },
507
+ buildPresence() {
508
+ throw notEnabled3("collection.presence()");
509
+ }
510
+ };
511
+
512
+ // src/with-commit/tx/strategy.ts
513
+ var NOT_ENABLED5 = new Error(
514
+ 'Multi-record transactions require the tx strategy. Import `{ withTransactions }` from "@noy-db/hub/tx" and pass it to `createNoydb({ txStrategy: withTransactions() })`.'
515
+ );
516
+ var NO_TX = {
517
+ async runTransaction(_db, _fn, _options, _txInvariants) {
518
+ throw NOT_ENABLED5;
519
+ },
520
+ async runDryRun() {
521
+ throw NOT_ENABLED5;
522
+ }
523
+ };
524
+
525
+ // src/port/with/archive-strategy.ts
526
+ var ARCHIVE_NOT_ENABLED = "vault.archive/restore/listArchived require `archiveStrategy: withArchive({ store })` in createNoydb";
527
+ var NO_ARCHIVE = {
528
+ get store() {
529
+ throw new Error(ARCHIVE_NOT_ENABLED);
530
+ }
531
+ };
532
+
533
+ // src/port/with/strategies.ts
534
+ var STRATEGY_DEFAULTS = {
535
+ aggregate: NO_AGGREGATE,
536
+ archive: NO_ARCHIVE,
537
+ attestation: NO_ATTESTATION,
538
+ blob: NO_BLOBS,
539
+ broker: NO_BROKER,
540
+ cargo: NO_CARGO,
541
+ classified: NO_CLASSIFIED,
542
+ consent: NO_CONSENT,
543
+ crdt: NO_CRDT,
544
+ custody: NO_CUSTODY,
545
+ forget: NO_FORGET,
546
+ history: NO_HISTORY,
547
+ i18n: NO_I18N,
548
+ index: NO_INDEXING,
549
+ lazy: IMPLICIT_LAZY,
550
+ periods: NO_PERIODS,
551
+ portability: NO_PORTABILITY,
552
+ sealedRecord: NO_SEALED_RECORD,
553
+ search: NO_SEARCH,
554
+ sequence: NO_SEQUENCE,
555
+ session: NO_SESSION,
556
+ shadow: NO_SHADOW,
557
+ snapshot: NO_SNAPSHOTS,
558
+ sync: NO_SYNC,
559
+ team: NO_TEAM,
560
+ tiers: NO_TIERS,
561
+ tx: NO_TX
562
+ };
563
+ var STRATEGY_KEYS = Object.keys(STRATEGY_DEFAULTS);
564
+ function resolveStrategies(options) {
565
+ const provided = options;
566
+ const resolved = { ...STRATEGY_DEFAULTS };
567
+ for (const key of STRATEGY_KEYS) {
568
+ const value = provided[`${key}Strategy`];
569
+ if (value !== void 0) resolved[key] = value;
570
+ }
571
+ return resolved;
572
+ }
573
+
296
574
  // src/port/by/default-provider.ts
297
575
  async function createDefaultCoordinationProvider(store) {
298
576
  const { StoreCoordinationProvider } = await import("./store-coordination-provider-3AELCZ4Y.js");
@@ -964,61 +1242,6 @@ async function resolveTierSyncPrior(ctx, id, priorEnvelope) {
964
1242
  return await ctx.codec.decryptRecord(priorEnvelope, { skipValidation: true, id });
965
1243
  }
966
1244
 
967
- // src/with-commit/history/strategy.ts
968
- function notEnabled(op) {
969
- return new Error(
970
- `${op} requires the history strategy. Import \`{ withHistory }\` from "@noy-db/hub/history" and pass it to \`createNoydb({ historyStrategy: withHistory() })\`.`
971
- );
972
- }
973
- var NO_HISTORY = {
974
- async saveHistory() {
975
- },
976
- async getHistoryEntries() {
977
- throw notEnabled("collection.history()");
978
- },
979
- async getVersionEnvelope() {
980
- throw notEnabled("collection.getVersion()");
981
- },
982
- async pruneHistory() {
983
- return 0;
984
- },
985
- async clearHistory() {
986
- return 0;
987
- },
988
- async tombstoneHistory() {
989
- return 0;
990
- },
991
- async rewrapHistory() {
992
- },
993
- async envelopePayloadHash() {
994
- return "";
995
- },
996
- computePatch() {
997
- return [];
998
- },
999
- diff() {
1000
- throw notEnabled("collection.diff()");
1001
- },
1002
- buildLedger() {
1003
- return null;
1004
- },
1005
- buildVaultInstant() {
1006
- throw notEnabled("vault.at() / vault.timeMachine()");
1007
- }
1008
- };
1009
-
1010
- // src/with-lookup/indexing/strategy.ts
1011
- var NO_INDEXING = {
1012
- createState() {
1013
- return DISABLED_STATE;
1014
- }
1015
- };
1016
- var DISABLED_STATE = {
1017
- isEnabled: false,
1018
- getEagerIndexes: () => null,
1019
- getPersistedIndexes: () => null
1020
- };
1021
-
1022
1245
  // src/with-lookup/search/serialize.ts
1023
1246
  function serializeIndex(idx) {
1024
1247
  return JSON.stringify(idx.toSnapshot());
@@ -1621,54 +1844,6 @@ function buildDescription(input) {
1621
1844
  };
1622
1845
  }
1623
1846
 
1624
- // src/via/blob/binding.ts
1625
- async function eraseBlobs(ctx, cfg) {
1626
- if (cfg.purgeBlobsForRecord) return cfg.purgeBlobsForRecord(ctx.id);
1627
- return { shredded: 0, residue: [] };
1628
- }
1629
- function buildBlobDescribeFragment(fields) {
1630
- return {
1631
- blobFields: Object.fromEntries(
1632
- Object.entries(fields).map(([field, p]) => [field, {
1633
- ...p.retainDays !== void 0 ? { retainDays: p.retainDays } : {},
1634
- ...p.evictWhen !== void 0 ? { evictWhen: true } : {},
1635
- ...p.legalHold !== void 0 ? { legalHold: true } : {},
1636
- ...p.retainUntil !== void 0 ? { retainUntil: true } : {},
1637
- ...p.external === true ? { external: true } : {},
1638
- ...p.public === true ? { public: true } : {},
1639
- ...p.backlink !== void 0 ? { backlink: p.backlink } : {}
1640
- }])
1641
- )
1642
- };
1643
- }
1644
- function blobBinding(cfg) {
1645
- return {
1646
- brand: "blob",
1647
- // encryptedAtRest: 'envelope' — blob chunks are AEAD-encrypted envelopes
1648
- // in `_blob_chunks` (service-side), not sealed record fields; queryable:
1649
- // 'none' — nothing indexes blob content; exportable — vault.exportBlobs()
1650
- // is a first-class door; forgettable — shredAllForRecord participates in
1651
- // vault.forget().
1652
- posture: { encryptedAtRest: "envelope", queryable: "none", exportable: true, forgettable: true },
1653
- covers: (field) => field in cfg.fields,
1654
- erase: (ctx) => eraseBlobs(ctx, cfg),
1655
- describeFragment: () => buildBlobDescribeFragment(cfg.fields)
1656
- };
1657
- }
1658
- function linkBlobVia() {
1659
- installViaBinder("blob", (c) => blobBinding(c));
1660
- }
1661
-
1662
- // src/port/with/blob-strategy.ts
1663
- linkBlobVia();
1664
- var NO_BLOBS = {
1665
- openSlot() {
1666
- throw new Error(
1667
- 'Blob storage is not enabled on this Noydb instance. Import `{ withBlobs }` from "@noy-db/hub/blobs" and pass `withBlobs()` to `createNoydb({ blobStrategy: withBlobs() })`.'
1668
- );
1669
- }
1670
- };
1671
-
1672
1847
  // src/with-formula/derivations/stale.ts
1673
1848
  var _staleByRegistry = /* @__PURE__ */ new WeakMap();
1674
1849
  var keyFor = (source, sourceId) => `${source}/${sourceId}`;
@@ -1742,43 +1917,6 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
1742
1917
  }
1743
1918
  }
1744
1919
 
1745
- // src/with-commit/crdt/strategy.ts
1746
- var NOT_ENABLED = new Error(
1747
- 'CRDT mode requires the CRDT strategy. Import `{ withCrdt }` from "@noy-db/hub/crdt" and pass it to `createNoydb({ crdtStrategy: withCrdt() })`.'
1748
- );
1749
- var NO_CRDT = {
1750
- buildLwwMapState() {
1751
- throw NOT_ENABLED;
1752
- },
1753
- buildRgaState() {
1754
- throw NOT_ENABLED;
1755
- },
1756
- mergeCrdtStates() {
1757
- throw NOT_ENABLED;
1758
- },
1759
- resolveCrdtSnapshot() {
1760
- throw NOT_ENABLED;
1761
- }
1762
- };
1763
-
1764
- // src/with-party/team/sync-strategy.ts
1765
- function notEnabled2(op) {
1766
- return new Error(
1767
- `${op} requires the sync strategy. Import \`{ withSync }\` from "@noy-db/hub/sync" and pass it to \`createNoydb({ syncStrategy: withSync() })\`.`
1768
- );
1769
- }
1770
- var NO_SYNC = {
1771
- buildSyncEngine() {
1772
- throw notEnabled2("SyncEngine");
1773
- },
1774
- buildSyncTransaction() {
1775
- throw notEnabled2("SyncTransaction");
1776
- },
1777
- buildPresence() {
1778
- throw notEnabled2("collection.presence()");
1779
- }
1780
- };
1781
-
1782
1920
  // src/kernel/via/compose.ts
1783
1921
  function via(...descriptors) {
1784
1922
  if (descriptors.length === 0) {
@@ -1942,7 +2080,7 @@ function compileViaBindings(opts, classifiedGuardCtx, eraseCfgOut) {
1942
2080
  ...i18nFields !== void 0 ? { i18nFields } : {},
1943
2081
  ...dictKeyFields !== void 0 ? { dictKeyFields } : {},
1944
2082
  ...i18nDensifyFields !== void 0 ? { i18nDensifyFields } : {},
1945
- strategy: opts.i18nStrategy ?? NO_I18N,
2083
+ strategy: opts.strategies.i18n,
1946
2084
  ...opts.defaultLocale !== void 0 ? { defaultLocale: opts.defaultLocale } : {},
1947
2085
  ...opts.autoTranslateHook !== void 0 ? { autoTranslateHook: opts.autoTranslateHook } : {},
1948
2086
  ...opts.dictLabelResolver !== void 0 ? { dictLabelResolver: opts.dictLabelResolver } : {},
@@ -2177,14 +2315,9 @@ function resolveCollectionConfig(opts) {
2177
2315
  writeHooks: opts.writeHooks,
2178
2316
  subsystemBus: opts.subsystemBus,
2179
2317
  activeTxId: opts.activeTxId,
2180
- blobStrategy: opts.blobStrategy ?? NO_BLOBS,
2318
+ strategies: opts.strategies,
2181
2319
  objectStore: opts.objectStore,
2182
2320
  blobFields: opts.blobFields,
2183
- aggregateStrategy: opts.aggregateStrategy ?? NO_AGGREGATE,
2184
- crdtStrategy: opts.crdtStrategy ?? NO_CRDT,
2185
- historyStrategy: opts.historyStrategy ?? NO_HISTORY,
2186
- i18nStrategy: opts.i18nStrategy ?? NO_I18N,
2187
- syncStrategy: opts.syncStrategy ?? NO_SYNC,
2188
2321
  reconcileOnOpen: opts.reconcileOnOpen ?? "off",
2189
2322
  getDEK: opts.getDEK,
2190
2323
  onDirty: opts.onDirty,
@@ -2213,7 +2346,6 @@ function resolveCollectionConfig(opts) {
2213
2346
  moneyFields: effectiveViaFields.moneyFields,
2214
2347
  classified: resolvedClassified,
2215
2348
  classifiedGuardCtx,
2216
- classifiedStrategy: opts.classifiedStrategy ?? NO_CLASSIFIED,
2217
2349
  computed: mergedComputed,
2218
2350
  computedEdges,
2219
2351
  viaDepsEdges,
@@ -2235,8 +2367,6 @@ function resolveCollectionConfig(opts) {
2235
2367
  materializedViewSource: opts.materializedViewSource,
2236
2368
  graphDispatch: opts.graphDispatch,
2237
2369
  tiers: opts.tiers && opts.tiers.length > 0 ? new Set(opts.tiers) : null,
2238
- tiersStrategy: opts.tiersStrategy ?? NO_TIERS,
2239
- searchStrategy: opts.searchStrategy ?? NO_SEARCH,
2240
2370
  tierMode: opts.tierMode ?? "invisibility",
2241
2371
  blobTierPolicy: opts.blobTierPolicy ?? "isolate",
2242
2372
  onCrossTierAccess: opts.onCrossTierAccess,
@@ -2298,25 +2428,20 @@ var Collection = class {
2298
2428
  /** True when the caller explicitly provided a `historyConfig` option (vs. inheriting the vault default). */
2299
2429
  historyConfigExplicit;
2300
2430
  /**
2301
- * tree-shake seam the strategy that backs `collection.blob(id)`.
2302
- * Defaults to `NO_BLOBS`, a ~10-line stub that throws with an actionable
2303
- * message. Consumers opt into real blob storage by importing
2304
- * `{ blobs }` from `@noy-db/hub/blobs` and passing the returned
2305
- * strategy to `createNoydb({ blobStrategy: blobs() })`. With the
2306
- * default stub, none of the BlobSet / chunk / MIME-magic machinery
2307
- * reaches the bundle.
2431
+ * Every opt-in service, resolved once by `createNoydb` (#838) and shared by
2432
+ * reference with the owning `Vault` and `Noydb`.
2433
+ *
2434
+ * Each entry is a tree-shake seam. An un-opted-in service resolves to its
2435
+ * `NO_*` stub a tiny module that throws with an actionable message —
2436
+ * so, for example, none of the BlobSet / chunk / MIME-magic machinery
2437
+ * reaches the bundle unless the consumer imports `@noy-db/hub/blobs` and
2438
+ * passes `blobStrategy: blobs()` to `createNoydb`.
2308
2439
  */
2309
- blobStrategy;
2440
+ strategies;
2310
2441
  objectStore;
2311
2442
  blobFields;
2312
2443
  blobTierPolicy;
2313
2444
  // #724 T2/T3
2314
- aggregateStrategy;
2315
- crdtStrategy;
2316
- tiersStrategy;
2317
- searchStrategy;
2318
- historyStrategy;
2319
- syncStrategy;
2320
2445
  // In-memory cache of decrypted records (eager mode only). Lazy mode
2321
2446
  // uses `lru` instead. Both fields exist so a single Collection instance
2322
2447
  // doesn't need a runtime branch on every cache access.
@@ -2486,12 +2611,6 @@ var Collection = class {
2486
2611
  /** C-A/R10 memoization: marker declared-set lookup (undefined=unresolved) + classified-handle persist-once flag. */
2487
2612
  _markerDigestOnlyCache = void 0;
2488
2613
  _markerPersisted = false;
2489
- /**
2490
- * Tree-shake seam for `reveal()` — defaults to `NO_CLASSIFIED`, which
2491
- * throws `ClassifiedNotEnabledError`. Set via the `classifiedStrategy`
2492
- * `createNoydb()` option (opt in with `withClassified()`).
2493
- */
2494
- classifiedStrategy;
2495
2614
  /** Async callback provided by the Vault to open a dynamic dictionary handle (for label-map pre-computation in the search index). Only used in `resolveDictLabelMaps()`; static dicts bypass this entirely. Mutable, assign-once — see {@link _reconcileReadState} (#671 item 1). */
2496
2615
  getDictionary;
2497
2616
  /**
@@ -2640,16 +2759,10 @@ var Collection = class {
2640
2759
  this.writeHooks = cfg.writeHooks;
2641
2760
  this.subsystemBus = cfg.subsystemBus;
2642
2761
  this.activeTxId = cfg.activeTxId;
2643
- this.blobStrategy = cfg.blobStrategy;
2762
+ this.strategies = cfg.strategies;
2644
2763
  this.objectStore = cfg.objectStore;
2645
2764
  this.blobFields = cfg.blobFields;
2646
2765
  this.blobTierPolicy = cfg.blobTierPolicy ?? "isolate";
2647
- this.aggregateStrategy = cfg.aggregateStrategy;
2648
- this.crdtStrategy = cfg.crdtStrategy;
2649
- this.tiersStrategy = cfg.tiersStrategy;
2650
- this.searchStrategy = cfg.searchStrategy;
2651
- this.historyStrategy = cfg.historyStrategy;
2652
- this.syncStrategy = cfg.syncStrategy;
2653
2766
  this.reconcileOnOpen = cfg.reconcileOnOpen;
2654
2767
  this.getDEK = cfg.getDEK;
2655
2768
  this.onDirty = cfg.onDirty;
@@ -2676,7 +2789,6 @@ var Collection = class {
2676
2789
  this.classified = cfg.classified;
2677
2790
  this.classifiedGuardCtx = cfg.classifiedGuardCtx;
2678
2791
  this.vdigFields = cfg.vdigFields;
2679
- this.classifiedStrategy = cfg.classifiedStrategy;
2680
2792
  this.computed = cfg.computed;
2681
2793
  this.getDictionary = cfg.getDictionary;
2682
2794
  this.defaultLocale = cfg.defaultLocale;
@@ -2724,7 +2836,7 @@ var Collection = class {
2724
2836
  deterministicFields: this.deterministicFields,
2725
2837
  vdigFields: this.vdigFields,
2726
2838
  crdtMode: this.crdtMode,
2727
- crdtStrategy: this.crdtStrategy,
2839
+ crdtStrategy: this.strategies.crdt,
2728
2840
  schema: this.schema,
2729
2841
  getDEK: (collection) => this.getDEK(collection ?? this.name),
2730
2842
  cekCache: this.cekCache,
@@ -2742,7 +2854,7 @@ var Collection = class {
2742
2854
  const remoteJson = await this.codec.decryptJsonString(remote, id);
2743
2855
  if (localJson === null) return local;
2744
2856
  if (remoteJson === null) return remote;
2745
- const merged = this.crdtStrategy.mergeCrdtStates(JSON.parse(localJson), JSON.parse(remoteJson));
2857
+ const merged = this.strategies.crdt.mergeCrdtStates(JSON.parse(localJson), JSON.parse(remoteJson));
2746
2858
  const mergedVersion = Math.max(local._v, remote._v) + 1;
2747
2859
  const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
2748
2860
  return this.codec.encryptJsonString(JSON.stringify(merged), mergedVersion, cek);
@@ -2800,12 +2912,12 @@ var Collection = class {
2800
2912
  }
2801
2913
  this.lazy = opts.prefetch === false;
2802
2914
  if (this.lazy) {
2803
- this.lru = (opts.lazyStrategy ?? IMPLICIT_LAZY).createCache(this.name, opts.cache);
2915
+ this.lru = opts.strategies.lazy.createCache(this.name, opts.cache);
2804
2916
  this.hydrated = true;
2805
2917
  } else {
2806
2918
  this.lru = null;
2807
2919
  }
2808
- const strategy = opts.indexStrategy ?? NO_INDEXING;
2920
+ const strategy = opts.strategies.index;
2809
2921
  this.indexState = strategy.createState({
2810
2922
  defs: opts.indexes ?? [],
2811
2923
  lazy: this.lazy
@@ -2960,7 +3072,7 @@ var Collection = class {
2960
3072
  if (spec.storage === "digest-only") {
2961
3073
  throw new ClassifiedRevealError(this.name, field, `storage:'digest-only' \u2014 verify-only; nothing recoverable to reveal`);
2962
3074
  }
2963
- return this.classifiedStrategy.reveal({
3075
+ return this.strategies.classified.reveal({
2964
3076
  collection: this.name,
2965
3077
  spec,
2966
3078
  encrypted: this.storeCiphertext,
@@ -2980,7 +3092,7 @@ var Collection = class {
2980
3092
  throw new ClassifiedVerifyError(this.name, field, `storage:'never' \u2014 nothing is stored to verify against`);
2981
3093
  }
2982
3094
  const ctx = this._classifiedVerifyCtx(spec);
2983
- return spec.storage === "digest-only" ? this.classifiedStrategy.verify(ctx, id, field, candidate) : this.classifiedStrategy.verifyText(ctx, id, field, candidate);
3095
+ return spec.storage === "digest-only" ? this.strategies.classified.verify(ctx, id, field, candidate) : this.strategies.classified.verifyText(ctx, id, field, candidate);
2984
3096
  }
2985
3097
  /** k-of-n challenge over the collection's secretAnswer members. Requires withClassified(). */
2986
3098
  async verifyGroup(id, answers, opts) {
@@ -2989,7 +3101,7 @@ var Collection = class {
2989
3101
  throw new ClassifiedVerifyError(this.name, "*", "no groupable digest-only (secretAnswer) fields declared");
2990
3102
  }
2991
3103
  const ctx = { ...this._classifiedVerifyCtx(members[0].spec), groupMembers: members };
2992
- return this.classifiedStrategy.matchGroup(ctx, id, answers, opts);
3104
+ return this.strategies.classified.matchGroup(ctx, id, answers, opts);
2993
3105
  }
2994
3106
  _classifiedVerifyCtx(spec) {
2995
3107
  return {
@@ -3044,7 +3156,7 @@ var Collection = class {
3044
3156
  if (typeof candidate !== "string") {
3045
3157
  throw new ClassifiedVerifyError(this.name, "*", "candidate must be a string");
3046
3158
  }
3047
- const target = await this.classifiedStrategy.computeTarget(this._classifiedVerifyCtx(spec), field, candidate);
3159
+ const target = await this.strategies.classified.computeTarget(this._classifiedVerifyCtx(spec), field, candidate);
3048
3160
  const ids = await this.adapter.list(this.vault, this.name);
3049
3161
  const hits = [];
3050
3162
  const hitMap = /* @__PURE__ */ new Map();
@@ -3131,7 +3243,7 @@ var Collection = class {
3131
3243
  id,
3132
3244
  version: scrubbed._v,
3133
3245
  actor: this.keyring.userId,
3134
- payloadHash: await this.historyStrategy.envelopePayloadHash(scrubbed),
3246
+ payloadHash: await this.strategies.history.envelopePayloadHash(scrubbed),
3135
3247
  reason: "classified:scrub-equatable-tags"
3136
3248
  });
3137
3249
  }
@@ -3357,7 +3469,7 @@ var Collection = class {
3357
3469
  if (this.syncAdapter !== void 0) presenceOpts.syncAdapter = this.syncAdapter;
3358
3470
  if (opts?.staleMs !== void 0) presenceOpts.staleMs = opts.staleMs;
3359
3471
  if (opts?.pollIntervalMs !== void 0) presenceOpts.pollIntervalMs = opts.pollIntervalMs;
3360
- return this.syncStrategy.buildPresence(presenceOpts);
3472
+ return this.strategies.sync.buildPresence(presenceOpts);
3361
3473
  }
3362
3474
  /**
3363
3475
  * Create or update a record. Runs inside the hub's write-queue tracker
@@ -3577,7 +3689,7 @@ var Collection = class {
3577
3689
  }
3578
3690
  }
3579
3691
  }
3580
- crdtState = this.crdtStrategy.buildLwwMapState(record, existingState, now);
3692
+ crdtState = this.strategies.crdt.buildLwwMapState(record, existingState, now);
3581
3693
  } else if (this.crdtMode === "rga") {
3582
3694
  let existingState;
3583
3695
  if (existingEnvelope) {
@@ -3589,7 +3701,7 @@ var Collection = class {
3589
3701
  }
3590
3702
  }
3591
3703
  }
3592
- crdtState = this.crdtStrategy.buildRgaState(Array.isArray(record) ? record : [record], existingState, generateULID);
3704
+ crdtState = this.strategies.crdt.buildRgaState(Array.isArray(record) ? record : [record], existingState, generateULID);
3593
3705
  } else {
3594
3706
  crdtState = { _crdt: "yjs", update: record };
3595
3707
  }
@@ -3597,14 +3709,14 @@ var Collection = class {
3597
3709
  const cek2 = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
3598
3710
  const envelope2 = await this.codec.encryptJsonString(JSON.stringify(crdtState), version2, cek2, options?.source, options?.sourceTs);
3599
3711
  await this.adapter.put(this.vault, this.name, id, envelope2);
3600
- const resolvedRecord = this.crdtStrategy.resolveCrdtSnapshot(crdtState);
3712
+ const resolvedRecord = this.strategies.crdt.resolveCrdtSnapshot(crdtState);
3601
3713
  const existingResolvedRecord = existingEnvelope ? await this.codec.decryptRecord(existingEnvelope, { skipValidation: true }) : null;
3602
3714
  const existingResolved = existingResolvedRecord !== null ? { record: existingResolvedRecord, version: existingVersion } : void 0;
3603
3715
  if (existingResolved && this.historyConfig.enabled !== false) {
3604
- await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, await this.codec.encryptRecord(existingResolved.record, existingResolved.version, cek2, void 0, void 0, this.vdigFields !== null ? { id, prev: existingEnvelope } : void 0, id));
3716
+ await this.strategies.history.saveHistory(this.adapter, this.vault, this.name, id, await this.codec.encryptRecord(existingResolved.record, existingResolved.version, cek2, void 0, void 0, this.vdigFields !== null ? { id, prev: existingEnvelope } : void 0, id));
3605
3717
  this.emitter.emit("history:save", { vault: this.vault, collection: this.name, id, version: existingResolved.version });
3606
3718
  if (this.historyConfig.maxVersions) {
3607
- await this.historyStrategy.pruneHistory(this.adapter, this.vault, this.name, id, { keepVersions: this.historyConfig.maxVersions });
3719
+ await this.strategies.history.pruneHistory(this.adapter, this.vault, this.name, id, { keepVersions: this.historyConfig.maxVersions });
3608
3720
  }
3609
3721
  }
3610
3722
  if (this.ledger) {
@@ -3614,9 +3726,9 @@ var Collection = class {
3614
3726
  id,
3615
3727
  version: version2,
3616
3728
  actor: this.keyring.userId,
3617
- payloadHash: await this.historyStrategy.envelopePayloadHash(envelope2)
3729
+ payloadHash: await this.strategies.history.envelopePayloadHash(envelope2)
3618
3730
  };
3619
- if (existingResolved) appendInput.delta = this.historyStrategy.computePatch(resolvedRecord, existingResolved.record);
3731
+ if (existingResolved) appendInput.delta = this.strategies.history.computePatch(resolvedRecord, existingResolved.record);
3620
3732
  if (options?.reason !== void 0) appendInput.reason = options.reason;
3621
3733
  await this.ledger.append(appendInput);
3622
3734
  }
@@ -3663,21 +3775,21 @@ var Collection = class {
3663
3775
  const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
3664
3776
  const vdigCtx = this.vdigFields !== null ? { id, prev: await this.adapter.get(this.vault, this.name, id) } : void 0;
3665
3777
  if (existing && this.historyConfig.enabled !== false) {
3666
- await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, await this.codec.encryptRecord(existing.record, existing.version, cek, void 0, void 0, vdigCtx, id));
3778
+ await this.strategies.history.saveHistory(this.adapter, this.vault, this.name, id, await this.codec.encryptRecord(existing.record, existing.version, cek, void 0, void 0, vdigCtx, id));
3667
3779
  this.emitter.emit("history:save", {
3668
3780
  vault: this.vault,
3669
3781
  collection: this.name,
3670
3782
  id,
3671
3783
  version: existing.version
3672
3784
  });
3673
- if (this.historyConfig.maxVersions) await this.historyStrategy.pruneHistory(this.adapter, this.vault, this.name, id, { keepVersions: this.historyConfig.maxVersions });
3785
+ if (this.historyConfig.maxVersions) await this.strategies.history.pruneHistory(this.adapter, this.vault, this.name, id, { keepVersions: this.historyConfig.maxVersions });
3674
3786
  }
3675
3787
  const envelope = await this.codec.encryptRecord(record, version, cek, options?.source, options?.sourceTs, vdigCtx, id);
3676
3788
  await this.adapter.put(this.vault, this.name, id, envelope);
3677
3789
  this.markerIds.delete(id);
3678
3790
  await this._ensureClassifiedMarker();
3679
3791
  if (this.embeddings) {
3680
- await this.searchStrategy.embedOnWrite(this.searchContext(), id, record, version);
3792
+ await this.strategies.search.embedOnWrite(this.searchContext(), id, record, version);
3681
3793
  }
3682
3794
  if (this.ledger) {
3683
3795
  const appendInput = {
@@ -3686,10 +3798,10 @@ var Collection = class {
3686
3798
  id,
3687
3799
  version,
3688
3800
  actor: this.keyring.userId,
3689
- payloadHash: await this.historyStrategy.envelopePayloadHash(envelope)
3801
+ payloadHash: await this.strategies.history.envelopePayloadHash(envelope)
3690
3802
  };
3691
3803
  if (existing) {
3692
- appendInput.delta = this.historyStrategy.computePatch(record, existing.record);
3804
+ appendInput.delta = this.strategies.history.computePatch(record, existing.record);
3693
3805
  }
3694
3806
  if (options?.reason !== void 0) appendInput.reason = options.reason;
3695
3807
  await this.ledger.append(appendInput);
@@ -4208,10 +4320,10 @@ var Collection = class {
4208
4320
  if (existing && this.historyConfig.enabled !== false) {
4209
4321
  const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
4210
4322
  const prevForVdig = this.vdigFields !== null ? await this.adapter.get(this.vault, this.name, id) : null;
4211
- await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, await this.codec.encryptRecord(existing.record, existing.version, cek, void 0, void 0, this.vdigFields !== null ? { id, prev: prevForVdig } : void 0, id));
4323
+ await this.strategies.history.saveHistory(this.adapter, this.vault, this.name, id, await this.codec.encryptRecord(existing.record, existing.version, cek, void 0, void 0, this.vdigFields !== null ? { id, prev: prevForVdig } : void 0, id));
4212
4324
  }
4213
4325
  const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
4214
- const previousPayloadHash = await this.historyStrategy.envelopePayloadHash(previousEnvelope);
4326
+ const previousPayloadHash = await this.strategies.history.envelopePayloadHash(previousEnvelope);
4215
4327
  let markerVersion;
4216
4328
  if (this.onDirty) {
4217
4329
  const live = previousEnvelope;
@@ -4422,28 +4534,28 @@ var Collection = class {
4422
4534
  * see `src/search/tokenize.ts` for the Thai/CJK caveat.
4423
4535
  */
4424
4536
  search(field, query, opts = {}) {
4425
- return this.searchStrategy.search(this.searchContext(), field, query, opts);
4537
+ return this.strategies.search.search(this.searchContext(), field, query, opts);
4426
4538
  }
4427
4539
  /** Force-persist the lexical index now — gated behind `searchStrategy: withSearch()`. */
4428
4540
  flushIndex() {
4429
- return this.searchStrategy.flushIndex(this.searchContext());
4541
+ return this.strategies.search.flushIndex(this.searchContext());
4430
4542
  }
4431
4543
  /** Pre-build the lexical index — gated behind `searchStrategy: withSearch()`. */
4432
4544
  warmIndex() {
4433
- return this.searchStrategy.warmIndex(this.searchContext());
4545
+ return this.strategies.search.warmIndex(this.searchContext());
4434
4546
  }
4435
4547
  /** Retrieval (lexical | semantic | hybrid) — gated behind `searchStrategy: withSearch()`. */
4436
4548
  retrieve(query, opts = {}) {
4437
- return this.searchStrategy.retrieve(this.searchContext(), query, opts);
4549
+ return this.strategies.search.retrieve(this.searchContext(), query, opts);
4438
4550
  }
4439
4551
  /** Raw-vector kNN — gated behind `searchStrategy: withSearch()`. */
4440
4552
  similarTo(vector, opts = {}) {
4441
- return this.searchStrategy.similarTo(this.searchContext(), vector, opts);
4553
+ return this.strategies.search.similarTo(this.searchContext(), vector, opts);
4442
4554
  }
4443
4555
  /** Opt-in bulk `_vec` re-derive (#788) — a plain collection has nothing to rebuild; gated behind `searchStrategy: withSearch()` otherwise. */
4444
4556
  rebuildEmbeddings() {
4445
4557
  if (!this.embeddings) return Promise.resolve({ rebuilt: 0, skipped: 0 });
4446
- return this.searchStrategy.rebuildEmbeddings(this.searchContext());
4558
+ return this.strategies.search.rebuildEmbeddings(this.searchContext());
4447
4559
  }
4448
4560
  /**
4449
4561
  * Bind the {@link SearchContext} the search/retrieval surface needs. The
@@ -4653,7 +4765,7 @@ var Collection = class {
4653
4765
  ...this.defaultLocale !== void 0 ? { defaultLocale: this.defaultLocale } : {},
4654
4766
  ...resolver.resolveDictSource ? { resolveDictSource: (field) => resolver.resolveDictSource(leftCollection, field) } : {}
4655
4767
  } : void 0;
4656
- return new Query(source, void 0, joinContext, this.aggregateStrategy);
4768
+ return new Query(source, void 0, joinContext, this.strategies.aggregate);
4657
4769
  }
4658
4770
  /**
4659
4771
  * Subscribe to every put/delete on this collection. Returns an
@@ -4767,7 +4879,7 @@ var Collection = class {
4767
4879
  // ─── History Methods ────────────────────────────────────────────
4768
4880
  /** Get version history for a record, newest first. */
4769
4881
  async history(id, options) {
4770
- const envelopes = await this.historyStrategy.getHistoryEntries(
4882
+ const envelopes = await this.strategies.history.getHistoryEntries(
4771
4883
  this.adapter,
4772
4884
  this.vault,
4773
4885
  this.name,
@@ -4799,7 +4911,7 @@ var Collection = class {
4799
4911
  * and re-put the records through the normal `put()` path.
4800
4912
  */
4801
4913
  async getVersion(id, version) {
4802
- const envelope = await this.historyStrategy.getVersionEnvelope(
4914
+ const envelope = await this.strategies.history.getVersionEnvelope(
4803
4915
  this.adapter,
4804
4916
  this.vault,
4805
4917
  this.name,
@@ -4824,7 +4936,7 @@ var Collection = class {
4824
4936
  */
4825
4937
  async diff(id, versionA, versionB) {
4826
4938
  const recordA = versionA === 0 ? null : await this.resolveVersion(id, versionA);
4827
- return this.historyStrategy.diff(recordA, versionB === void 0 || versionB === 0 ? versionB === 0 ? null : await this.resolveCurrentOrVersion(id) : await this.resolveVersion(id, versionB));
4939
+ return this.strategies.history.diff(recordA, versionB === void 0 || versionB === 0 ? versionB === 0 ? null : await this.resolveCurrentOrVersion(id) : await this.resolveVersion(id, versionB));
4828
4940
  }
4829
4941
  /** Resolve a version: try history first, then check if it's the current version. */
4830
4942
  async resolveVersion(id, version) {
@@ -4841,7 +4953,7 @@ var Collection = class {
4841
4953
  }
4842
4954
  /** Prune history entries for a record (or all records if id is undefined). */
4843
4955
  async pruneRecordHistory(id, options) {
4844
- const pruned = await this.historyStrategy.pruneHistory(
4956
+ const pruned = await this.strategies.history.pruneHistory(
4845
4957
  this.adapter,
4846
4958
  this.vault,
4847
4959
  this.name,
@@ -4860,7 +4972,7 @@ var Collection = class {
4860
4972
  }
4861
4973
  /** Clear all history for this collection (or a specific record). */
4862
4974
  async clearHistory(id) {
4863
- return this.historyStrategy.clearHistory(this.adapter, this.vault, this.name, id);
4975
+ return this.strategies.history.clearHistory(this.adapter, this.vault, this.name, id);
4864
4976
  }
4865
4977
  // ─── Core Methods ─────────────────────────────────────────────
4866
4978
  /**
@@ -5282,7 +5394,7 @@ var Collection = class {
5282
5394
  * vault-shared `_blob` DEK (enabling cross-collection deduplication).
5283
5395
  */
5284
5396
  blob(id) {
5285
- return this.blobStrategy.openSlot({
5397
+ return this.strategies.blob.openSlot({
5286
5398
  store: this.adapter,
5287
5399
  vault: this.vault,
5288
5400
  collection: this.name,
@@ -5603,23 +5715,23 @@ var Collection = class {
5603
5715
  // ─── Hierarchical Access ──────────────────────────
5604
5716
  /** tier-aware put — gated behind `tiersStrategy: withTiers()`. `searchResidue: true` = stuck search compensation (#774, mirroring #764's elevate/demote posture); the write itself always completes. */
5605
5717
  putAtTier(id, record, tier, opts) {
5606
- return this.tiersStrategy.putAtTier(this.tiersContext(), id, record, tier, opts);
5718
+ return this.strategies.tiers.putAtTier(this.tiersContext(), id, record, tier, opts);
5607
5719
  }
5608
5720
  /** tier-aware get — gated behind `tiersStrategy: withTiers()`. */
5609
5721
  getAtTier(id) {
5610
- return this.tiersStrategy.getAtTier(this.tiersContext(), id);
5722
+ return this.strategies.tiers.getAtTier(this.tiersContext(), id);
5611
5723
  }
5612
5724
  /** list ids grouped by the caller's readability — gated behind `withTiers()`. */
5613
5725
  listAtTier() {
5614
- return this.tiersStrategy.listAtTier(this.tiersContext());
5726
+ return this.strategies.tiers.listAtTier(this.tiersContext());
5615
5727
  }
5616
5728
  /** elevate a record to a higher tier — gated behind `withTiers()`. `searchResidue: true` = stuck search compensation (#764); the move itself always completes. */
5617
5729
  elevate(id, toTier) {
5618
- return this.tiersStrategy.elevate(this.tiersContext(), id, toTier);
5730
+ return this.strategies.tiers.elevate(this.tiersContext(), id, toTier);
5619
5731
  }
5620
5732
  /** demote a record to a lower tier — gated behind `withTiers()`. Same `searchResidue` posture as {@link elevate} (#764). */
5621
5733
  demote(id, toTier) {
5622
- return this.tiersStrategy.demote(this.tiersContext(), id, toTier);
5734
+ return this.strategies.tiers.demote(this.tiersContext(), id, toTier);
5623
5735
  }
5624
5736
  /**
5625
5737
  * Emit a cross-tier access event. The subscriber sink stays collection-resident (it captures `onCrossTierAccess`); the tiers module reaches it via the {@link TiersContext.emitCrossTierEvent} callback.
@@ -5680,16 +5792,16 @@ var Collection = class {
5680
5792
  },
5681
5793
  syncIndexes: (id, rec, version, priorEnvelope) => syncTierIndexes(this.indexingContext(), id, rec, version, priorEnvelope),
5682
5794
  syncSearch: (id, rec, version) => syncTierSearch(this.searchContext(), id, rec, version),
5683
- syncHistory: async (id, fromDek, toDek) => this.historyStrategy.rewrapHistory(this.adapter, this.vault, this.name, id, fromDek, toDek, await this.getDEK(this.name)),
5795
+ syncHistory: async (id, fromDek, toDek) => this.strategies.history.rewrapHistory(this.adapter, this.vault, this.name, id, fromDek, toDek, await this.getDEK(this.name)),
5684
5796
  saveHistorySnapshot: async (id, envelope) => {
5685
5797
  if (this.historyConfig.enabled === false) return;
5686
- await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, envelope);
5798
+ await this.strategies.history.saveHistory(this.adapter, this.vault, this.name, id, envelope);
5687
5799
  this.emitter.emit("history:save", { vault: this.vault, collection: this.name, id, version: envelope._v });
5688
- if (this.historyConfig.maxVersions) await this.historyStrategy.pruneHistory(this.adapter, this.vault, this.name, id, { keepVersions: this.historyConfig.maxVersions });
5800
+ if (this.historyConfig.maxVersions) await this.strategies.history.pruneHistory(this.adapter, this.vault, this.name, id, { keepVersions: this.historyConfig.maxVersions });
5689
5801
  },
5690
- historyEnabled: this.historyConfig.enabled !== false && this.historyStrategy !== NO_HISTORY,
5802
+ historyEnabled: this.historyConfig.enabled !== false && this.strategies.history !== NO_HISTORY,
5691
5803
  // #728/#737: lets putAtTier skip decrypting `existing` when no real history strategy is wired
5692
- syncBlobs: (id, fromTier, toTier) => this.blobStrategy !== NO_BLOBS ? this.blob(id).syncTierMove(fromTier, toTier, this.blobTierPolicy) : Promise.resolve(),
5804
+ syncBlobs: (id, fromTier, toTier) => this.strategies.blob !== NO_BLOBS ? this.blob(id).syncTierMove(fromTier, toTier, this.blobTierPolicy) : Promise.resolve(),
5693
5805
  // #724 I1: gate on blob storage enabled (not on declared blobFields) so undeclared-field blobs still rehome; #746: syncTierMove mints/resumes the rehome journal marker; self-no-ops on an empty slot map
5694
5806
  syncLedger: async (id) => {
5695
5807
  await this.ledger?.purgeRecordDeltas(this.name, id);
@@ -6017,46 +6129,6 @@ var DeferredNumberingStore = class {
6017
6129
  }
6018
6130
  };
6019
6131
 
6020
- // src/with-fork/shadow/strategy.ts
6021
- var NOT_ENABLED2 = new Error(
6022
- 'VaultFrame requires the shadow strategy. Import `{ withShadow }` from "@noy-db/hub/shadow" and pass it to `createNoydb({ shadowStrategy: withShadow() })`.'
6023
- );
6024
- var NO_SHADOW = {
6025
- buildFrame() {
6026
- throw NOT_ENABLED2;
6027
- }
6028
- };
6029
-
6030
- // src/with-audit/consent/strategy.ts
6031
- var NO_CONSENT = {
6032
- async write() {
6033
- },
6034
- async read() {
6035
- return [];
6036
- }
6037
- };
6038
-
6039
- // src/with-audit/periods/strategy.ts
6040
- var NOT_ENABLED3 = new Error(
6041
- 'Accounting periods require the periods strategy. Import `{ withPeriods }` from "@noy-db/hub/periods" and pass it to `createNoydb({ periodsStrategy: withPeriods() })`.'
6042
- );
6043
- var NO_PERIODS = {
6044
- async loadPeriods() {
6045
- return [];
6046
- },
6047
- async chainAnchor() {
6048
- return { priorPeriodHash: "" };
6049
- },
6050
- assertTsWritable() {
6051
- },
6052
- validatePeriodName() {
6053
- throw NOT_ENABLED3;
6054
- },
6055
- async appendPeriodLedgerEntry() {
6056
- throw NOT_ENABLED3;
6057
- }
6058
- };
6059
-
6060
6132
  // src/with-audit/periods/vault-facade.ts
6061
6133
  var VaultPeriods = class {
6062
6134
  constructor(deps) {
@@ -8058,36 +8130,22 @@ var Vault = class {
8058
8130
  syncAdapter;
8059
8131
  getPurgeableTargets;
8060
8132
  historyConfig;
8061
- /** tree-shake seam for the optional blob service; `undefined` means "blobs are off for this
8062
- * vault" every `collection.blob(id)` call throws with a pointer at `@noy-db/hub/blobs`. */
8063
- blobStrategy;
8133
+ /**
8134
+ * Every opt-in service, resolved once by `createNoydb` (#838). Shared by
8135
+ * reference with the owning `Noydb` and with every `Collection` this vault
8136
+ * opens, so all three always agree about which services are enabled.
8137
+ */
8138
+ strategies;
8064
8139
  objectStore;
8065
- /** Cold-storage archival strategy (the archive target store). */
8066
- archiveStrategy;
8067
8140
  /** Per-collection record archival policies. Indexed by collection name. */
8068
8141
  archiveRegistry = /* @__PURE__ */ new Map();
8069
- indexStrategy;
8070
- lazyStrategy;
8071
- aggregateStrategy;
8072
- crdtStrategy;
8073
- tiersStrategy;
8074
- searchStrategy;
8075
- /** Cargo (partition extraction) strategy — `NO_CARGO` (throwing) unless `withCargo()` was passed.
8076
- * Public so the `extractPartition` free function (which takes a `Vault`) routes through it. */
8077
- cargoStrategy;
8078
- brokerStrategy;
8079
- sealedRecordStrategy;
8080
- portabilityStrategy;
8081
- sequenceStrategy;
8082
- consentStrategy;
8083
8142
  periods;
8084
8143
  linksEnforcer;
8085
- shadowStrategy;
8086
- historyStrategy;
8087
- forgetStrategy;
8088
- i18nStrategy;
8089
- syncStrategy;
8090
- classifiedStrategy;
8144
+ /** Cargo (partition extraction) strategy — `NO_CARGO` (throwing) unless `withCargo()` was passed.
8145
+ * Public so the `extractPartition` free function (which takes a `Vault`) routes through it. */
8146
+ get cargoStrategy() {
8147
+ return this.strategies.cargo;
8148
+ }
8091
8149
  /** Per-vault guard registry; `null` until `_initGuards()` runs (or for vaults that never register
8092
8150
  * one) — the runtime class is dynamic-imported on demand to keep it out of the floor bundle. */
8093
8151
  guardRegistry = null;
@@ -8269,23 +8327,10 @@ var Vault = class {
8269
8327
  this.onRegisterConflictResolver = opts.onRegisterConflictResolver;
8270
8328
  this.syncAdapter = opts.syncAdapter;
8271
8329
  this.getPurgeableTargets = opts.getPurgeableTargets ?? (() => []);
8272
- this.blobStrategy = opts.blobStrategy;
8330
+ this.strategies = opts.strategies;
8273
8331
  this.objectStore = opts.objectStore;
8274
- this.archiveStrategy = opts.archiveStrategy;
8275
- this.indexStrategy = opts.indexStrategy;
8276
- this.lazyStrategy = opts.lazyStrategy;
8277
- this.aggregateStrategy = opts.aggregateStrategy;
8278
- this.crdtStrategy = opts.crdtStrategy;
8279
- this.tiersStrategy = opts.tiersStrategy;
8280
- this.searchStrategy = opts.searchStrategy;
8281
- this.cargoStrategy = opts.cargoStrategy ?? NO_CARGO;
8282
- this.brokerStrategy = opts.brokerStrategy ?? NO_BROKER;
8283
- this.sealedRecordStrategy = opts.sealedRecordStrategy ?? NO_SEALED_RECORD;
8284
- this.portabilityStrategy = opts.portabilityStrategy ?? NO_PORTABILITY;
8285
- this.sequenceStrategy = opts.sequenceStrategy ?? NO_SEQUENCE;
8286
- this.consentStrategy = opts.consentStrategy ?? NO_CONSENT;
8287
8332
  this.periods = new VaultPeriods({
8288
- strategy: opts.periodsStrategy ?? NO_PERIODS,
8333
+ strategy: this.strategies.periods,
8289
8334
  adapter: this.adapter,
8290
8335
  vault: this.name,
8291
8336
  encrypted: this.encrypted,
@@ -8310,12 +8355,6 @@ var Vault = class {
8310
8355
  getActiveTxContext: () => this.noydb._activeTxContextOrNull,
8311
8356
  emitter: this.emitter
8312
8357
  });
8313
- this.shadowStrategy = opts.shadowStrategy ?? NO_SHADOW;
8314
- this.historyStrategy = opts.historyStrategy ?? NO_HISTORY;
8315
- this.forgetStrategy = opts.forgetStrategy ?? NO_FORGET;
8316
- this.i18nStrategy = opts.i18nStrategy ?? NO_I18N;
8317
- this.syncStrategy = opts.syncStrategy ?? NO_SYNC;
8318
- this.classifiedStrategy = opts.classifiedStrategy ?? NO_CLASSIFIED;
8319
8358
  this.graph = new ViaGraph();
8320
8359
  void opts.guardStrategies;
8321
8360
  this.historyConfig = opts.historyConfig ?? { enabled: true };
@@ -8330,19 +8369,19 @@ var Vault = class {
8330
8369
  getDEK: (collection) => this.getDEK(collection),
8331
8370
  role: () => this.keyring.role,
8332
8371
  getRawRecord: async (collection, recId) => await this.collection(collection).get(recId, { locale: "raw" })
8333
- }, opts.attestationStrategy ?? NO_ATTESTATION);
8372
+ }, this.strategies.attestation);
8334
8373
  this.user = this.noydb.userApiFactory({
8335
8374
  adapter: this.adapter,
8336
8375
  vaultName: this.name,
8337
8376
  writerKeyringId: this.keyring.userId,
8338
8377
  getDek: () => this.getDEK(USER_ENVELOPE_COLLECTION),
8339
8378
  checkGate: (gate, presented) => this.noydb.checkGate(this.name, gate, presented),
8340
- exportAccessible: (opts2) => this.portabilityStrategy.exportAccessibleData(this, opts2),
8341
- unilateralWithdraw: (opts2) => this.portabilityStrategy.withdrawAccessibleData(this, opts2),
8342
- requestWithdraw: (opts2) => this.portabilityStrategy.requestWithdrawal(this, opts2),
8343
- listWithdrawals: (opts2) => this.portabilityStrategy.listWithdrawalRequests(this, opts2),
8344
- approveWithdraw: (requestId, opts2) => this.portabilityStrategy.approveWithdrawal(this, requestId, opts2),
8345
- rejectWithdraw: (requestId, opts2) => this.portabilityStrategy.rejectWithdrawal(this, requestId, opts2)
8379
+ exportAccessible: (opts2) => this.strategies.portability.exportAccessibleData(this, opts2),
8380
+ unilateralWithdraw: (opts2) => this.strategies.portability.withdrawAccessibleData(this, opts2),
8381
+ requestWithdraw: (opts2) => this.strategies.portability.requestWithdrawal(this, opts2),
8382
+ listWithdrawals: (opts2) => this.strategies.portability.listWithdrawalRequests(this, opts2),
8383
+ approveWithdraw: (requestId, opts2) => this.strategies.portability.approveWithdrawal(this, requestId, opts2),
8384
+ rejectWithdraw: (requestId, opts2) => this.strategies.portability.rejectWithdrawal(this, requestId, opts2)
8346
8385
  });
8347
8386
  this.custody = new CustodyApi(
8348
8387
  (options, factors) => this.noydb.grantCustodian(this.name, options, factors),
@@ -8400,7 +8439,7 @@ var Vault = class {
8400
8439
  * dispatch reads/writes — from this vault's OWN privates, rather than passing `this` itself. */
8401
8440
  _viaReconcileCtx() {
8402
8441
  return {
8403
- i18nStrategy: this.i18nStrategy,
8442
+ i18nStrategy: this.strategies.i18n,
8404
8443
  locale: this.locale,
8405
8444
  translateText: this.translateText,
8406
8445
  i18nFieldRegistry: this.i18nFieldRegistry,
@@ -8441,7 +8480,7 @@ var Vault = class {
8441
8480
  this.satelliteRegistry = declareSatellite({
8442
8481
  adapter: this.adapter,
8443
8482
  vaultName: this.name,
8444
- forgetSubjects: this.forgetStrategy.subjects,
8483
+ forgetSubjects: this.strategies.forget.subjects,
8445
8484
  getDEK: this.getDEK,
8446
8485
  getBaseSchema: (base) => this.collectionCache.get(base)?.getSchema(),
8447
8486
  getBaseCrdt: (base) => this.collectionCache.get(base)?.getConfig()?.crdt,
@@ -8561,22 +8600,9 @@ var Vault = class {
8561
8600
  // #693: re-create gate fallback
8562
8601
  historyConfig: effectiveHistoryConfig,
8563
8602
  historyConfigExplicit: options?.historyConfig !== void 0,
8564
- // thread the vault-wide blob strategy into every
8565
- // collection. `undefined` is intentionally preserved so the
8566
- // Collection constructor uses its NO_BLOBS default.
8567
- ...this.blobStrategy !== void 0 ? { blobStrategy: this.blobStrategy } : {},
8568
8603
  ...this.objectStore !== void 0 ? { objectStore: this.objectStore } : {},
8569
8604
  ...options?.blobFields !== void 0 ? { blobFields: options.blobFields } : {},
8570
- ...this.indexStrategy !== void 0 ? { indexStrategy: this.indexStrategy } : {},
8571
- ...this.lazyStrategy !== void 0 ? { lazyStrategy: this.lazyStrategy } : {},
8572
- ...this.aggregateStrategy !== void 0 ? { aggregateStrategy: this.aggregateStrategy } : {},
8573
- ...this.crdtStrategy !== void 0 ? { crdtStrategy: this.crdtStrategy } : {},
8574
- ...this.tiersStrategy !== void 0 ? { tiersStrategy: this.tiersStrategy } : {},
8575
- ...this.searchStrategy !== void 0 ? { searchStrategy: this.searchStrategy } : {},
8576
- historyStrategy: this.historyStrategy,
8577
- i18nStrategy: this.i18nStrategy,
8578
- syncStrategy: this.syncStrategy,
8579
- classifiedStrategy: this.classifiedStrategy,
8605
+ strategies: this.strategies,
8580
8606
  // Per-collection ledger opt-out: when this collection sets
8581
8607
  // `historyConfig.ledger: false`, withhold the ledger reference so all
8582
8608
  // four `if (this.ledger)` append sites in Collection no-op. The chain
@@ -8634,7 +8660,7 @@ var Vault = class {
8634
8660
  collOpts.sensitive = options.sensitive;
8635
8661
  }
8636
8662
  if (options?.perRecordKeys !== void 0) collOpts.perRecordKeys = options.perRecordKeys;
8637
- const subjectKey = this.forgetStrategy.subjects[collectionName];
8663
+ const subjectKey = this.strategies.forget.subjects[collectionName];
8638
8664
  if (subjectKey !== void 0) {
8639
8665
  if (options?.perRecordKeys === false) {
8640
8666
  console.warn(
@@ -9015,7 +9041,7 @@ var Vault = class {
9015
9041
  const values = getAtPath(obj, field);
9016
9042
  for (const value of values) {
9017
9043
  if (value === void 0 || value === null) continue;
9018
- this.i18nStrategy.validateI18nTextValue(value, field, descriptor);
9044
+ this.strategies.i18n.validateI18nTextValue(value, field, descriptor);
9019
9045
  }
9020
9046
  }
9021
9047
  }
@@ -9056,7 +9082,7 @@ var Vault = class {
9056
9082
  this.reservedLookupCollections.set(dictCollectionName(name), name);
9057
9083
  let handle = this.dictionaryCache.get(name);
9058
9084
  if (!handle) {
9059
- handle = this.i18nStrategy.buildDictionaryHandle({
9085
+ handle = this.strategies.i18n.buildDictionaryHandle({
9060
9086
  adapter: this.adapter,
9061
9087
  compartmentName: this.name,
9062
9088
  dictionaryName: name,
@@ -9269,7 +9295,7 @@ var Vault = class {
9269
9295
  };
9270
9296
  }
9271
9297
  if (!this.sequenceStore) {
9272
- this.sequenceStore = this.sequenceStrategy.createStore({
9298
+ this.sequenceStore = this.strategies.sequence.createStore({
9273
9299
  adapter: this.adapter,
9274
9300
  vault: this.name,
9275
9301
  encrypted: this.encrypted,
@@ -9367,7 +9393,7 @@ var Vault = class {
9367
9393
  return runListArchived(this._archiveContext(), collection);
9368
9394
  }
9369
9395
  _archiveContext() {
9370
- const strategy = this.archiveStrategy;
9396
+ const strategy = this.strategies.archive;
9371
9397
  if (!strategy) {
9372
9398
  throw new Error(
9373
9399
  "vault.archive/restore/listArchived require `archiveStrategy: withArchive({ store })` in createNoydb"
@@ -9534,7 +9560,7 @@ var Vault = class {
9534
9560
  */
9535
9561
  getLedgerOrNull() {
9536
9562
  if (!this.ledgerStore) {
9537
- this.ledgerStore = this.historyStrategy.buildLedger({
9563
+ this.ledgerStore = this.strategies.history.buildLedger({
9538
9564
  adapter: this.adapter,
9539
9565
  vault: this.name,
9540
9566
  encrypted: this.encrypted,
@@ -9559,7 +9585,7 @@ var Vault = class {
9559
9585
  * number of distinct subjects re-indexed.
9560
9586
  */
9561
9587
  async rebuildSubjectIndex() {
9562
- if (Object.keys(this.forgetStrategy.subjects).length === 0) {
9588
+ if (Object.keys(this.strategies.forget.subjects).length === 0) {
9563
9589
  throw new ForgetStrategyNotConfiguredError();
9564
9590
  }
9565
9591
  return rebuildSubjectIndex(
@@ -9567,7 +9593,7 @@ var Vault = class {
9567
9593
  this.name,
9568
9594
  this.getDEK,
9569
9595
  this.encrypted,
9570
- this.forgetStrategy.subjects,
9596
+ this.strategies.forget.subjects,
9571
9597
  async (collectionName, id, env) => {
9572
9598
  const coll = this.collection(collectionName);
9573
9599
  return coll._decodeEnvelope(env, id);
@@ -9592,7 +9618,7 @@ var Vault = class {
9592
9618
  * @throws ForgetStrategyNotConfiguredError when no `withForgetCascade` was set.
9593
9619
  */
9594
9620
  async forget(subjectId) {
9595
- if (Object.keys(this.forgetStrategy.subjects).length === 0) {
9621
+ if (Object.keys(this.strategies.forget.subjects).length === 0) {
9596
9622
  throw new ForgetStrategyNotConfiguredError();
9597
9623
  }
9598
9624
  const ledger = this.getLedgerOrNull();
@@ -9614,17 +9640,17 @@ var Vault = class {
9614
9640
  const sealedResidue = [];
9615
9641
  const indexResidue = [];
9616
9642
  const ledgerDeltaResidue = [];
9617
- const blobsEnabled = this.blobStrategy !== void 0;
9643
+ const blobsEnabled = this.strategies.blob !== NO_BLOBS;
9618
9644
  const actor = this.keyring.userId;
9619
9645
  const fanoutStats = { recordsErased: 0, aggregatesRecomputed: 0, residueFrozen: [], lookupReferencesCascaded: 0, lookupReferencesNullified: 0, lookupReferencesResidue: [], derivedResidueUndecodable: [], derivedResidueDeclined: [] };
9620
9646
  const { partitionSealedCekKeys, shouldSkipBlobScan, bumpResidueCount, residueNoticesFromMap } = await import("./purge-scope-FGNNLWHD.js");
9621
- const scopedPurge = this.forgetStrategy.scopedPurge === true;
9647
+ const scopedPurge = this.strategies.forget.scopedPurge === true;
9622
9648
  const sealedCekScopedSkipped = /* @__PURE__ */ new Map();
9623
9649
  const blobScopedSkipped = /* @__PURE__ */ new Map();
9624
9650
  for (const ref2 of allRefs) {
9625
9651
  const coll = this.collection(ref2.collection);
9626
9652
  const satelliteOf = ref2.satelliteOf;
9627
- const perRecordKeys = this.forgetStrategy.subjects[satelliteOf ?? ref2.collection] !== void 0;
9653
+ const perRecordKeys = this.strategies.forget.subjects[satelliteOf ?? ref2.collection] !== void 0;
9628
9654
  const lookupCompareKeys = await this.linksEnforcer.checkLookupRefsRestrict(this.graph, ref2.collection, ref2.collection, ref2.id);
9629
9655
  const live = await this.adapter.get(this.name, ref2.collection, ref2.id);
9630
9656
  if (perRecordKeys && live && live._data && !hasPerRecordKey(live)) {
@@ -9684,7 +9710,7 @@ var Vault = class {
9684
9710
  collections.add(ref2.collection);
9685
9711
  await forgetDerivedFanout(this, { collection: ref2.collection, id: ref2.id }, live, fanoutStats, lookupCompareKeys);
9686
9712
  }
9687
- historyVersionsShredded += await this.historyStrategy.tombstoneHistory(
9713
+ historyVersionsShredded += await this.strategies.history.tombstoneHistory(
9688
9714
  this.adapter,
9689
9715
  this.name,
9690
9716
  ref2.collection,
@@ -9814,7 +9840,7 @@ var Vault = class {
9814
9840
  * `<collection>/<id>/<pid>` key the delivery envelope was written under.
9815
9841
  */
9816
9842
  async sealRecordToHost(collection, id, hostSealer, opts) {
9817
- return this.sealedRecordStrategy.sealRecordToHost(this._sealingContext(), collection, id, hostSealer, opts);
9843
+ return this.strategies.sealedRecord.sealRecordToHost(this._sealingContext(), collection, id, hostSealer, opts);
9818
9844
  }
9819
9845
  /**
9820
9846
  * Revoke a sealed-CEK grant. **Default is SOFT**: deletes the `pid` delivery
@@ -9824,7 +9850,7 @@ var Vault = class {
9824
9850
  * longer open the record. See `revokeSealedRecord` in `record-keys/sealing.ts`.
9825
9851
  */
9826
9852
  async revokeSealedRecord(collection, id, pid, opts) {
9827
- return this.sealedRecordStrategy.revokeSealedRecord(this._sealingContext(), collection, id, pid, opts);
9853
+ return this.strategies.sealedRecord.revokeSealedRecord(this._sealingContext(), collection, id, pid, opts);
9828
9854
  }
9829
9855
  /**
9830
9856
  * HARD-rotate a record's CEK: decrypt the live body under the old CEK,
@@ -9844,7 +9870,7 @@ var Vault = class {
9844
9870
  * @throws {@link RecordCekNotFoundError} if the record is missing or has no `_cek`.
9845
9871
  */
9846
9872
  async rotateRecordCek(collection, id) {
9847
- return this.sealedRecordStrategy.rotateRecordCek(this._sealingContext(), collection, id);
9873
+ return this.strategies.sealedRecord.rotateRecordCek(this._sealingContext(), collection, id);
9848
9874
  }
9849
9875
  /**
9850
9876
  * Build the {@link SealingContext} the record-keys grantor functions need:
@@ -10151,7 +10177,7 @@ var Vault = class {
10151
10177
  */
10152
10178
  at(timestamp) {
10153
10179
  const iso = timestamp instanceof Date ? timestamp.toISOString() : timestamp;
10154
- return this.historyStrategy.buildVaultInstant(
10180
+ return this.strategies.history.buildVaultInstant(
10155
10181
  {
10156
10182
  adapter: this.adapter,
10157
10183
  name: this.name,
@@ -10183,11 +10209,11 @@ var Vault = class {
10183
10209
  *.
10184
10210
  */
10185
10211
  frame() {
10186
- return this.shadowStrategy.buildFrame(this);
10212
+ return this.strategies.shadow.buildFrame(this);
10187
10213
  }
10188
10214
  /** Credential-broker handle (#479); `NO_BROKER` throws unless `brokerStrategy: withBroker(config)` was passed to createNoydb(). */
10189
10215
  broker() {
10190
- return buildCredentialBrokerHandle(this.brokerStrategy, this.adapter, this.name, () => this.keyring);
10216
+ return buildCredentialBrokerHandle(this.strategies.broker, this.adapter, this.name, () => this.keyring);
10191
10217
  }
10192
10218
  /**
10193
10219
  * Run `fn` under a consent scope. Every `get` / `put` / `delete`
@@ -10229,7 +10255,7 @@ var Vault = class {
10229
10255
  *.
10230
10256
  */
10231
10257
  async consentAudit(filter = {}) {
10232
- return this.consentStrategy.read(this.adapter, this.name, this.encrypted, this.getDEK, filter);
10258
+ return this.strategies.consent.read(this.adapter, this.name, this.encrypted, this.getDEK, filter);
10233
10259
  }
10234
10260
  /**
10235
10261
  * Called by Collection after every access when a consent scope is
@@ -10240,7 +10266,7 @@ var Vault = class {
10240
10266
  async _logConsent(op, collection, recordId) {
10241
10267
  const ctx = this.consentContext;
10242
10268
  if (!ctx) return;
10243
- await this.consentStrategy.write(
10269
+ await this.strategies.consent.write(
10244
10270
  this.adapter,
10245
10271
  this.name,
10246
10272
  this.encrypted,
@@ -10719,7 +10745,7 @@ var Vault = class {
10719
10745
  vault: this.name,
10720
10746
  userId: () => this.keyring.userId,
10721
10747
  getLedgerOrNull: () => this.getLedgerOrNull(),
10722
- envelopePayloadHash: (envelope) => this.historyStrategy.envelopePayloadHash(envelope),
10748
+ envelopePayloadHash: (envelope) => this.strategies.history.envelopePayloadHash(envelope),
10723
10749
  reloadKeyringAndRebuildDEK: async () => {
10724
10750
  if (this.reloadKeyring) {
10725
10751
  this.keyring = await this.reloadKeyring();
@@ -11404,25 +11430,6 @@ var QuickUnlockStore = class {
11404
11430
  }
11405
11431
  };
11406
11432
 
11407
- // src/with-fork/snapshots/strategy.ts
11408
- var NOT_ENABLED4 = new Error(
11409
- 'Snapshots require the snapshot strategy. Import `{ withSnapshots }` from "@noy-db/hub/snapshots" and pass it to `createNoydb({ snapshotStrategy: withSnapshots({ store }) })`.'
11410
- );
11411
- var NO_SNAPSHOTS = {
11412
- async snapshot() {
11413
- throw NOT_ENABLED4;
11414
- },
11415
- async listSnapshots() {
11416
- throw NOT_ENABLED4;
11417
- },
11418
- async restoreSnapshot() {
11419
- throw NOT_ENABLED4;
11420
- },
11421
- async autoSnapshot() {
11422
- throw NOT_ENABLED4;
11423
- }
11424
- };
11425
-
11426
11433
  // src/with-fork/snapshots/scheduler.ts
11427
11434
  var SnapshotScheduler = class {
11428
11435
  policy;
@@ -11623,19 +11630,6 @@ var NoydbSnapshots = class {
11623
11630
  }
11624
11631
  };
11625
11632
 
11626
- // src/with-commit/tx/strategy.ts
11627
- var NOT_ENABLED5 = new Error(
11628
- 'Multi-record transactions require the tx strategy. Import `{ withTransactions }` from "@noy-db/hub/tx" and pass it to `createNoydb({ txStrategy: withTransactions() })`.'
11629
- );
11630
- var NO_TX = {
11631
- async runTransaction(_db, _fn, _options, _txInvariants) {
11632
- throw NOT_ENABLED5;
11633
- },
11634
- async runDryRun() {
11635
- throw NOT_ENABLED5;
11636
- }
11637
- };
11638
-
11639
11633
  // src/kernel/memory-store.ts
11640
11634
  function memoryStore() {
11641
11635
  const store = /* @__PURE__ */ new Map();
@@ -11719,23 +11713,6 @@ function memoryStore() {
11719
11713
  };
11720
11714
  }
11721
11715
 
11722
- // src/with-party/session/strategy.ts
11723
- function notEnabled3(op) {
11724
- return new Error(
11725
- `${op} requires the session strategy. Import \`{ withSession }\` from "@noy-db/hub/session" and pass it to \`createNoydb({ sessionStrategy: withSession() })\`.`
11726
- );
11727
- }
11728
- var NO_SESSION = {
11729
- validateSessionPolicy() {
11730
- throw notEnabled3("sessionPolicy");
11731
- },
11732
- createEnforcer() {
11733
- throw notEnabled3("session policy enforcement");
11734
- },
11735
- revokeAllSessions() {
11736
- }
11737
- };
11738
-
11739
11716
  // src/with-party/auth-introspection/index.ts
11740
11717
  async function describeAuthConfig(store, vault) {
11741
11718
  const policy = await loadVaultPolicy(store, vault) ?? defaultPolicySnapshot();
@@ -12689,21 +12666,22 @@ var Noydb = class {
12689
12666
  writeRelay;
12690
12667
  /** Per-vault policy enforcers. */
12691
12668
  policyEnforcers = /* @__PURE__ */ new Map();
12692
- txStrategy;
12693
- forgetStrategy;
12694
12669
  /**
12695
- * Opt-in sovereign-custody (FR-6) strategy `NO_CUSTODY` (throwing) unless
12696
- * `withCustody()` was passed. Public so `Vault` routes `vault.custody.liberate`
12697
- * through it; grant/revoke route through it from this class. @internal
12670
+ * Every opt-in service, resolved once (#838). Shared by reference with each
12671
+ * `Vault` this instance opens and, through them, every `Collection` so
12672
+ * there is exactly one resolution and no construction path can disagree
12673
+ * about which services are enabled (the #834 bug class).
12698
12674
  */
12699
- custodyStrategy;
12675
+ strategies;
12700
12676
  /**
12701
- * Opt-in multi-user team strategy (#267 keyring-grant → team split)
12702
- * `NO_TEAM` (throwing) unless `withTeam()` was passed; the keyring
12703
- * engines are linked only by the active strategy, not by this file. */
12704
- teamStrategy;
12705
- sessionStrategy;
12706
- syncStrategy;
12677
+ * Opt-in sovereign-custody (FR-6) strategy `NO_CUSTODY` (throwing) unless
12678
+ * `withCustody()` was passed. Kept as a public accessor because `Vault`
12679
+ * routes `vault.custody.liberate` through `this.noydb.custodyStrategy`.
12680
+ * @internal
12681
+ */
12682
+ get custodyStrategy() {
12683
+ return this.strategies.custody;
12684
+ }
12707
12685
  snapshots;
12708
12686
  policyManager;
12709
12687
  /** Pre-resolved policy-gate engine function (mirrors `coordinationProvider`/`userApiFactory` above). */
@@ -12747,14 +12725,9 @@ var Noydb = class {
12747
12725
  this.userApiFactory = options.userApiFactory;
12748
12726
  if (!options.policyFactory || !options.policyCheckGateFn) throw new ValidationError("Noydb must be constructed via createNoydb(), which resolves the default policy service.");
12749
12727
  this.policyCheckGate = options.policyCheckGateFn;
12750
- this.txStrategy = options.txStrategy ?? NO_TX;
12751
- this.forgetStrategy = options.forgetStrategy ?? NO_FORGET;
12752
- this.custodyStrategy = options.custodyStrategy ?? NO_CUSTODY;
12753
- this.teamStrategy = options.teamStrategy ?? NO_TEAM;
12754
- this.sessionStrategy = options.sessionStrategy ?? NO_SESSION;
12755
- this.syncStrategy = options.syncStrategy ?? NO_SYNC;
12728
+ this.strategies = resolveStrategies(options);
12756
12729
  this.snapshots = new NoydbSnapshots({
12757
- strategy: options.snapshotStrategy ?? NO_SNAPSHOTS,
12730
+ strategy: this.strategies.snapshot,
12758
12731
  user: options.user,
12759
12732
  isClosed: () => this.closed,
12760
12733
  getVault: (name) => this.vaultCache.get(name),
@@ -12791,7 +12764,7 @@ var Noydb = class {
12791
12764
  }
12792
12765
  });
12793
12766
  if (options.sessionPolicy) {
12794
- this.sessionStrategy.validateSessionPolicy(options.sessionPolicy);
12767
+ this.strategies.session.validateSessionPolicy(options.sessionPolicy);
12795
12768
  }
12796
12769
  this.#registerGuardGate();
12797
12770
  this.#registerPeriodGate();
@@ -12800,7 +12773,7 @@ var Noydb = class {
12800
12773
  }
12801
12774
  /** @internal — resolved forget strategy (NO_FORGET when not configured). */
12802
12775
  get _forgetStrategy() {
12803
- return this.forgetStrategy;
12776
+ return this.strategies.forget;
12804
12777
  }
12805
12778
  // GDPR subject-index maintenance. When `withForgetCascade` declares
12806
12779
  // any subject fields, keep the encrypted `_subject_index` in lock-step with
@@ -12813,7 +12786,7 @@ var Noydb = class {
12813
12786
  // does NOT) — drop the ref so a deleted record never lingers in the
12814
12787
  // index (RISK #2). Without it, forget() would try to shred a ghost.
12815
12788
  #registerForgetHooks() {
12816
- const subjects = this.forgetStrategy.subjects;
12789
+ const subjects = this.strategies.forget.subjects;
12817
12790
  if (Object.keys(subjects).length === 0) return;
12818
12791
  const subjectFieldFor = (collection) => subjects[collection];
12819
12792
  this.writeHooks.onAfterWrite(async (event) => {
@@ -13002,7 +12975,7 @@ var Noydb = class {
13002
12975
  if (targets.length > 0) {
13003
12976
  const primary = targets.find((t) => t.role === "sync-peer") ?? targets[0];
13004
12977
  const effectivePolicy = this.options.syncPolicy ?? primary.policy ?? INDEXED_STORE_POLICY;
13005
- syncEngine = this.syncStrategy.buildSyncEngine({
12978
+ syncEngine = this.strategies.sync.buildSyncEngine({
13006
12979
  local: this.options.store,
13007
12980
  remote: primary.store,
13008
12981
  vault: name,
@@ -13016,7 +12989,7 @@ var Noydb = class {
13016
12989
  for (const target of targets) {
13017
12990
  if (target === primary) continue;
13018
12991
  const targetPolicy = target.policy ?? this.options.syncPolicy ?? INDEXED_STORE_POLICY;
13019
- const engine = this.syncStrategy.buildSyncEngine({
12992
+ const engine = this.strategies.sync.buildSyncEngine({
13020
12993
  local: this.options.store,
13021
12994
  remote: target.store,
13022
12995
  vault: name,
@@ -13049,31 +13022,14 @@ var Noydb = class {
13049
13022
  syncAdapter: targets.length > 0 ? targets[0].store : void 0,
13050
13023
  getPurgeableTargets: () => targets.filter((t) => t.role === "backup" || t.role === "archive").map((t) => ({ store: t.store, role: t.role, ...t.label !== void 0 ? { label: t.label } : {} })),
13051
13024
  historyConfig: this.options.history,
13052
- ...this.options.blobStrategy !== void 0 ? { blobStrategy: this.options.blobStrategy } : {},
13025
+ // #838 the 21 conditional strategy spreads that used to live here are
13026
+ // one shared reference. This block is where #834's drift happened: a
13027
+ // copy of it silently omitted six strategies, and there was nothing to
13028
+ // catch that. There is no longer a per-service line to omit.
13029
+ strategies: this.strategies,
13053
13030
  ...this.options.objectStore !== void 0 ? { objectStore: this.options.objectStore } : {},
13054
- ...this.options.archiveStrategy !== void 0 ? { archiveStrategy: this.options.archiveStrategy } : {},
13055
- ...this.options.indexStrategy !== void 0 ? { indexStrategy: this.options.indexStrategy } : {},
13056
- ...this.options.lazyStrategy !== void 0 ? { lazyStrategy: this.options.lazyStrategy } : {},
13057
- ...this.options.aggregateStrategy !== void 0 ? { aggregateStrategy: this.options.aggregateStrategy } : {},
13058
- ...this.options.crdtStrategy !== void 0 ? { crdtStrategy: this.options.crdtStrategy } : {},
13059
- ...this.options.tiersStrategy !== void 0 ? { tiersStrategy: this.options.tiersStrategy } : {},
13060
- ...this.options.searchStrategy !== void 0 ? { searchStrategy: this.options.searchStrategy } : {},
13061
- ...this.options.cargoStrategy !== void 0 ? { cargoStrategy: this.options.cargoStrategy } : {},
13062
- ...this.options.brokerStrategy !== void 0 ? { brokerStrategy: this.options.brokerStrategy } : {},
13063
- ...this.options.consentStrategy !== void 0 ? { consentStrategy: this.options.consentStrategy } : {},
13064
- ...this.options.periodsStrategy !== void 0 ? { periodsStrategy: this.options.periodsStrategy } : {},
13065
- ...this.options.shadowStrategy !== void 0 ? { shadowStrategy: this.options.shadowStrategy } : {},
13066
- ...this.options.historyStrategy !== void 0 ? { historyStrategy: this.options.historyStrategy } : {},
13067
- ...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
13068
- ...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
13069
13031
  ...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
13070
13032
  ...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
13071
- forgetStrategy: this.forgetStrategy,
13072
- ...this.options.attestationStrategy !== void 0 ? { attestationStrategy: this.options.attestationStrategy } : {},
13073
- ...this.options.classifiedStrategy !== void 0 ? { classifiedStrategy: this.options.classifiedStrategy } : {},
13074
- ...this.options.sealedRecordStrategy !== void 0 ? { sealedRecordStrategy: this.options.sealedRecordStrategy } : {},
13075
- ...this.options.portabilityStrategy !== void 0 ? { portabilityStrategy: this.options.portabilityStrategy } : {},
13076
- ...this.options.sequenceStrategy !== void 0 ? { sequenceStrategy: this.options.sequenceStrategy } : {},
13077
13033
  locale: opts?.locale,
13078
13034
  ...opts?.meta !== void 0 ? { meta: opts.meta } : {},
13079
13035
  // Thread the translator hook so Collection.put() can invoke it
@@ -13142,7 +13098,7 @@ var Noydb = class {
13142
13098
  * Opt-in (#267): throws {@link TeamNotEnabledError} without `withTeam()`.
13143
13099
  */
13144
13100
  async grant(vault, options, factors) {
13145
- return this.teamStrategy.grant(this.team, vault, options, factors);
13101
+ return this.strategies.team.grant(this.team, vault, options, factors);
13146
13102
  }
13147
13103
  /**
13148
13104
  * Revoke a user's access to a vault.
@@ -13155,7 +13111,7 @@ var Noydb = class {
13155
13111
  * Opt-in (#267): throws {@link TeamNotEnabledError} without `withTeam()`.
13156
13112
  */
13157
13113
  async revoke(vault, options, factors) {
13158
- return this.teamStrategy.revoke(this.team, vault, options, factors);
13114
+ return this.strategies.team.revoke(this.team, vault, options, factors);
13159
13115
  }
13160
13116
  /**
13161
13117
  * Grant the FR-6 `custodian` role to a user (owner-only custody API).
@@ -13168,7 +13124,7 @@ var Noydb = class {
13168
13124
  * owner-only invariant even if a host mis-configures the gate.
13169
13125
  */
13170
13126
  async grantCustodian(vault, options, factors) {
13171
- return this.custodyStrategy.grantCustodian(this, vault, options, factors);
13127
+ return this.strategies.custody.grantCustodian(this, vault, options, factors);
13172
13128
  }
13173
13129
  /** @internal — grant-custodian engine, reached only via withCustody(); the
13174
13130
  * keyring engine arrives as an argument so the floor never carries it (#267). */
@@ -13187,7 +13143,7 @@ var Noydb = class {
13187
13143
  * 'owner'` check, so an admin cannot unwind a custodianship.
13188
13144
  */
13189
13145
  async revokeCustodian(vault, options, factors) {
13190
- return this.custodyStrategy.revokeCustodian(this, vault, options, factors);
13146
+ return this.strategies.custody.revokeCustodian(this, vault, options, factors);
13191
13147
  }
13192
13148
  /** @internal — revoke-custodian engine, reached only via withCustody().
13193
13149
  * Mirrors `_grantCustodianImpl` (#267: engine passed in). */
@@ -13266,7 +13222,7 @@ var Noydb = class {
13266
13222
  * Opt-in (#267): throws {@link TeamNotEnabledError} without `withTeam()`.
13267
13223
  */
13268
13224
  async rotate(vault, collections) {
13269
- return this.teamStrategy.rotate(this.team, vault, collections);
13225
+ return this.strategies.team.rotate(this.team, vault, collections);
13270
13226
  }
13271
13227
  /** List all users with access to a vault. */
13272
13228
  async listUsers(vault) {
@@ -13556,7 +13512,7 @@ var Noydb = class {
13556
13512
  }
13557
13513
  transaction(arg, maybeFn) {
13558
13514
  if (typeof arg === "function") {
13559
- return this.txStrategy.runTransaction(this, arg);
13515
+ return this.strategies.tx.runTransaction(this, arg);
13560
13516
  }
13561
13517
  if (typeof arg === "object" && arg !== null && arg.dryRun === true) {
13562
13518
  if (typeof maybeFn !== "function") {
@@ -13564,7 +13520,7 @@ var Noydb = class {
13564
13520
  "db.transaction({ dryRun: true }, fn) requires the callback as the second argument."
13565
13521
  );
13566
13522
  }
13567
- return this.txStrategy.runDryRun(this, maybeFn);
13523
+ return this.strategies.tx.runDryRun(this, maybeFn);
13568
13524
  }
13569
13525
  if (typeof arg === "object" && arg !== null && arg.amendment === true) {
13570
13526
  if (typeof maybeFn !== "function") {
@@ -13572,7 +13528,7 @@ var Noydb = class {
13572
13528
  "db.transaction({ amendment: true }, fn) requires the callback as the second argument."
13573
13529
  );
13574
13530
  }
13575
- return this.txStrategy.runTransaction(this, maybeFn, arg);
13531
+ return this.strategies.tx.runTransaction(this, maybeFn, arg);
13576
13532
  }
13577
13533
  const vault = arg;
13578
13534
  const comp = this.vaultCache.get(vault);
@@ -13582,7 +13538,7 @@ var Noydb = class {
13582
13538
  );
13583
13539
  }
13584
13540
  const engine = this.getSyncEngine(vault);
13585
- return this.syncStrategy.buildSyncTransaction(comp, engine);
13541
+ return this.strategies.sync.buildSyncTransaction(comp, engine);
13586
13542
  }
13587
13543
  /**
13588
13544
  * Internal accessor for the primary store — used by the tx
@@ -13863,7 +13819,7 @@ var Noydb = class {
13863
13819
  enforcer.destroy();
13864
13820
  }
13865
13821
  this.policyEnforcers.clear();
13866
- this.sessionStrategy.revokeAllSessions();
13822
+ this.strategies.session.revokeAllSessions();
13867
13823
  for (const engine of this.syncEngines.values()) {
13868
13824
  engine.stopAutoSync();
13869
13825
  }
@@ -14364,4 +14320,4 @@ export {
14364
14320
  Noydb,
14365
14321
  createNoydb
14366
14322
  };
14367
- //# sourceMappingURL=chunk-LUH2MPEF.js.map
14323
+ //# sourceMappingURL=chunk-W7TCXZWU.js.map