@klum-db/lobby 0.4.0-pre.0 → 0.4.0-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,6 +30,396 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/portal/share-link.ts
34
+ var share_link_exports = {};
35
+ __export(share_link_exports, {
36
+ ShareLinkResolutionError: () => ShareLinkResolutionError,
37
+ buildShareLinkForClient: () => buildShareLinkForClient,
38
+ resolveShareLinkAgainstRegistry: () => resolveShareLinkAgainstRegistry
39
+ });
40
+ async function registryRows(registry) {
41
+ await registry.list();
42
+ return registry.query().toArray();
43
+ }
44
+ async function resolveShareLinkAgainstRegistry(link, registry) {
45
+ const parsed = typeof link === "string" || link instanceof URL ? (0, import_share_link.parseShareLink)(link) : link;
46
+ const rows = await registryRows(registry);
47
+ const row = rows.find((r) => r.handle === parsed.vaultHandle);
48
+ if (!row) {
49
+ throw new ShareLinkResolutionError(
50
+ "UNKNOWN_VAULT_HANDLE",
51
+ `share link addresses vault handle "${parsed.vaultHandle}", which is not registered in this fleet.`
52
+ );
53
+ }
54
+ return {
55
+ link: parsed,
56
+ row,
57
+ consoleRoute: {
58
+ vaultId: row.vaultId,
59
+ collection: parsed.collection,
60
+ recordId: parsed.recordId,
61
+ ...parsed.period !== void 0 ? { period: parsed.period } : {},
62
+ ...parsed.version !== void 0 ? { version: parsed.version } : {}
63
+ }
64
+ };
65
+ }
66
+ async function buildShareLinkForClient(opts, registry) {
67
+ const rows = await registryRows(registry);
68
+ const row = rows.find((r) => r.partitionKey === opts.client && r.group === opts.group);
69
+ if (!row) {
70
+ throw new ShareLinkResolutionError(
71
+ "UNKNOWN_CLIENT",
72
+ `client "${opts.client}" is not registered in group "${opts.group}".`
73
+ );
74
+ }
75
+ if (row.handle === void 0) {
76
+ throw new ShareLinkResolutionError(
77
+ "NO_PORTAL_HANDLE",
78
+ `client "${opts.client}" has no portal vault handle \u2014 provision the portal before minting share links.`
79
+ );
80
+ }
81
+ return (0, import_share_link.buildShareLink)(
82
+ {
83
+ vaultHandle: row.handle,
84
+ collection: opts.collection,
85
+ recordId: opts.record,
86
+ ...opts.period !== void 0 ? { period: opts.period } : {},
87
+ ...opts.version !== void 0 ? { version: opts.version } : {},
88
+ ...opts.grantToken !== void 0 ? { grantToken: opts.grantToken } : {}
89
+ },
90
+ opts.base
91
+ );
92
+ }
93
+ var import_share_link, ShareLinkResolutionError;
94
+ var init_share_link = __esm({
95
+ "src/portal/share-link.ts"() {
96
+ "use strict";
97
+ import_share_link = require("@noy-db/hub/share-link");
98
+ ShareLinkResolutionError = class extends Error {
99
+ constructor(code, message) {
100
+ super(message);
101
+ this.code = code;
102
+ }
103
+ code;
104
+ name = "ShareLinkResolutionError";
105
+ };
106
+ }
107
+ });
108
+
109
+ // src/federation/insight-auto-push.ts
110
+ var InsightAutoPush;
111
+ var init_insight_auto_push = __esm({
112
+ "src/federation/insight-auto-push.ts"() {
113
+ "use strict";
114
+ InsightAutoPush = class {
115
+ constructor(recompute, isSource, onError = (err, pk) => console.warn(`[klum-db] insight auto-push failed for shard "${pk}":`, err), debounceMs) {
116
+ this.recompute = recompute;
117
+ this.isSource = isSource;
118
+ this.onError = onError;
119
+ this.debounceMs = debounceMs;
120
+ }
121
+ recompute;
122
+ isSource;
123
+ onError;
124
+ debounceMs;
125
+ dirty = /* @__PURE__ */ new Set();
126
+ pending = null;
127
+ timer = null;
128
+ resolvePending = null;
129
+ flushing = false;
130
+ /** Called from a shard's onAfterWrite hook. Marks the shard dirty + schedules a flush. */
131
+ noteWrite(partitionKey, collection) {
132
+ if (!this.isSource(collection)) return;
133
+ this.dirty.add(partitionKey);
134
+ if (this.debounceMs !== void 0) this.scheduleDebounced();
135
+ else this.schedule();
136
+ }
137
+ /** Resolve once no flush is pending (drains rescheduled flushes too). */
138
+ async whenSettled() {
139
+ while (this.pending) await this.pending;
140
+ }
141
+ // ── microtask path (unchanged default) ──
142
+ schedule() {
143
+ if (this.pending) return;
144
+ this.pending = this.runFlush().finally(() => {
145
+ this.pending = null;
146
+ if (this.dirty.size > 0) this.schedule();
147
+ });
148
+ }
149
+ // ── debounce path (#13) ──
150
+ scheduleDebounced() {
151
+ if (!this.pending) this.pending = new Promise((r) => {
152
+ this.resolvePending = r;
153
+ });
154
+ if (this.timer) clearTimeout(this.timer);
155
+ this.timer = setTimeout(() => {
156
+ this.timer = null;
157
+ if (this.flushing) return;
158
+ this.flushing = true;
159
+ void this.runFlush().finally(() => {
160
+ this.flushing = false;
161
+ if (this.dirty.size > 0) {
162
+ this.scheduleDebounced();
163
+ } else {
164
+ const resolve = this.resolvePending;
165
+ this.resolvePending = null;
166
+ this.pending = null;
167
+ resolve?.();
168
+ }
169
+ });
170
+ }, this.debounceMs);
171
+ }
172
+ async runFlush() {
173
+ await Promise.resolve();
174
+ const pks = [...this.dirty];
175
+ this.dirty.clear();
176
+ for (const pk of pks) {
177
+ try {
178
+ await this.recompute(pk);
179
+ } catch (err) {
180
+ this.onError(err, pk);
181
+ }
182
+ }
183
+ }
184
+ };
185
+ }
186
+ });
187
+
188
+ // src/federation/read-model.ts
189
+ var read_model_exports = {};
190
+ __export(read_model_exports, {
191
+ PostureViolationError: () => PostureViolationError,
192
+ ReadModel: () => ReadModel,
193
+ deriveShardSummary: () => deriveShardSummary,
194
+ openReadModel: () => openReadModel
195
+ });
196
+ function deriveShardSummary(spec, records, row) {
197
+ const ctx = {
198
+ vaultId: row.vaultId,
199
+ partitionKey: row.partitionKey,
200
+ schemaVersion: row.schemaVersion
201
+ };
202
+ return spec.derive(records, ctx);
203
+ }
204
+ function checkPosture(model, row, shard) {
205
+ const declared = new Set(model.posture.surface);
206
+ const undeclared = Object.keys(row).filter((k) => !declared.has(k) && !PROVENANCE_FIELDS.has(k));
207
+ if (undeclared.length > 0) throw new PostureViolationError(model.name, undeclared, shard);
208
+ }
209
+ async function openReadModel(group, opts) {
210
+ const target = opts.vault;
211
+ if (target === group.name || target.startsWith(`${group.name}--`)) {
212
+ throw new import_cargo.ValidationError(
213
+ `openReadModel: vault "${target}" is the "${group.name}" group itself or one of its shards \u2014 the read-model must live outside the group it composes.`
214
+ );
215
+ }
216
+ if (opts.models.length === 0) {
217
+ throw new import_cargo.ValidationError("openReadModel: at least one model is required.");
218
+ }
219
+ const seen = /* @__PURE__ */ new Set();
220
+ for (const m of opts.models) {
221
+ if (seen.has(m.name)) throw new import_cargo.ValidationError(`openReadModel: duplicate model name "${m.name}".`);
222
+ seen.add(m.name);
223
+ if (m.posture.surface.length === 0) {
224
+ throw new import_cargo.ValidationError(`openReadModel: model "${m.name}" declares an empty posture.surface.`);
225
+ }
226
+ }
227
+ const vault = await group.db.openVault(target);
228
+ const rm = new ReadModel(group, target, vault, opts.models, opts.shards);
229
+ const autoPush = opts.freshness?.autoPush;
230
+ if (autoPush) rm._armAutoPush(autoPush === true ? {} : autoPush);
231
+ return rm;
232
+ }
233
+ var import_cargo, PostureViolationError, PROVENANCE_FIELDS, ReadModel;
234
+ var init_read_model = __esm({
235
+ "src/federation/read-model.ts"() {
236
+ "use strict";
237
+ import_cargo = require("@noy-db/hub/cargo");
238
+ init_insight_auto_push();
239
+ PostureViolationError = class extends Error {
240
+ constructor(model, fields, shard) {
241
+ super(
242
+ `read-model "${model}": derive emitted undeclared field(s) [${fields.join(", ")}] for shard "${shard}" \u2014 every emitted field must be listed in posture.surface (fail closed; nothing was written).`
243
+ );
244
+ this.model = model;
245
+ this.fields = fields;
246
+ this.shard = shard;
247
+ }
248
+ model;
249
+ fields;
250
+ shard;
251
+ name = "PostureViolationError";
252
+ };
253
+ PROVENANCE_FIELDS = /* @__PURE__ */ new Set(["_shard", "_sourceId", "_sourceVersion"]);
254
+ ReadModel = class {
255
+ /** @internal */
256
+ constructor(group, vaultName, vault, models, shards) {
257
+ this.group = group;
258
+ this.vaultName = vaultName;
259
+ this.vault = vault;
260
+ this.models = models;
261
+ this.shards = shards;
262
+ }
263
+ group;
264
+ vaultName;
265
+ vault;
266
+ models;
267
+ shards;
268
+ /** @internal — auto-push controller; armed by {@link _armAutoPush}. */
269
+ autoPush;
270
+ /**
271
+ * @internal — wire the #13 controller (spec § 3). Trigger is the
272
+ * Noydb-level change stream filtered to this group's shard vaults
273
+ * (`<group>--<pk>`); the read-model vault can never match that prefix
274
+ * (guarded at open), so its own writes can't loop back. Recompute is
275
+ * a per-shard `refresh({ only })` — rollup re-reduces and mirror
276
+ * reconciles just that shard; other shards' rows are untouched
277
+ * (registered shards are preserved by the reconcile asymmetry).
278
+ */
279
+ _armAutoPush(cfg) {
280
+ const controller = new InsightAutoPush(
281
+ async (partitionKey) => {
282
+ await this.refresh({
283
+ only: [partitionKey],
284
+ ...cfg.minVersion !== void 0 ? { minVersion: cfg.minVersion } : {}
285
+ });
286
+ },
287
+ (collection) => this.models.some((m) => m.source === collection),
288
+ void 0,
289
+ cfg.debounceMs
290
+ );
291
+ this.autoPush = controller;
292
+ const prefix = `${this.group.name}--`;
293
+ this.group.db.on("change", (e) => {
294
+ if (!e.vault.startsWith(prefix)) return;
295
+ controller.noteWrite(e.vault.slice(prefix.length), e.collection);
296
+ });
297
+ }
298
+ /**
299
+ * Await any pending auto-push flush. Resolves immediately when
300
+ * `freshness.autoPush` is off or nothing is pending.
301
+ */
302
+ async whenSettled() {
303
+ if (this.autoPush) await this.autoPush.whenSettled();
304
+ }
305
+ /** An output collection of the read-model vault (keyed by model `name`). */
306
+ collection(model) {
307
+ return this.vault.collection(model);
308
+ }
309
+ /**
310
+ * Explicit refresh: for each model, read every eligible shard's
311
+ * `source`, posture-check, and write — rollup: one summary row per
312
+ * shard (id = partitionKey); mirror: one row per source record
313
+ * (id = `${shard}:${idOf(row)}`). Every row is stamped with `_shard`
314
+ * + `_sourceVersion` (mirror rows additionally `_sourceId`).
315
+ *
316
+ * Reconciliation (spec § 4): rows whose source record is gone, and
317
+ * all rows of a shard that LEFT the group, are deleted (`retracted`).
318
+ * Rows of a merely-unreachable shard are PRESERVED — offline is not
319
+ * departure; those shards land in `skippedVaults` instead.
320
+ */
321
+ async refresh(options = {}) {
322
+ const resolved = await this.group.resolveEligible({
323
+ ...options.minVersion !== void 0 ? { minVersion: options.minVersion } : {},
324
+ ...options.only !== void 0 ? { only: options.only } : {},
325
+ ...options.failFast !== void 0 ? { failFast: options.failFast } : {}
326
+ });
327
+ const eligible = this.shards ? resolved.eligible.filter((r) => this.shards(r)) : resolved.eligible;
328
+ const skipped = resolved.skipped;
329
+ let written = 0;
330
+ let retracted = 0;
331
+ for (const model of this.models) {
332
+ const results = await this.group.db.queryAcross(
333
+ eligible.map((r) => r.vaultId),
334
+ async (vault) => {
335
+ this.group.template.configure(vault);
336
+ const coll = vault.collection(model.source);
337
+ const records = await coll.list();
338
+ if (model.kind === "rollup") return records.map((record) => ({ record, id: "", version: 0 }));
339
+ return Promise.all(records.map(async (record) => {
340
+ const id = model.idOf(record);
341
+ const meta = await coll.getMetadata(id);
342
+ return { record, id, version: meta?.version ?? 0 };
343
+ }));
344
+ },
345
+ { create: false, ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {} }
346
+ );
347
+ const out = this.vault.collection(model.name);
348
+ const expected = /* @__PURE__ */ new Set();
349
+ const healthyShards = /* @__PURE__ */ new Set();
350
+ for (let i = 0; i < eligible.length; i++) {
351
+ const row = eligible[i];
352
+ const res = results[i];
353
+ if (!res || res.result === void 0) {
354
+ if (options.failFast && res?.error) throw res.error;
355
+ if (!skipped.some((s) => s.vaultId === row.vaultId)) {
356
+ skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
357
+ }
358
+ continue;
359
+ }
360
+ healthyShards.add(row.partitionKey);
361
+ if (model.kind === "rollup") {
362
+ const summary = deriveShardSummary(model, res.result.map((e) => e.record), row);
363
+ checkPosture(model, summary, row.partitionKey);
364
+ await out.put(row.partitionKey, {
365
+ ...summary,
366
+ _shard: row.partitionKey,
367
+ _sourceVersion: row.schemaVersion
368
+ });
369
+ expected.add(row.partitionKey);
370
+ written++;
371
+ } else {
372
+ for (const entry of res.result) {
373
+ checkPosture(model, entry.record, row.partitionKey);
374
+ const id = `${row.partitionKey}:${entry.id}`;
375
+ await out.put(id, {
376
+ ...entry.record,
377
+ _shard: row.partitionKey,
378
+ _sourceId: entry.id,
379
+ _sourceVersion: entry.version
380
+ });
381
+ expected.add(id);
382
+ written++;
383
+ }
384
+ }
385
+ }
386
+ retracted += await this.reconcile(out, model, expected, healthyShards);
387
+ }
388
+ return { written, retracted, skippedVaults: skipped };
389
+ }
390
+ /**
391
+ * Delete stale rows: id not in `expected` AND the row's shard is
392
+ * healthy this refresh (source record deleted), no longer in the
393
+ * group registry (shard departed), or outside the audience predicate
394
+ * (scope narrowed — out-of-scope data never lingers, spec § 5).
395
+ * Unreachable-but-registered in-scope shards are preserved.
396
+ */
397
+ async reconcile(out, model, expected, healthyShards) {
398
+ await this.group.registry.list();
399
+ const registryRows2 = new Map(
400
+ this.group.registry.query().toArray().filter((r) => r.group === this.group.name).map((r) => [r.partitionKey, r])
401
+ );
402
+ await out.list();
403
+ const rows = out.query().toArray();
404
+ let retracted = 0;
405
+ for (const row of rows) {
406
+ const shard = row["_shard"];
407
+ if (shard === void 0) continue;
408
+ const id = model.kind === "rollup" ? shard : `${shard}:${String(row["_sourceId"])}`;
409
+ if (expected.has(id)) continue;
410
+ const registryRow = registryRows2.get(shard);
411
+ const outOfScope = registryRow !== void 0 && this.shards !== void 0 && !this.shards(registryRow);
412
+ if (healthyShards.has(shard) || registryRow === void 0 || outOfScope) {
413
+ await out.delete(id);
414
+ retracted++;
415
+ }
416
+ }
417
+ return retracted;
418
+ }
419
+ };
420
+ }
421
+ });
422
+
33
423
  // src/bundle/uint32.ts
34
424
  function readUint32BE(bytes, offset) {
35
425
  return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
@@ -140,7 +530,7 @@ async function writeMultiVaultBundle(compartments, opts = {}) {
140
530
  handle: header.handle,
141
531
  exportedAt: c.exportedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
142
532
  innerBytes: innerBytes.length,
143
- innerSha256: await (0, import_cargo.sha256Hex)(innerBytes)
533
+ innerSha256: await (0, import_cargo2.sha256Hex)(innerBytes)
144
534
  };
145
535
  if (c.roleTag !== void 0) entry.roleTag = c.roleTag;
146
536
  if (c.disclose?.name !== void 0 && c.disclose.name !== false) {
@@ -163,7 +553,7 @@ async function writeMultiVaultBundle(compartments, opts = {}) {
163
553
  }
164
554
  const manifest = {
165
555
  multiFormatVersion: NOYDB_MULTI_BUNDLE_VERSION,
166
- handle: opts.handle ?? (0, import_cargo.generateULID)(),
556
+ handle: opts.handle ?? (0, import_cargo2.generateULID)(),
167
557
  compartments: entries
168
558
  };
169
559
  return encodeMultiBundle(manifest, inner);
@@ -176,7 +566,7 @@ async function readNoydbBundleManifest(bytes) {
176
566
  const entry = {
177
567
  handle: header.handle,
178
568
  innerBytes: bytes.length,
179
- innerSha256: await (0, import_cargo.sha256Hex)(bytes)
569
+ innerSha256: await (0, import_cargo2.sha256Hex)(bytes)
180
570
  };
181
571
  if (env !== void 0) entry.publicEnvelope = env;
182
572
  return [entry];
@@ -197,11 +587,11 @@ function readMultiVaultBundleCompartment(bytes, selector) {
197
587
  if (idx < 0 || idx >= inner.length) throw new Error(`readMultiVaultBundleCompartment: no compartment ${typeof selector === "number" ? `at index ${selector}` : `"${selector}"`}.`);
198
588
  return inner[idx];
199
589
  }
200
- var import_cargo, import_pod, NOYDB_MULTI_BUNDLE_MAGIC, NOYDB_MULTI_BUNDLE_PREFIX_BYTES, NOYDB_MULTI_BUNDLE_VERSION;
590
+ var import_cargo2, import_pod, NOYDB_MULTI_BUNDLE_MAGIC, NOYDB_MULTI_BUNDLE_PREFIX_BYTES, NOYDB_MULTI_BUNDLE_VERSION;
201
591
  var init_multi_bundle = __esm({
202
592
  "src/bundle/multi-bundle.ts"() {
203
593
  "use strict";
204
- import_cargo = require("@noy-db/hub/cargo");
594
+ import_cargo2 = require("@noy-db/hub/cargo");
205
595
  import_pod = require("@noy-db/hub/pod");
206
596
  init_uint32();
207
597
  NOYDB_MULTI_BUNDLE_MAGIC = new Uint8Array([78, 68, 66, 77]);
@@ -275,7 +665,7 @@ async function walkCrossVaultClosure(openVault, opts) {
275
665
  for (const vaultName of batch) {
276
666
  const seeds = perVaultSeeds.get(vaultName);
277
667
  const v = await openVault(vaultName);
278
- const { closure } = await (0, import_bundle.walkClosure)(v, {
668
+ const { closure } = await (0, import_cargo3.walkClosure)(v, {
279
669
  seeds,
280
670
  ...opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {}
281
671
  });
@@ -327,7 +717,7 @@ async function extractCrossVaultPartition(openVault, opts) {
327
717
  const sealIds = {};
328
718
  for (const [vaultName, seeds] of plan.perVaultSeeds) {
329
719
  const v = await openVault(vaultName);
330
- const { bundleBytes, transferKey, sealId } = await (0, import_bundle.extractPartition)(v, {
720
+ const { bundleBytes, transferKey, sealId } = await (0, import_cargo3.extractPartition)(v, {
331
721
  seeds,
332
722
  ...opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {},
333
723
  carrySchemas: opts.carrySchemas ?? true,
@@ -340,7 +730,7 @@ async function extractCrossVaultPartition(openVault, opts) {
340
730
  handle: header.handle,
341
731
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
342
732
  innerBytes: bundleBytes.length,
343
- innerSha256: await (0, import_cargo2.sha256Hex)(bundleBytes)
733
+ innerSha256: await (0, import_cargo4.sha256Hex)(bundleBytes)
344
734
  };
345
735
  if (meta?.roleTag !== void 0) entry.roleTag = meta.roleTag;
346
736
  if (meta?.disclose?.name !== void 0 && meta.disclose.name !== false) {
@@ -359,7 +749,7 @@ async function extractCrossVaultPartition(openVault, opts) {
359
749
  }
360
750
  const manifest = {
361
751
  multiFormatVersion: NOYDB_MULTI_BUNDLE_VERSION,
362
- handle: (0, import_cargo2.generateULID)(),
752
+ handle: (0, import_cargo4.generateULID)(),
363
753
  compartments
364
754
  };
365
755
  return { bundle: encodeMultiBundle(manifest, inner), transferKeys, sealIds };
@@ -369,7 +759,7 @@ async function describeCrossVaultExtraction(openVault, opts) {
369
759
  const compartments = [];
370
760
  for (const [vaultName, seeds] of plan.perVaultSeeds) {
371
761
  const v = await openVault(vaultName);
372
- const preview = await (0, import_bundle.describeExtraction)(v, {
762
+ const preview = await (0, import_cargo3.describeExtraction)(v, {
373
763
  seeds,
374
764
  ...opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {}
375
765
  });
@@ -377,13 +767,13 @@ async function describeCrossVaultExtraction(openVault, opts) {
377
767
  }
378
768
  return { compartments, dangling: plan.dangling };
379
769
  }
380
- var import_bundle, import_pod2, import_cargo2, CrossVaultDanglingRefError;
770
+ var import_cargo3, import_pod2, import_cargo4, CrossVaultDanglingRefError;
381
771
  var init_extract_cross_vault = __esm({
382
772
  "src/interchange/extract-cross-vault.ts"() {
383
773
  "use strict";
384
- import_bundle = require("@noy-db/hub/bundle");
774
+ import_cargo3 = require("@noy-db/hub/cargo");
385
775
  import_pod2 = require("@noy-db/hub/pod");
386
- import_cargo2 = require("@noy-db/hub/cargo");
776
+ import_cargo4 = require("@noy-db/hub/cargo");
387
777
  init_multi_bundle();
388
778
  CrossVaultDanglingRefError = class extends Error {
389
779
  constructor(dangling) {
@@ -469,7 +859,7 @@ async function mergeDecryptedRecords(receiver, decrypted, opts) {
469
859
  incomingSource.set(coll, srcMap);
470
860
  incomingSourceTs.set(coll, stsMap);
471
861
  }
472
- const diff = await (0, import_cargo3.diffVault)(receiver, candidate);
862
+ const diff = await (0, import_cargo5.diffVault)(receiver, candidate);
473
863
  const byCollection = {};
474
864
  const conflicts = [];
475
865
  const writes = [];
@@ -585,15 +975,15 @@ async function mergeDecryptedRecords(receiver, decrypted, opts) {
585
975
  };
586
976
  }
587
977
  async function mergeCompartment(receiver, compartmentBytes, opts) {
588
- const incoming = await (0, import_bundle2.decryptExtractedPartition)(compartmentBytes, opts.transferKey);
978
+ const incoming = await (0, import_cargo6.decryptExtractedPartition)(compartmentBytes, opts.transferKey);
589
979
  return mergeDecryptedRecords(receiver, incoming, opts);
590
980
  }
591
- var import_cargo3, import_bundle2, FieldLevelDeferredError;
981
+ var import_cargo5, import_cargo6, FieldLevelDeferredError;
592
982
  var init_merge_compartment = __esm({
593
983
  "src/interchange/merge-compartment.ts"() {
594
984
  "use strict";
595
- import_cargo3 = require("@noy-db/hub/cargo");
596
- import_bundle2 = require("@noy-db/hub/bundle");
985
+ import_cargo5 = require("@noy-db/hub/cargo");
986
+ import_cargo6 = require("@noy-db/hub/cargo");
597
987
  init_field_authority();
598
988
  FieldLevelDeferredError = class extends Error {
599
989
  constructor(collection) {
@@ -689,7 +1079,7 @@ async function graduate(noydb, docked, opts) {
689
1079
  await mergeDecryptedRecords(vault, staged, { strategy: "take-incoming", reason: "dock:graduate" });
690
1080
  let deedSealed = false;
691
1081
  if (opts.deed) {
692
- await (0, import_cargo4.createDeedOwner)(noydb._store, opts.vaultName, opts.deed.ownerId, opts.deed.sealingProvider);
1082
+ await (0, import_cargo8.createDeedOwner)(noydb._store, opts.vaultName, opts.deed.ownerId, opts.deed.sealingProvider);
693
1083
  deedSealed = true;
694
1084
  }
695
1085
  if (opts.stateVault) {
@@ -709,11 +1099,11 @@ async function graduate(noydb, docked, opts) {
709
1099
  event: { type: "unit-graduated", unitId: docked.unitId, vault: opts.vaultName }
710
1100
  };
711
1101
  }
712
- var import_cargo4, UnitGraduationError, ISO_EPOCH;
1102
+ var import_cargo8, UnitGraduationError, ISO_EPOCH;
713
1103
  var init_graduate = __esm({
714
1104
  "src/dock/graduate.ts"() {
715
1105
  "use strict";
716
- import_cargo4 = require("@noy-db/hub/cargo");
1106
+ import_cargo8 = require("@noy-db/hub/cargo");
717
1107
  init_merge_compartment();
718
1108
  init_stage_records();
719
1109
  UnitGraduationError = class extends Error {
@@ -743,7 +1133,7 @@ __export(surface_exports, {
743
1133
  async function proposeSurface(smv, def, proposedBy, now) {
744
1134
  const row = {
745
1135
  ...def,
746
- id: def.id ?? (0, import_cargo5.generateULID)(),
1136
+ id: def.id ?? (0, import_cargo9.generateULID)(),
747
1137
  status: "proposed",
748
1138
  proposedBy,
749
1139
  createdAt: now
@@ -764,7 +1154,7 @@ async function exportSurface(source, surface) {
764
1154
  throw new SurfaceStateError(surface.id, surface.status, "agreed");
765
1155
  }
766
1156
  const seeds = Object.fromEntries(surface.collections.map((c) => [c, () => true]));
767
- const { bundleBytes, transferKey } = await (0, import_bundle4.extractPartition)(source, {
1157
+ const { bundleBytes, transferKey } = await (0, import_cargo10.extractPartition)(source, {
768
1158
  seeds,
769
1159
  maxDepth: 0,
770
1160
  ...surface.fields ? { fieldProjection: surface.fields } : {},
@@ -802,12 +1192,12 @@ async function markSynced(smv, id, now) {
802
1192
  nextSyncDueAt: now + cadenceMs
803
1193
  });
804
1194
  }
805
- var import_cargo5, import_bundle4, SurfaceNotFoundError, SurfaceStateError, SurfaceCadenceScheduler;
1195
+ var import_cargo9, import_cargo10, SurfaceNotFoundError, SurfaceStateError, SurfaceCadenceScheduler;
806
1196
  var init_surface = __esm({
807
1197
  "src/interchange/surface.ts"() {
808
1198
  "use strict";
809
- import_cargo5 = require("@noy-db/hub/cargo");
810
- import_bundle4 = require("@noy-db/hub/bundle");
1199
+ import_cargo9 = require("@noy-db/hub/cargo");
1200
+ import_cargo10 = require("@noy-db/hub/cargo");
811
1201
  init_merge_compartment();
812
1202
  SurfaceNotFoundError = class extends Error {
813
1203
  name = "SurfaceNotFoundError";
@@ -912,38 +1302,38 @@ function canonical(value) {
912
1302
  return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
913
1303
  }
914
1304
  async function fingerprintBlueprint(bp) {
915
- return (0, import_cargo6.sha256Hex)(new TextEncoder().encode(canonical(bp)));
1305
+ return (0, import_cargo11.sha256Hex)(new TextEncoder().encode(canonical(bp)));
916
1306
  }
917
- var import_cargo6;
1307
+ var import_cargo11;
918
1308
  var init_schema_manifest = __esm({
919
1309
  "src/federation/schema-manifest.ts"() {
920
1310
  "use strict";
921
- import_cargo6 = require("@noy-db/hub/cargo");
1311
+ import_cargo11 = require("@noy-db/hub/cargo");
922
1312
  }
923
1313
  });
924
1314
 
925
1315
  // src/federation/constants.ts
926
- var import_cargo7;
1316
+ var import_cargo12;
927
1317
  var init_constants = __esm({
928
1318
  "src/federation/constants.ts"() {
929
1319
  "use strict";
930
- import_cargo7 = require("@noy-db/hub/cargo");
1320
+ import_cargo12 = require("@noy-db/hub/cargo");
931
1321
  }
932
1322
  });
933
1323
 
934
1324
  // src/federation/state-vault.ts
935
1325
  var state_vault_exports = {};
936
1326
  __export(state_vault_exports, {
937
- STATE_VAULT_NAME: () => import_cargo7.STATE_VAULT_NAME,
1327
+ STATE_VAULT_NAME: () => import_cargo12.STATE_VAULT_NAME,
938
1328
  StateManagementVault: () => StateManagementVault
939
1329
  });
940
- var import_cargo8, REGISTRY, MANIFEST, EVENTS, MIGRATION_STATUS, SURFACES, StateManagementVault;
1330
+ var import_cargo13, REGISTRY, MANIFEST, EVENTS, MIGRATION_STATUS, SURFACES, StateManagementVault;
941
1331
  var init_state_vault = __esm({
942
1332
  "src/federation/state-vault.ts"() {
943
1333
  "use strict";
944
1334
  init_schema_manifest();
945
1335
  init_constants();
946
- import_cargo8 = require("@noy-db/hub/cargo");
1336
+ import_cargo13 = require("@noy-db/hub/cargo");
947
1337
  init_constants();
948
1338
  REGISTRY = "vaultRegistry";
949
1339
  MANIFEST = "schemaManifest";
@@ -973,7 +1363,7 @@ var init_state_vault = __esm({
973
1363
  #surfaces;
974
1364
  /** Idempotently open the reserved state vault and bind the control-plane collections. */
975
1365
  static async open(db) {
976
- const vault = await db.openVault(import_cargo7.STATE_VAULT_NAME);
1366
+ const vault = await db.openVault(import_cargo12.STATE_VAULT_NAME);
977
1367
  return new _StateManagementVault(
978
1368
  vault.collection(REGISTRY),
979
1369
  vault.collection(MANIFEST),
@@ -1033,7 +1423,7 @@ var init_state_vault = __esm({
1033
1423
  */
1034
1424
  async appendEvent(event) {
1035
1425
  const ts = event.ts ?? Date.now();
1036
- const id = (0, import_cargo8.generateULID)();
1426
+ const id = (0, import_cargo13.generateULID)();
1037
1427
  await this.#events.put(id, { ...event, id, ts });
1038
1428
  }
1039
1429
  /**
@@ -1075,13 +1465,13 @@ var init_state_vault = __esm({
1075
1465
 
1076
1466
  // src/federation/classify-skip.ts
1077
1467
  function classifyShardSkip(err) {
1078
- return err instanceof import_cargo9.NoAccessError ? "no-grant" : "error";
1468
+ return err instanceof import_cargo14.NoAccessError ? "no-grant" : "error";
1079
1469
  }
1080
- var import_cargo9;
1470
+ var import_cargo14;
1081
1471
  var init_classify_skip = __esm({
1082
1472
  "src/federation/classify-skip.ts"() {
1083
1473
  "use strict";
1084
- import_cargo9 = require("@noy-db/hub/cargo");
1474
+ import_cargo14 = require("@noy-db/hub/cargo");
1085
1475
  }
1086
1476
  });
1087
1477
 
@@ -1106,7 +1496,7 @@ async function applyBroadcastLegs(rows, legs) {
1106
1496
  for (const leg of legs) {
1107
1497
  const map = /* @__PURE__ */ new Map();
1108
1498
  for (const rec of await leg.from.list()) {
1109
- const k = coerceKey((0, import_cargo10.readPath)(rec, leg.on));
1499
+ const k = coerceKey((0, import_cargo15.readPath)(rec, leg.on));
1110
1500
  if (k !== null && !map.has(k)) map.set(k, rec);
1111
1501
  }
1112
1502
  indexes.push({ leg, map });
@@ -1114,7 +1504,7 @@ async function applyBroadcastLegs(rows, legs) {
1114
1504
  return rows.map((row) => {
1115
1505
  const out = { ...row };
1116
1506
  for (const { leg, map } of indexes) {
1117
- const key = coerceKey((0, import_cargo10.readPath)(row, leg.field));
1507
+ const key = coerceKey((0, import_cargo15.readPath)(row, leg.field));
1118
1508
  const match = key === null ? null : map.get(key) ?? null;
1119
1509
  if (match === null && leg.mode === "warn") {
1120
1510
  warnOnceBroadcastMiss(leg.field, leg.as, key ?? "<null>");
@@ -1124,11 +1514,11 @@ async function applyBroadcastLegs(rows, legs) {
1124
1514
  return out;
1125
1515
  });
1126
1516
  }
1127
- var import_cargo10, warnedBroadcastKeys;
1517
+ var import_cargo15, warnedBroadcastKeys;
1128
1518
  var init_cross_shard_join = __esm({
1129
1519
  "src/federation/cross-shard-join.ts"() {
1130
1520
  "use strict";
1131
- import_cargo10 = require("@noy-db/hub/cargo");
1521
+ import_cargo15 = require("@noy-db/hub/cargo");
1132
1522
  warnedBroadcastKeys = /* @__PURE__ */ new Set();
1133
1523
  }
1134
1524
  });
@@ -1222,85 +1612,6 @@ var init_cross_vault_live = __esm({
1222
1612
  }
1223
1613
  });
1224
1614
 
1225
- // src/federation/insight-auto-push.ts
1226
- var InsightAutoPush;
1227
- var init_insight_auto_push = __esm({
1228
- "src/federation/insight-auto-push.ts"() {
1229
- "use strict";
1230
- InsightAutoPush = class {
1231
- constructor(recompute, isSource, onError = (err, pk) => console.warn(`[klum-db] insight auto-push failed for shard "${pk}":`, err), debounceMs) {
1232
- this.recompute = recompute;
1233
- this.isSource = isSource;
1234
- this.onError = onError;
1235
- this.debounceMs = debounceMs;
1236
- }
1237
- recompute;
1238
- isSource;
1239
- onError;
1240
- debounceMs;
1241
- dirty = /* @__PURE__ */ new Set();
1242
- pending = null;
1243
- timer = null;
1244
- resolvePending = null;
1245
- flushing = false;
1246
- /** Called from a shard's onAfterWrite hook. Marks the shard dirty + schedules a flush. */
1247
- noteWrite(partitionKey, collection) {
1248
- if (!this.isSource(collection)) return;
1249
- this.dirty.add(partitionKey);
1250
- if (this.debounceMs !== void 0) this.scheduleDebounced();
1251
- else this.schedule();
1252
- }
1253
- /** Resolve once no flush is pending (drains rescheduled flushes too). */
1254
- async whenSettled() {
1255
- while (this.pending) await this.pending;
1256
- }
1257
- // ── microtask path (unchanged default) ──
1258
- schedule() {
1259
- if (this.pending) return;
1260
- this.pending = this.runFlush().finally(() => {
1261
- this.pending = null;
1262
- if (this.dirty.size > 0) this.schedule();
1263
- });
1264
- }
1265
- // ── debounce path (#13) ──
1266
- scheduleDebounced() {
1267
- if (!this.pending) this.pending = new Promise((r) => {
1268
- this.resolvePending = r;
1269
- });
1270
- if (this.timer) clearTimeout(this.timer);
1271
- this.timer = setTimeout(() => {
1272
- this.timer = null;
1273
- if (this.flushing) return;
1274
- this.flushing = true;
1275
- void this.runFlush().finally(() => {
1276
- this.flushing = false;
1277
- if (this.dirty.size > 0) {
1278
- this.scheduleDebounced();
1279
- } else {
1280
- const resolve = this.resolvePending;
1281
- this.resolvePending = null;
1282
- this.pending = null;
1283
- resolve?.();
1284
- }
1285
- });
1286
- }, this.debounceMs);
1287
- }
1288
- async runFlush() {
1289
- await Promise.resolve();
1290
- const pks = [...this.dirty];
1291
- this.dirty.clear();
1292
- for (const pk of pks) {
1293
- try {
1294
- await this.recompute(pk);
1295
- } catch (err) {
1296
- this.onError(err, pk);
1297
- }
1298
- }
1299
- }
1300
- };
1301
- }
1302
- });
1303
-
1304
1615
  // src/federation/partial-reduce.ts
1305
1616
  function canPartialReduce(spec) {
1306
1617
  return Object.values(spec).every((r) => typeof r.merge === "function");
@@ -1339,12 +1650,12 @@ var init_partial_reduce = __esm({
1339
1650
  });
1340
1651
 
1341
1652
  // src/federation/aggregate-across.ts
1342
- var import_cargo11, import_cargo12, CrossVaultAggregation, CrossVaultGroupedAggregation;
1653
+ var import_cargo16, import_cargo17, CrossVaultAggregation, CrossVaultGroupedAggregation;
1343
1654
  var init_aggregate_across = __esm({
1344
1655
  "src/federation/aggregate-across.ts"() {
1345
1656
  "use strict";
1346
- import_cargo11 = require("@noy-db/hub/cargo");
1347
- import_cargo12 = require("@noy-db/hub/cargo");
1657
+ import_cargo16 = require("@noy-db/hub/cargo");
1658
+ import_cargo17 = require("@noy-db/hub/cargo");
1348
1659
  init_cross_vault_live();
1349
1660
  init_partial_reduce();
1350
1661
  CrossVaultAggregation = class {
@@ -1362,7 +1673,7 @@ var init_aggregate_across = __esm({
1362
1673
  return { result: finalizePartial(this.spec, mergePartials(this.spec, partials)), skippedVaults: skippedVaults2 };
1363
1674
  }
1364
1675
  const { records, skippedVaults } = await this.src.fanoutRecords(options);
1365
- return { result: (0, import_cargo11.reduceRecords)(records, this.spec), skippedVaults };
1676
+ return { result: (0, import_cargo16.reduceRecords)(records, this.spec), skippedVaults };
1366
1677
  }
1367
1678
  live(options = {}) {
1368
1679
  if (!this.bind) throw new Error("CrossVaultAggregation: live() requires a LiveBinding \u2014 use ShardedQuery.aggregate()");
@@ -1373,7 +1684,7 @@ var init_aggregate_across = __esm({
1373
1684
  isRelevant: this.bind.isRelevant,
1374
1685
  compute: async () => {
1375
1686
  const { records, skippedVaults } = await src.fanoutRecords(options);
1376
- return { value: (0, import_cargo11.reduceRecords)(records, spec), skipped: skippedVaults };
1687
+ return { value: (0, import_cargo16.reduceRecords)(records, spec), skipped: skippedVaults };
1377
1688
  },
1378
1689
  initialSnapshot: { value: void 0, skipped: [] },
1379
1690
  ...options.debounceMs !== void 0 ? { debounceMs: options.debounceMs } : {}
@@ -1408,7 +1719,7 @@ var init_aggregate_across = __esm({
1408
1719
  async run(options = {}) {
1409
1720
  const { records, skippedVaults } = await this.src.fanoutRecords(options);
1410
1721
  return {
1411
- results: (0, import_cargo12.groupAndReduce)(records, this.field, this.spec),
1722
+ results: (0, import_cargo17.groupAndReduce)(records, this.field, this.spec),
1412
1723
  skippedVaults
1413
1724
  };
1414
1725
  }
@@ -1423,7 +1734,7 @@ var init_aggregate_across = __esm({
1423
1734
  compute: async () => {
1424
1735
  const { records, skippedVaults } = await src.fanoutRecords(options);
1425
1736
  return {
1426
- records: (0, import_cargo12.groupAndReduce)(records, field, spec),
1737
+ records: (0, import_cargo17.groupAndReduce)(records, field, spec),
1427
1738
  skipped: skippedVaults
1428
1739
  };
1429
1740
  },
@@ -1496,7 +1807,7 @@ async function retrieveAcross(group, collectionName, query, opts = {}) {
1496
1807
  }
1497
1808
  return pv.hits.map((h) => ({ ...h, id: pv.vault + SEP + h.id }));
1498
1809
  });
1499
- const fused = (0, import_cargo13.fuseRetrieval)(lists, {
1810
+ const fused = (0, import_cargo18.fuseRetrieval)(lists, {
1500
1811
  ...opts.limit !== void 0 ? { limit: opts.limit } : {},
1501
1812
  ...opts.rrfK !== void 0 ? { k: opts.rrfK } : {}
1502
1813
  });
@@ -1506,11 +1817,11 @@ async function retrieveAcross(group, collectionName, query, opts = {}) {
1506
1817
  });
1507
1818
  return { hits, skippedVaults };
1508
1819
  }
1509
- var import_cargo13, SEP;
1820
+ var import_cargo18, SEP;
1510
1821
  var init_retrieve_across = __esm({
1511
1822
  "src/federation/retrieve-across.ts"() {
1512
1823
  "use strict";
1513
- import_cargo13 = require("@noy-db/hub/cargo");
1824
+ import_cargo18 = require("@noy-db/hub/cargo");
1514
1825
  init_classify_skip();
1515
1826
  SEP = "\0";
1516
1827
  }
@@ -1530,30 +1841,31 @@ function autoPushConfig(spec) {
1530
1841
  }
1531
1842
  function assertSafePartitionKey(partitionKey) {
1532
1843
  if (partitionKey.length === 0) {
1533
- throw new import_cargo14.ValidationError("partitionKey must be a non-empty string");
1844
+ throw new import_cargo19.ValidationError("partitionKey must be a non-empty string");
1534
1845
  }
1535
- if (partitionKey === import_cargo7.STATE_VAULT_NAME) {
1536
- throw new import_cargo14.ReservedVaultNameError(partitionKey);
1846
+ if (partitionKey === import_cargo12.STATE_VAULT_NAME) {
1847
+ throw new import_cargo19.ReservedVaultNameError(partitionKey);
1537
1848
  }
1538
1849
  if (!SAFE_PARTITION_KEY.test(partitionKey)) {
1539
- throw new import_cargo14.ValidationError(
1850
+ throw new import_cargo19.ValidationError(
1540
1851
  `partitionKey "${partitionKey}" contains characters outside [A-Za-z0-9._-]. Map your records to a store-safe key in sharding.keyOf.`
1541
1852
  );
1542
1853
  }
1543
1854
  if (partitionKey.includes(SHARD_SEPARATOR)) {
1544
- throw new import_cargo14.ValidationError(
1855
+ throw new import_cargo19.ValidationError(
1545
1856
  `partitionKey "${partitionKey}" must not contain "--" \u2014 it is reserved as the shard vault-id separator and would risk shard-id collisions.`
1546
1857
  );
1547
1858
  }
1548
1859
  }
1549
- var import_cargo14, SHARD_SEPARATOR, SAFE_PARTITION_KEY, VaultGroup, ShardedCollection, ShardedQuery, ShardedGroupedQuery;
1860
+ var import_cargo19, SHARD_SEPARATOR, SAFE_PARTITION_KEY, VaultGroup, ShardedCollection, ShardedQuery, ShardedGroupedQuery;
1550
1861
  var init_vault_group = __esm({
1551
1862
  "src/federation/vault-group.ts"() {
1552
1863
  "use strict";
1553
1864
  init_state_vault();
1554
- import_cargo14 = require("@noy-db/hub/cargo");
1865
+ import_cargo19 = require("@noy-db/hub/cargo");
1555
1866
  init_constants();
1556
1867
  init_classify_skip();
1868
+ init_read_model();
1557
1869
  init_cross_shard_join();
1558
1870
  init_cross_vault_live();
1559
1871
  init_insight_auto_push();
@@ -1572,7 +1884,7 @@ var init_vault_group = __esm({
1572
1884
  this.cutoverOnOpen = cutoverOnOpen;
1573
1885
  this.meta = meta;
1574
1886
  if (name.includes(SHARD_SEPARATOR)) {
1575
- throw new import_cargo14.ValidationError(
1887
+ throw new import_cargo19.ValidationError(
1576
1888
  `VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
1577
1889
  );
1578
1890
  }
@@ -1677,11 +1989,11 @@ var init_vault_group = __esm({
1677
1989
  const vaultId = this.shardVaultId(partitionKey);
1678
1990
  const row = await this.registry.get(this.registryId(partitionKey));
1679
1991
  const provisioned = await this.db._shardVaultProvisioned(vaultId);
1680
- if (row && !provisioned) throw new import_cargo14.ShardProvisioningError(vaultId, partitionKey);
1992
+ if (row && !provisioned) throw new import_cargo19.ShardProvisioningError(vaultId, partitionKey);
1681
1993
  if (row && provisioned) return this.openShard(partitionKey);
1682
1994
  if (region !== void 0) {
1683
1995
  const backendRegion = this.db._resolveBackend(vaultId).capabilities?.region;
1684
- if (backendRegion !== region) throw new import_cargo14.DataResidencyError(vaultId, region, backendRegion);
1996
+ if (backendRegion !== region) throw new import_cargo19.DataResidencyError(vaultId, region, backendRegion);
1685
1997
  }
1686
1998
  const vault = await this.db.openVault(vaultId);
1687
1999
  this.template.configure(vault);
@@ -1715,9 +2027,9 @@ var init_vault_group = __esm({
1715
2027
  async shard(partitionKey) {
1716
2028
  const vaultId = this.shardVaultId(partitionKey);
1717
2029
  const row = await this.registry.get(this.registryId(partitionKey));
1718
- if (!row) throw new import_cargo14.UnknownShardError(partitionKey, this.name);
2030
+ if (!row) throw new import_cargo19.UnknownShardError(partitionKey, this.name);
1719
2031
  const provisioned = await this.db._shardVaultProvisioned(vaultId);
1720
- if (!provisioned) throw new import_cargo14.ShardProvisioningError(vaultId, partitionKey);
2032
+ if (!provisioned) throw new import_cargo19.ShardProvisioningError(vaultId, partitionKey);
1721
2033
  return this.openShard(partitionKey);
1722
2034
  }
1723
2035
  /** A sharded view over one logical collection across all shards. */
@@ -1749,7 +2061,7 @@ var init_vault_group = __esm({
1749
2061
  for (const p of probes) {
1750
2062
  if ("error" in p) skipped.push({ vaultId: p.row.vaultId, reason: "error", error: p.error });
1751
2063
  else if (p.provisioned) eligible.push(p.row);
1752
- else skipped.push({ vaultId: p.row.vaultId, reason: "error", error: new import_cargo14.ShardProvisioningError(p.row.vaultId, p.row.partitionKey) });
2064
+ else skipped.push({ vaultId: p.row.vaultId, reason: "error", error: new import_cargo19.ShardProvisioningError(p.row.vaultId, p.row.partitionKey) });
1753
2065
  }
1754
2066
  return { eligible, skipped };
1755
2067
  }
@@ -1786,7 +2098,7 @@ var init_vault_group = __esm({
1786
2098
  withCrossVaultDerivation(spec) {
1787
2099
  const target = spec.target.vault;
1788
2100
  if (target === this.name || target.startsWith(`${this.name}${SHARD_SEPARATOR}`)) {
1789
- throw new import_cargo14.ValidationError(
2101
+ throw new import_cargo19.ValidationError(
1790
2102
  `withCrossVaultDerivation: target.vault "${target}" is the "${this.name}" group itself or one of its shards \u2014 an Insight summary must target a SEPARATE analytics vault, never write back into client-shard data (it would breach the per-shard DEK boundary). Use a distinct vault name.`
1791
2103
  );
1792
2104
  }
@@ -1846,12 +2158,7 @@ var init_vault_group = __esm({
1846
2158
  skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
1847
2159
  continue;
1848
2160
  }
1849
- const ctx = {
1850
- vaultId: row.vaultId,
1851
- partitionKey: row.partitionKey,
1852
- schemaVersion: row.schemaVersion
1853
- };
1854
- const summary = spec.derive(res.result, ctx);
2161
+ const summary = deriveShardSummary(spec, res.result, row);
1855
2162
  await out.put(row.partitionKey, summary);
1856
2163
  written++;
1857
2164
  }
@@ -1871,17 +2178,12 @@ var init_vault_group = __esm({
1871
2178
  const row = await this.registry.get(this.registryId(partitionKey));
1872
2179
  if (!row) return;
1873
2180
  const shard = await this.openShard(partitionKey);
1874
- const ctx = {
1875
- vaultId: row.vaultId,
1876
- partitionKey,
1877
- schemaVersion: row.schemaVersion
1878
- };
1879
2181
  for (const spec of this.crossVaultDerivations) {
1880
2182
  if (!spec.autoPush) continue;
1881
2183
  const min = autoPushConfig(spec)?.minVersion;
1882
2184
  if (min !== void 0 && row.schemaVersion < min) continue;
1883
2185
  const records = await shard.collection(spec.source).list();
1884
- const summary = spec.derive(records, ctx);
2186
+ const summary = deriveShardSummary(spec, records, row);
1885
2187
  const insight = await this.db.openVault(spec.target.vault);
1886
2188
  await insight.collection(spec.target.collection).put(partitionKey, summary);
1887
2189
  }
@@ -1912,7 +2214,7 @@ var init_vault_group = __esm({
1912
2214
  async cutoverShard(partitionKey) {
1913
2215
  const vaultId = this.shardVaultId(partitionKey);
1914
2216
  const row = await this.registry.get(this.registryId(partitionKey));
1915
- if (!row) throw new import_cargo14.UnknownShardError(partitionKey, this.name);
2217
+ if (!row) throw new import_cargo19.UnknownShardError(partitionKey, this.name);
1916
2218
  const target = this.template.version;
1917
2219
  const sv = await this.ensureStateVault();
1918
2220
  const base = { vaultId, group: this.name, currentVersion: row.schemaVersion, targetVersion: target };
@@ -1995,7 +2297,7 @@ var init_vault_group = __esm({
1995
2297
  let vault;
1996
2298
  if (!row) {
1997
2299
  if (this.group.sharding.autoCreate === false) {
1998
- throw new import_cargo14.UnknownShardError(key, this.group.name);
2300
+ throw new import_cargo19.UnknownShardError(key, this.group.name);
1999
2301
  }
2000
2302
  vault = await this.group.createShard(key, this.group.sharding.regionOf?.(record));
2001
2303
  } else {
@@ -2078,7 +2380,7 @@ var init_vault_group = __esm({
2078
2380
  this.group.template.configure(probe);
2079
2381
  for (const leg of this.coPartitionedLegs) {
2080
2382
  if (!probe.resolveRef(this.collectionName, leg.field)) {
2081
- throw new import_cargo14.CrossShardJoinError(
2383
+ throw new import_cargo19.CrossShardJoinError(
2082
2384
  `crossShardJoin("${leg.field}"): no ref() declared for "${leg.field}" on collection "${this.collectionName}" in template "${this.group.sharding.vaultTemplate}". Add refs: { ${leg.field}: ref('<target>') } to the template's collection options.`
2083
2385
  );
2084
2386
  }
@@ -2240,13 +2542,117 @@ var init_vault_group = __esm({
2240
2542
  }
2241
2543
  });
2242
2544
 
2545
+ // src/portal/provision.ts
2546
+ var provision_exports = {};
2547
+ __export(provision_exports, {
2548
+ provisionPortal: () => provisionPortal,
2549
+ revokePortalInvite: () => revokePortalInvite
2550
+ });
2551
+ async function requireRow(group, client) {
2552
+ const row = await group.registry.get(group.registryId(client));
2553
+ if (!row) throw new Error(`provisionPortal: client "${client}" has no registry row in group.`);
2554
+ return row;
2555
+ }
2556
+ async function provisionPortal(db, group, opts) {
2557
+ const existing = await group.registry.get(group.registryId(opts.client));
2558
+ if (!existing) await group.createShard(opts.client);
2559
+ const row = existing ?? await requireRow(group, opts.client);
2560
+ const vaultId = row.vaultId;
2561
+ for (const g of opts.firmGrants ?? []) {
2562
+ await db.grant(vaultId, {
2563
+ userId: g.userId,
2564
+ displayName: g.displayName,
2565
+ role: g.role,
2566
+ passphrase: g.passphrase
2567
+ });
2568
+ }
2569
+ const { issueInvite, issuePeerRecovery } = await import("@noy-db/on-magic-link");
2570
+ const mode = opts.mode ?? "invite";
2571
+ const issued = mode === "rebind" ? await issuePeerRecovery(db, vaultId, {
2572
+ userId: opts.invite.userId,
2573
+ displayName: opts.invite.displayName,
2574
+ ...opts.invite.role !== void 0 ? { role: opts.invite.role } : {},
2575
+ ...opts.invite.ttlMs !== void 0 ? { ttlMs: opts.invite.ttlMs } : {},
2576
+ ...opts.invite.tempPhrase !== void 0 ? { tempPhrase: opts.invite.tempPhrase } : {}
2577
+ }) : await issueInvite(db, vaultId, {
2578
+ userId: opts.invite.userId,
2579
+ displayName: opts.invite.displayName,
2580
+ role: opts.invite.role ?? "client",
2581
+ ...opts.invite.ttlMs !== void 0 ? { ttlMs: opts.invite.ttlMs } : {},
2582
+ ...opts.invite.tempPhrase !== void 0 ? { tempPhrase: opts.invite.tempPhrase } : {}
2583
+ });
2584
+ const auditRef = {
2585
+ tokenId: issued.payload.tokenId,
2586
+ userId: opts.invite.userId,
2587
+ kind: mode,
2588
+ issuedAt: Date.now(),
2589
+ expiresAt: issued.payload.expiresAt
2590
+ };
2591
+ const handle = row.handle ?? (0, import_cargo20.generateULID)();
2592
+ const updated = {
2593
+ ...row,
2594
+ handle,
2595
+ portal: {
2596
+ enabledAt: row.portal?.enabledAt ?? Date.now(),
2597
+ invites: [...row.portal?.invites ?? [], auditRef]
2598
+ }
2599
+ };
2600
+ await group.registry.put(group.registryId(opts.client), updated);
2601
+ return {
2602
+ vaultId,
2603
+ handle,
2604
+ invite: {
2605
+ encoded: issued.encoded,
2606
+ tokenId: issued.payload.tokenId,
2607
+ userId: opts.invite.userId,
2608
+ expiresAt: issued.payload.expiresAt
2609
+ },
2610
+ row: updated
2611
+ };
2612
+ }
2613
+ async function revokePortalInvite(db, group, opts) {
2614
+ const row = await requireRow(group, opts.client);
2615
+ const invites = row.portal?.invites ?? [];
2616
+ const ref = invites.find((i) => i.tokenId === opts.tokenId);
2617
+ if (!ref) {
2618
+ throw new Error(`revokePortalInvite: no invite with tokenId "${opts.tokenId}" for client "${opts.client}".`);
2619
+ }
2620
+ if (ref.revokedAt !== void 0) return row;
2621
+ const { revokeInvite } = await import("@noy-db/on-magic-link");
2622
+ await revokeInvite(db, row.vaultId, {
2623
+ tokenId: ref.tokenId,
2624
+ vault: row.vaultId,
2625
+ userId: ref.userId,
2626
+ kind: ref.kind === "rebind" ? "peer-recovery" : "invite",
2627
+ issuer: "",
2628
+ tempPhrase: "",
2629
+ expiresAt: ref.expiresAt
2630
+ });
2631
+ const updated = {
2632
+ ...row,
2633
+ portal: {
2634
+ enabledAt: row.portal?.enabledAt ?? Date.now(),
2635
+ invites: invites.map((i) => i.tokenId === opts.tokenId ? { ...i, revokedAt: Date.now() } : i)
2636
+ }
2637
+ };
2638
+ await group.registry.put(group.registryId(opts.client), updated);
2639
+ return updated;
2640
+ }
2641
+ var import_cargo20;
2642
+ var init_provision = __esm({
2643
+ "src/portal/provision.ts"() {
2644
+ "use strict";
2645
+ import_cargo20 = require("@noy-db/hub/cargo");
2646
+ }
2647
+ });
2648
+
2243
2649
  // src/index.ts
2244
2650
  var index_exports = {};
2245
2651
  __export(index_exports, {
2246
- CrossShardJoinError: () => import_cargo16.CrossShardJoinError,
2652
+ CrossShardJoinError: () => import_cargo22.CrossShardJoinError,
2247
2653
  CrossVaultDanglingRefError: () => CrossVaultDanglingRefError,
2248
- CustodyApi: () => import_cargo17.CustodyApi,
2249
- DataResidencyError: () => import_cargo16.DataResidencyError,
2654
+ CustodyApi: () => import_cargo23.CustodyApi,
2655
+ DataResidencyError: () => import_cargo22.DataResidencyError,
2250
2656
  DockedUnit: () => DockedUnit,
2251
2657
  FieldAuthorityPolicyMissingError: () => FieldAuthorityPolicyMissingError,
2252
2658
  FieldLevelDeferredError: () => FieldLevelDeferredError,
@@ -2257,17 +2663,19 @@ __export(index_exports, {
2257
2663
  NOYDB_MULTI_BUNDLE_MAGIC: () => NOYDB_MULTI_BUNDLE_MAGIC,
2258
2664
  NOYDB_MULTI_BUNDLE_PREFIX_BYTES: () => NOYDB_MULTI_BUNDLE_PREFIX_BYTES,
2259
2665
  NOYDB_MULTI_BUNDLE_VERSION: () => NOYDB_MULTI_BUNDLE_VERSION,
2260
- ReservedVaultNameError: () => import_cargo16.ReservedVaultNameError,
2261
- ShardProvisioningError: () => import_cargo16.ShardProvisioningError,
2666
+ PostureViolationError: () => PostureViolationError,
2667
+ ReservedVaultNameError: () => import_cargo22.ReservedVaultNameError,
2668
+ ShardProvisioningError: () => import_cargo22.ShardProvisioningError,
2669
+ ShareLinkResolutionError: () => ShareLinkResolutionError,
2262
2670
  SurfaceCadenceScheduler: () => SurfaceCadenceScheduler,
2263
2671
  SurfaceNotFoundError: () => SurfaceNotFoundError,
2264
2672
  SurfaceStateError: () => SurfaceStateError,
2265
2673
  UnitGraduationError: () => UnitGraduationError,
2266
- UnknownShardError: () => import_cargo16.UnknownShardError,
2267
- VaultTemplateNotFoundError: () => import_cargo16.VaultTemplateNotFoundError,
2674
+ UnknownShardError: () => import_cargo22.UnknownShardError,
2675
+ VaultTemplateNotFoundError: () => import_cargo22.VaultTemplateNotFoundError,
2268
2676
  agreeSurface: () => agreeSurface,
2269
2677
  applySurface: () => applySurface,
2270
- createDeedOwner: () => import_cargo17.createDeedOwner,
2678
+ createDeedOwner: () => import_cargo23.createDeedOwner,
2271
2679
  createLobby: () => createLobby,
2272
2680
  decodeMultiBundle: () => decodeMultiBundle,
2273
2681
  describeCrossVaultExtraction: () => describeCrossVaultExtraction,
@@ -2275,11 +2683,11 @@ __export(index_exports, {
2275
2683
  exportSurface: () => exportSurface,
2276
2684
  extractCrossVaultPartition: () => extractCrossVaultPartition,
2277
2685
  groupInspector: () => groupInspector,
2278
- isDeedVault: () => import_cargo17.isDeedVault,
2686
+ isDeedVault: () => import_cargo23.isDeedVault,
2279
2687
  isSurfaceDue: () => isSurfaceDue,
2280
- liberateVault: () => import_cargo17.liberateVault,
2688
+ liberateVault: () => import_cargo23.liberateVault,
2281
2689
  listDueSurfaces: () => listDueSurfaces,
2282
- loadDeedMarker: () => import_cargo17.loadDeedMarker,
2690
+ loadDeedMarker: () => import_cargo23.loadDeedMarker,
2283
2691
  markSynced: () => markSynced,
2284
2692
  mergeCompartment: () => mergeCompartment,
2285
2693
  mergeDecryptedRecords: () => mergeDecryptedRecords,
@@ -2294,7 +2702,7 @@ __export(index_exports, {
2294
2702
  writeMultiVaultBundle: () => writeMultiVaultBundle
2295
2703
  });
2296
2704
  module.exports = __toCommonJS(index_exports);
2297
- var import_cargo15 = require("@noy-db/hub/cargo");
2705
+ var import_cargo21 = require("@noy-db/hub/cargo");
2298
2706
 
2299
2707
  // src/dock/docked-unit.ts
2300
2708
  var DockedUnit = class {
@@ -2313,6 +2721,10 @@ var DockedUnit = class {
2313
2721
  }
2314
2722
  };
2315
2723
 
2724
+ // src/index.ts
2725
+ init_share_link();
2726
+ init_read_model();
2727
+
2316
2728
  // src/federation/group-inspector.ts
2317
2729
  function groupInspector(group) {
2318
2730
  let shardIds = /* @__PURE__ */ new Set();
@@ -2376,13 +2788,13 @@ async function meterGroup(group, opts = {}) {
2376
2788
  }
2377
2789
 
2378
2790
  // src/index.ts
2379
- var import_cargo16 = require("@noy-db/hub/cargo");
2791
+ var import_cargo22 = require("@noy-db/hub/cargo");
2380
2792
  init_multi_bundle();
2381
2793
  init_extract_cross_vault();
2382
2794
  init_merge_compartment();
2383
2795
 
2384
2796
  // src/interchange/migrate-then-merge.ts
2385
- var import_bundle3 = require("@noy-db/hub/bundle");
2797
+ var import_cargo7 = require("@noy-db/hub/cargo");
2386
2798
  init_merge_compartment();
2387
2799
  init_stage_records();
2388
2800
  init_stage_records();
@@ -2407,7 +2819,7 @@ async function migrateThenMerge(receiver, compartmentBytes, opts) {
2407
2819
  );
2408
2820
  }
2409
2821
  if (fromVersion > toVersion) throw new MinVersionError(fromVersion, toVersion);
2410
- const incoming = await (0, import_bundle3.decryptExtractedPartition)(compartmentBytes, opts.transferKey);
2822
+ const incoming = await (0, import_cargo7.decryptExtractedPartition)(compartmentBytes, opts.transferKey);
2411
2823
  const transformsByCollection = {};
2412
2824
  const migrationByCollection = {};
2413
2825
  for (const [coll, recs] of Object.entries(incoming)) {
@@ -2430,7 +2842,7 @@ async function migrateThenMerge(receiver, compartmentBytes, opts) {
2430
2842
 
2431
2843
  // src/index.ts
2432
2844
  init_field_authority();
2433
- var import_cargo17 = require("@noy-db/hub/cargo");
2845
+ var import_cargo23 = require("@noy-db/hub/cargo");
2434
2846
 
2435
2847
  // src/dock/unit-driver.ts
2436
2848
  var InMemoryUnitDriver = class {
@@ -2464,10 +2876,10 @@ var Lobby = class {
2464
2876
  }
2465
2877
  async openVaultGroup(name, opts) {
2466
2878
  const db = this.noydb;
2467
- if (db.isClosed) throw new import_cargo15.ValidationError("Instance is closed");
2468
- if (name === import_cargo15.STATE_VAULT_NAME) throw new import_cargo15.ReservedVaultNameError(name);
2879
+ if (db.isClosed) throw new import_cargo21.ValidationError("Instance is closed");
2880
+ if (name === import_cargo21.STATE_VAULT_NAME) throw new import_cargo21.ReservedVaultNameError(name);
2469
2881
  const template = this.vaultTemplates.get(opts.sharding.vaultTemplate);
2470
- if (!template) throw new import_cargo15.VaultTemplateNotFoundError(opts.sharding.vaultTemplate);
2882
+ if (!template) throw new import_cargo21.VaultTemplateNotFoundError(opts.sharding.vaultTemplate);
2471
2883
  const { VaultGroup: VaultGroup2 } = await Promise.resolve().then(() => (init_vault_group(), vault_group_exports));
2472
2884
  const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
2473
2885
  const stateVault = opts.registry ? void 0 : await StateManagementVault2.open(db);
@@ -2486,7 +2898,7 @@ var Lobby = class {
2486
2898
  }
2487
2899
  async openStateManagementVault() {
2488
2900
  const db = this.noydb;
2489
- if (db.isClosed) throw new import_cargo15.ValidationError("Instance is closed");
2901
+ if (db.isClosed) throw new import_cargo21.ValidationError("Instance is closed");
2490
2902
  const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
2491
2903
  return StateManagementVault2.open(db);
2492
2904
  }
@@ -2506,6 +2918,64 @@ var Lobby = class {
2506
2918
  const { graduate: graduateFn } = await Promise.resolve().then(() => (init_graduate(), graduate_exports));
2507
2919
  return graduateFn(this.noydb, docked, opts);
2508
2920
  }
2921
+ // ─── Portal deep-link addressing (#43) ────────────────────────────────────
2922
+ /**
2923
+ * Resolve a portal share link (raw URL/path or a parsed `ShareLink`)
2924
+ * against the fleet registry: `vaultHandle → registry row` plus a
2925
+ * console-route descriptor the firm app turns into its own URL.
2926
+ * Unknown/foreign handles fail closed with `ShareLinkResolutionError`.
2927
+ * Delegates to `resolveShareLinkAgainstRegistry` in `portal/share-link.ts`.
2928
+ */
2929
+ async resolveShareLink(link, opts) {
2930
+ const { resolveShareLinkAgainstRegistry: resolveShareLinkAgainstRegistry2 } = await Promise.resolve().then(() => (init_share_link(), share_link_exports));
2931
+ return resolveShareLinkAgainstRegistry2(link, opts.registry);
2932
+ }
2933
+ /**
2934
+ * Mint a portal share link for a fleet client (`client` partition key +
2935
+ * `group`), delegating the link grammar to `@noy-db/hub/share-link`.
2936
+ * Fails closed when the client is unknown or has no portal handle.
2937
+ * Delegates to `buildShareLinkForClient` in `portal/share-link.ts`.
2938
+ */
2939
+ async buildShareLink(parts, opts) {
2940
+ const { buildShareLinkForClient: buildShareLinkForClient2 } = await Promise.resolve().then(() => (init_share_link(), share_link_exports));
2941
+ return buildShareLinkForClient2(parts, opts.registry);
2942
+ }
2943
+ // ─── Portal provisioning (#42) ────────────────────────────────────────────
2944
+ /**
2945
+ * Provision (or re-invite, `mode: 'rebind'`) a client portal vault in
2946
+ * one audited operation: ensure the shard, assign the portal handle
2947
+ * (#43's link target), apply firm grants, issue the magic-link invite,
2948
+ * stamp the audit trail on the fleet registry row.
2949
+ * Needs `@noy-db/on-magic-link` (optional peer) and a session created
2950
+ * with `teamStrategy: withTeam()`.
2951
+ * Delegates to `provisionPortal` in `portal/provision.ts`.
2952
+ */
2953
+ async provisionPortal(group, opts) {
2954
+ const { provisionPortal: provisionPortalFn } = await Promise.resolve().then(() => (init_provision(), provision_exports));
2955
+ return provisionPortalFn(this.noydb, group, opts);
2956
+ }
2957
+ /**
2958
+ * Revoke an outstanding portal invite and stamp `revokedAt` on the
2959
+ * fleet audit trail. Idempotent; fails closed on an unknown tokenId.
2960
+ * Delegates to `revokePortalInvite` in `portal/provision.ts`.
2961
+ */
2962
+ async revokePortalInvite(group, opts) {
2963
+ const { revokePortalInvite: revokePortalInviteFn } = await Promise.resolve().then(() => (init_provision(), provision_exports));
2964
+ return revokePortalInviteFn(this.noydb, group, opts);
2965
+ }
2966
+ // ─── Federated read-model (#44) ───────────────────────────────────────────
2967
+ /**
2968
+ * Open a maintained federated read-model over `group` (#44 S1): rollup
2969
+ * models reduce each shard's `source` into one summary row per shard in
2970
+ * the read-model vault — deterministic ids, `_shard`/`_sourceVersion`
2971
+ * provenance, fail-closed `posture.surface` allowlist, explicit
2972
+ * `refresh()`. Spec: docs/federated-read-model.md.
2973
+ * Delegates to `openReadModel` in `federation/read-model.ts`.
2974
+ */
2975
+ async openReadModel(group, opts) {
2976
+ const { openReadModel: openReadModelFn } = await Promise.resolve().then(() => (init_read_model(), read_model_exports));
2977
+ return openReadModelFn(group, opts);
2978
+ }
2509
2979
  // ─── FR-7 Surface API ─────────────────────────────────────────────────────
2510
2980
  /**
2511
2981
  * Export a scoped partition from `vaultName` bounded to the given surface.
@@ -2588,8 +3058,10 @@ function createLobby(noydb) {
2588
3058
  NOYDB_MULTI_BUNDLE_MAGIC,
2589
3059
  NOYDB_MULTI_BUNDLE_PREFIX_BYTES,
2590
3060
  NOYDB_MULTI_BUNDLE_VERSION,
3061
+ PostureViolationError,
2591
3062
  ReservedVaultNameError,
2592
3063
  ShardProvisioningError,
3064
+ ShareLinkResolutionError,
2593
3065
  SurfaceCadenceScheduler,
2594
3066
  SurfaceNotFoundError,
2595
3067
  SurfaceStateError,