@klum-db/lobby 0.3.0-pre.1 → 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/README.md +7 -4
- package/dist/bin/klum.d.cts +1 -2
- package/dist/bin/klum.d.ts +1 -2
- package/dist/index.cjs +662 -186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +325 -9
- package/dist/index.d.ts +325 -9
- package/dist/index.js +588 -113
- package/dist/index.js.map +1 -1
- package/dist/{vault-group-D0EC-D5c.d.cts → vault-group-DJtzjwZK.d.cts} +23 -3
- package/dist/{vault-group-D0EC-D5c.d.ts → vault-group-DJtzjwZK.d.ts} +23 -3
- package/package.json +16 -11
package/dist/index.js
CHANGED
|
@@ -8,6 +8,399 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/portal/share-link.ts
|
|
12
|
+
var share_link_exports = {};
|
|
13
|
+
__export(share_link_exports, {
|
|
14
|
+
ShareLinkResolutionError: () => ShareLinkResolutionError,
|
|
15
|
+
buildShareLinkForClient: () => buildShareLinkForClient,
|
|
16
|
+
resolveShareLinkAgainstRegistry: () => resolveShareLinkAgainstRegistry
|
|
17
|
+
});
|
|
18
|
+
import {
|
|
19
|
+
parseShareLink,
|
|
20
|
+
buildShareLink as buildShareLinkFromParts
|
|
21
|
+
} from "@noy-db/hub/share-link";
|
|
22
|
+
async function registryRows(registry) {
|
|
23
|
+
await registry.list();
|
|
24
|
+
return registry.query().toArray();
|
|
25
|
+
}
|
|
26
|
+
async function resolveShareLinkAgainstRegistry(link, registry) {
|
|
27
|
+
const parsed = typeof link === "string" || link instanceof URL ? parseShareLink(link) : link;
|
|
28
|
+
const rows = await registryRows(registry);
|
|
29
|
+
const row = rows.find((r) => r.handle === parsed.vaultHandle);
|
|
30
|
+
if (!row) {
|
|
31
|
+
throw new ShareLinkResolutionError(
|
|
32
|
+
"UNKNOWN_VAULT_HANDLE",
|
|
33
|
+
`share link addresses vault handle "${parsed.vaultHandle}", which is not registered in this fleet.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
link: parsed,
|
|
38
|
+
row,
|
|
39
|
+
consoleRoute: {
|
|
40
|
+
vaultId: row.vaultId,
|
|
41
|
+
collection: parsed.collection,
|
|
42
|
+
recordId: parsed.recordId,
|
|
43
|
+
...parsed.period !== void 0 ? { period: parsed.period } : {},
|
|
44
|
+
...parsed.version !== void 0 ? { version: parsed.version } : {}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function buildShareLinkForClient(opts, registry) {
|
|
49
|
+
const rows = await registryRows(registry);
|
|
50
|
+
const row = rows.find((r) => r.partitionKey === opts.client && r.group === opts.group);
|
|
51
|
+
if (!row) {
|
|
52
|
+
throw new ShareLinkResolutionError(
|
|
53
|
+
"UNKNOWN_CLIENT",
|
|
54
|
+
`client "${opts.client}" is not registered in group "${opts.group}".`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (row.handle === void 0) {
|
|
58
|
+
throw new ShareLinkResolutionError(
|
|
59
|
+
"NO_PORTAL_HANDLE",
|
|
60
|
+
`client "${opts.client}" has no portal vault handle \u2014 provision the portal before minting share links.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return buildShareLinkFromParts(
|
|
64
|
+
{
|
|
65
|
+
vaultHandle: row.handle,
|
|
66
|
+
collection: opts.collection,
|
|
67
|
+
recordId: opts.record,
|
|
68
|
+
...opts.period !== void 0 ? { period: opts.period } : {},
|
|
69
|
+
...opts.version !== void 0 ? { version: opts.version } : {},
|
|
70
|
+
...opts.grantToken !== void 0 ? { grantToken: opts.grantToken } : {}
|
|
71
|
+
},
|
|
72
|
+
opts.base
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
var ShareLinkResolutionError;
|
|
76
|
+
var init_share_link = __esm({
|
|
77
|
+
"src/portal/share-link.ts"() {
|
|
78
|
+
"use strict";
|
|
79
|
+
ShareLinkResolutionError = class extends Error {
|
|
80
|
+
constructor(code, message) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.code = code;
|
|
83
|
+
}
|
|
84
|
+
code;
|
|
85
|
+
name = "ShareLinkResolutionError";
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// src/federation/insight-auto-push.ts
|
|
91
|
+
var InsightAutoPush;
|
|
92
|
+
var init_insight_auto_push = __esm({
|
|
93
|
+
"src/federation/insight-auto-push.ts"() {
|
|
94
|
+
"use strict";
|
|
95
|
+
InsightAutoPush = class {
|
|
96
|
+
constructor(recompute, isSource, onError = (err, pk) => console.warn(`[klum-db] insight auto-push failed for shard "${pk}":`, err), debounceMs) {
|
|
97
|
+
this.recompute = recompute;
|
|
98
|
+
this.isSource = isSource;
|
|
99
|
+
this.onError = onError;
|
|
100
|
+
this.debounceMs = debounceMs;
|
|
101
|
+
}
|
|
102
|
+
recompute;
|
|
103
|
+
isSource;
|
|
104
|
+
onError;
|
|
105
|
+
debounceMs;
|
|
106
|
+
dirty = /* @__PURE__ */ new Set();
|
|
107
|
+
pending = null;
|
|
108
|
+
timer = null;
|
|
109
|
+
resolvePending = null;
|
|
110
|
+
flushing = false;
|
|
111
|
+
/** Called from a shard's onAfterWrite hook. Marks the shard dirty + schedules a flush. */
|
|
112
|
+
noteWrite(partitionKey, collection) {
|
|
113
|
+
if (!this.isSource(collection)) return;
|
|
114
|
+
this.dirty.add(partitionKey);
|
|
115
|
+
if (this.debounceMs !== void 0) this.scheduleDebounced();
|
|
116
|
+
else this.schedule();
|
|
117
|
+
}
|
|
118
|
+
/** Resolve once no flush is pending (drains rescheduled flushes too). */
|
|
119
|
+
async whenSettled() {
|
|
120
|
+
while (this.pending) await this.pending;
|
|
121
|
+
}
|
|
122
|
+
// ── microtask path (unchanged default) ──
|
|
123
|
+
schedule() {
|
|
124
|
+
if (this.pending) return;
|
|
125
|
+
this.pending = this.runFlush().finally(() => {
|
|
126
|
+
this.pending = null;
|
|
127
|
+
if (this.dirty.size > 0) this.schedule();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// ── debounce path (#13) ──
|
|
131
|
+
scheduleDebounced() {
|
|
132
|
+
if (!this.pending) this.pending = new Promise((r) => {
|
|
133
|
+
this.resolvePending = r;
|
|
134
|
+
});
|
|
135
|
+
if (this.timer) clearTimeout(this.timer);
|
|
136
|
+
this.timer = setTimeout(() => {
|
|
137
|
+
this.timer = null;
|
|
138
|
+
if (this.flushing) return;
|
|
139
|
+
this.flushing = true;
|
|
140
|
+
void this.runFlush().finally(() => {
|
|
141
|
+
this.flushing = false;
|
|
142
|
+
if (this.dirty.size > 0) {
|
|
143
|
+
this.scheduleDebounced();
|
|
144
|
+
} else {
|
|
145
|
+
const resolve = this.resolvePending;
|
|
146
|
+
this.resolvePending = null;
|
|
147
|
+
this.pending = null;
|
|
148
|
+
resolve?.();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}, this.debounceMs);
|
|
152
|
+
}
|
|
153
|
+
async runFlush() {
|
|
154
|
+
await Promise.resolve();
|
|
155
|
+
const pks = [...this.dirty];
|
|
156
|
+
this.dirty.clear();
|
|
157
|
+
for (const pk of pks) {
|
|
158
|
+
try {
|
|
159
|
+
await this.recompute(pk);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
this.onError(err, pk);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// src/federation/read-model.ts
|
|
170
|
+
var read_model_exports = {};
|
|
171
|
+
__export(read_model_exports, {
|
|
172
|
+
PostureViolationError: () => PostureViolationError,
|
|
173
|
+
ReadModel: () => ReadModel,
|
|
174
|
+
deriveShardSummary: () => deriveShardSummary,
|
|
175
|
+
openReadModel: () => openReadModel
|
|
176
|
+
});
|
|
177
|
+
import { ValidationError } from "@noy-db/hub/cargo";
|
|
178
|
+
function deriveShardSummary(spec, records, row) {
|
|
179
|
+
const ctx = {
|
|
180
|
+
vaultId: row.vaultId,
|
|
181
|
+
partitionKey: row.partitionKey,
|
|
182
|
+
schemaVersion: row.schemaVersion
|
|
183
|
+
};
|
|
184
|
+
return spec.derive(records, ctx);
|
|
185
|
+
}
|
|
186
|
+
function checkPosture(model, row, shard) {
|
|
187
|
+
const declared = new Set(model.posture.surface);
|
|
188
|
+
const undeclared = Object.keys(row).filter((k) => !declared.has(k) && !PROVENANCE_FIELDS.has(k));
|
|
189
|
+
if (undeclared.length > 0) throw new PostureViolationError(model.name, undeclared, shard);
|
|
190
|
+
}
|
|
191
|
+
async function openReadModel(group, opts) {
|
|
192
|
+
const target = opts.vault;
|
|
193
|
+
if (target === group.name || target.startsWith(`${group.name}--`)) {
|
|
194
|
+
throw new ValidationError(
|
|
195
|
+
`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.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (opts.models.length === 0) {
|
|
199
|
+
throw new ValidationError("openReadModel: at least one model is required.");
|
|
200
|
+
}
|
|
201
|
+
const seen = /* @__PURE__ */ new Set();
|
|
202
|
+
for (const m of opts.models) {
|
|
203
|
+
if (seen.has(m.name)) throw new ValidationError(`openReadModel: duplicate model name "${m.name}".`);
|
|
204
|
+
seen.add(m.name);
|
|
205
|
+
if (m.posture.surface.length === 0) {
|
|
206
|
+
throw new ValidationError(`openReadModel: model "${m.name}" declares an empty posture.surface.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const vault = await group.db.openVault(target);
|
|
210
|
+
const rm = new ReadModel(group, target, vault, opts.models, opts.shards);
|
|
211
|
+
const autoPush = opts.freshness?.autoPush;
|
|
212
|
+
if (autoPush) rm._armAutoPush(autoPush === true ? {} : autoPush);
|
|
213
|
+
return rm;
|
|
214
|
+
}
|
|
215
|
+
var PostureViolationError, PROVENANCE_FIELDS, ReadModel;
|
|
216
|
+
var init_read_model = __esm({
|
|
217
|
+
"src/federation/read-model.ts"() {
|
|
218
|
+
"use strict";
|
|
219
|
+
init_insight_auto_push();
|
|
220
|
+
PostureViolationError = class extends Error {
|
|
221
|
+
constructor(model, fields, shard) {
|
|
222
|
+
super(
|
|
223
|
+
`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).`
|
|
224
|
+
);
|
|
225
|
+
this.model = model;
|
|
226
|
+
this.fields = fields;
|
|
227
|
+
this.shard = shard;
|
|
228
|
+
}
|
|
229
|
+
model;
|
|
230
|
+
fields;
|
|
231
|
+
shard;
|
|
232
|
+
name = "PostureViolationError";
|
|
233
|
+
};
|
|
234
|
+
PROVENANCE_FIELDS = /* @__PURE__ */ new Set(["_shard", "_sourceId", "_sourceVersion"]);
|
|
235
|
+
ReadModel = class {
|
|
236
|
+
/** @internal */
|
|
237
|
+
constructor(group, vaultName, vault, models, shards) {
|
|
238
|
+
this.group = group;
|
|
239
|
+
this.vaultName = vaultName;
|
|
240
|
+
this.vault = vault;
|
|
241
|
+
this.models = models;
|
|
242
|
+
this.shards = shards;
|
|
243
|
+
}
|
|
244
|
+
group;
|
|
245
|
+
vaultName;
|
|
246
|
+
vault;
|
|
247
|
+
models;
|
|
248
|
+
shards;
|
|
249
|
+
/** @internal — auto-push controller; armed by {@link _armAutoPush}. */
|
|
250
|
+
autoPush;
|
|
251
|
+
/**
|
|
252
|
+
* @internal — wire the #13 controller (spec § 3). Trigger is the
|
|
253
|
+
* Noydb-level change stream filtered to this group's shard vaults
|
|
254
|
+
* (`<group>--<pk>`); the read-model vault can never match that prefix
|
|
255
|
+
* (guarded at open), so its own writes can't loop back. Recompute is
|
|
256
|
+
* a per-shard `refresh({ only })` — rollup re-reduces and mirror
|
|
257
|
+
* reconciles just that shard; other shards' rows are untouched
|
|
258
|
+
* (registered shards are preserved by the reconcile asymmetry).
|
|
259
|
+
*/
|
|
260
|
+
_armAutoPush(cfg) {
|
|
261
|
+
const controller = new InsightAutoPush(
|
|
262
|
+
async (partitionKey) => {
|
|
263
|
+
await this.refresh({
|
|
264
|
+
only: [partitionKey],
|
|
265
|
+
...cfg.minVersion !== void 0 ? { minVersion: cfg.minVersion } : {}
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
(collection) => this.models.some((m) => m.source === collection),
|
|
269
|
+
void 0,
|
|
270
|
+
cfg.debounceMs
|
|
271
|
+
);
|
|
272
|
+
this.autoPush = controller;
|
|
273
|
+
const prefix = `${this.group.name}--`;
|
|
274
|
+
this.group.db.on("change", (e) => {
|
|
275
|
+
if (!e.vault.startsWith(prefix)) return;
|
|
276
|
+
controller.noteWrite(e.vault.slice(prefix.length), e.collection);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Await any pending auto-push flush. Resolves immediately when
|
|
281
|
+
* `freshness.autoPush` is off or nothing is pending.
|
|
282
|
+
*/
|
|
283
|
+
async whenSettled() {
|
|
284
|
+
if (this.autoPush) await this.autoPush.whenSettled();
|
|
285
|
+
}
|
|
286
|
+
/** An output collection of the read-model vault (keyed by model `name`). */
|
|
287
|
+
collection(model) {
|
|
288
|
+
return this.vault.collection(model);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Explicit refresh: for each model, read every eligible shard's
|
|
292
|
+
* `source`, posture-check, and write — rollup: one summary row per
|
|
293
|
+
* shard (id = partitionKey); mirror: one row per source record
|
|
294
|
+
* (id = `${shard}:${idOf(row)}`). Every row is stamped with `_shard`
|
|
295
|
+
* + `_sourceVersion` (mirror rows additionally `_sourceId`).
|
|
296
|
+
*
|
|
297
|
+
* Reconciliation (spec § 4): rows whose source record is gone, and
|
|
298
|
+
* all rows of a shard that LEFT the group, are deleted (`retracted`).
|
|
299
|
+
* Rows of a merely-unreachable shard are PRESERVED — offline is not
|
|
300
|
+
* departure; those shards land in `skippedVaults` instead.
|
|
301
|
+
*/
|
|
302
|
+
async refresh(options = {}) {
|
|
303
|
+
const resolved = await this.group.resolveEligible({
|
|
304
|
+
...options.minVersion !== void 0 ? { minVersion: options.minVersion } : {},
|
|
305
|
+
...options.only !== void 0 ? { only: options.only } : {},
|
|
306
|
+
...options.failFast !== void 0 ? { failFast: options.failFast } : {}
|
|
307
|
+
});
|
|
308
|
+
const eligible = this.shards ? resolved.eligible.filter((r) => this.shards(r)) : resolved.eligible;
|
|
309
|
+
const skipped = resolved.skipped;
|
|
310
|
+
let written = 0;
|
|
311
|
+
let retracted = 0;
|
|
312
|
+
for (const model of this.models) {
|
|
313
|
+
const results = await this.group.db.queryAcross(
|
|
314
|
+
eligible.map((r) => r.vaultId),
|
|
315
|
+
async (vault) => {
|
|
316
|
+
this.group.template.configure(vault);
|
|
317
|
+
const coll = vault.collection(model.source);
|
|
318
|
+
const records = await coll.list();
|
|
319
|
+
if (model.kind === "rollup") return records.map((record) => ({ record, id: "", version: 0 }));
|
|
320
|
+
return Promise.all(records.map(async (record) => {
|
|
321
|
+
const id = model.idOf(record);
|
|
322
|
+
const meta = await coll.getMetadata(id);
|
|
323
|
+
return { record, id, version: meta?.version ?? 0 };
|
|
324
|
+
}));
|
|
325
|
+
},
|
|
326
|
+
{ create: false, ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {} }
|
|
327
|
+
);
|
|
328
|
+
const out = this.vault.collection(model.name);
|
|
329
|
+
const expected = /* @__PURE__ */ new Set();
|
|
330
|
+
const healthyShards = /* @__PURE__ */ new Set();
|
|
331
|
+
for (let i = 0; i < eligible.length; i++) {
|
|
332
|
+
const row = eligible[i];
|
|
333
|
+
const res = results[i];
|
|
334
|
+
if (!res || res.result === void 0) {
|
|
335
|
+
if (options.failFast && res?.error) throw res.error;
|
|
336
|
+
if (!skipped.some((s) => s.vaultId === row.vaultId)) {
|
|
337
|
+
skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
|
|
338
|
+
}
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
healthyShards.add(row.partitionKey);
|
|
342
|
+
if (model.kind === "rollup") {
|
|
343
|
+
const summary = deriveShardSummary(model, res.result.map((e) => e.record), row);
|
|
344
|
+
checkPosture(model, summary, row.partitionKey);
|
|
345
|
+
await out.put(row.partitionKey, {
|
|
346
|
+
...summary,
|
|
347
|
+
_shard: row.partitionKey,
|
|
348
|
+
_sourceVersion: row.schemaVersion
|
|
349
|
+
});
|
|
350
|
+
expected.add(row.partitionKey);
|
|
351
|
+
written++;
|
|
352
|
+
} else {
|
|
353
|
+
for (const entry of res.result) {
|
|
354
|
+
checkPosture(model, entry.record, row.partitionKey);
|
|
355
|
+
const id = `${row.partitionKey}:${entry.id}`;
|
|
356
|
+
await out.put(id, {
|
|
357
|
+
...entry.record,
|
|
358
|
+
_shard: row.partitionKey,
|
|
359
|
+
_sourceId: entry.id,
|
|
360
|
+
_sourceVersion: entry.version
|
|
361
|
+
});
|
|
362
|
+
expected.add(id);
|
|
363
|
+
written++;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
retracted += await this.reconcile(out, model, expected, healthyShards);
|
|
368
|
+
}
|
|
369
|
+
return { written, retracted, skippedVaults: skipped };
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Delete stale rows: id not in `expected` AND the row's shard is
|
|
373
|
+
* healthy this refresh (source record deleted), no longer in the
|
|
374
|
+
* group registry (shard departed), or outside the audience predicate
|
|
375
|
+
* (scope narrowed — out-of-scope data never lingers, spec § 5).
|
|
376
|
+
* Unreachable-but-registered in-scope shards are preserved.
|
|
377
|
+
*/
|
|
378
|
+
async reconcile(out, model, expected, healthyShards) {
|
|
379
|
+
await this.group.registry.list();
|
|
380
|
+
const registryRows2 = new Map(
|
|
381
|
+
this.group.registry.query().toArray().filter((r) => r.group === this.group.name).map((r) => [r.partitionKey, r])
|
|
382
|
+
);
|
|
383
|
+
await out.list();
|
|
384
|
+
const rows = out.query().toArray();
|
|
385
|
+
let retracted = 0;
|
|
386
|
+
for (const row of rows) {
|
|
387
|
+
const shard = row["_shard"];
|
|
388
|
+
if (shard === void 0) continue;
|
|
389
|
+
const id = model.kind === "rollup" ? shard : `${shard}:${String(row["_sourceId"])}`;
|
|
390
|
+
if (expected.has(id)) continue;
|
|
391
|
+
const registryRow = registryRows2.get(shard);
|
|
392
|
+
const outOfScope = registryRow !== void 0 && this.shards !== void 0 && !this.shards(registryRow);
|
|
393
|
+
if (healthyShards.has(shard) || registryRow === void 0 || outOfScope) {
|
|
394
|
+
await out.delete(id);
|
|
395
|
+
retracted++;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return retracted;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
11
404
|
// src/bundle/uint32.ts
|
|
12
405
|
function readUint32BE(bytes, offset) {
|
|
13
406
|
return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
|
|
@@ -28,12 +421,10 @@ var init_uint32 = __esm({
|
|
|
28
421
|
import { sha256Hex, generateULID } from "@noy-db/hub/cargo";
|
|
29
422
|
import {
|
|
30
423
|
writePod,
|
|
31
|
-
readPodHeader
|
|
424
|
+
readPodHeader,
|
|
425
|
+
readPodCover,
|
|
426
|
+
NOYDB_BUNDLE_MAGIC
|
|
32
427
|
} from "@noy-db/hub/pod";
|
|
33
|
-
import {
|
|
34
|
-
readNoydbBundlePublicEnvelope,
|
|
35
|
-
hasNoydbBundleMagic
|
|
36
|
-
} from "@noy-db/hub";
|
|
37
428
|
function encodeMultiBundle(manifest, inner) {
|
|
38
429
|
validateManifest(manifest);
|
|
39
430
|
if (manifest.compartments.length !== inner.length) {
|
|
@@ -59,6 +450,11 @@ function encodeMultiBundle(manifest, inner) {
|
|
|
59
450
|
}
|
|
60
451
|
return out;
|
|
61
452
|
}
|
|
453
|
+
function hasPodMagic(bytes) {
|
|
454
|
+
if (bytes.length < NOYDB_BUNDLE_MAGIC.length) return false;
|
|
455
|
+
for (let i = 0; i < NOYDB_BUNDLE_MAGIC.length; i++) if (bytes[i] !== NOYDB_BUNDLE_MAGIC[i]) return false;
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
62
458
|
function hasMultiMagic(bytes) {
|
|
63
459
|
if (bytes.length < NOYDB_MULTI_BUNDLE_MAGIC.length) return false;
|
|
64
460
|
for (let i = 0; i < NOYDB_MULTI_BUNDLE_MAGIC.length; i++) if (bytes[i] !== NOYDB_MULTI_BUNDLE_MAGIC[i]) return false;
|
|
@@ -135,7 +531,7 @@ async function writeMultiVaultBundle(compartments, opts = {}) {
|
|
|
135
531
|
);
|
|
136
532
|
}
|
|
137
533
|
if (c.disclose?.publicEnvelope === true) {
|
|
138
|
-
const env =
|
|
534
|
+
const env = readPodCover(innerBytes);
|
|
139
535
|
if (env !== void 0) entry.publicEnvelope = env;
|
|
140
536
|
}
|
|
141
537
|
const fence = await c.vault.schemaFenceState();
|
|
@@ -152,9 +548,9 @@ async function writeMultiVaultBundle(compartments, opts = {}) {
|
|
|
152
548
|
}
|
|
153
549
|
async function readNoydbBundleManifest(bytes) {
|
|
154
550
|
if (hasMultiMagic(bytes)) return [...decodeMultiBundle(bytes).manifest.compartments];
|
|
155
|
-
if (
|
|
551
|
+
if (hasPodMagic(bytes)) {
|
|
156
552
|
const header = readPodHeader(bytes);
|
|
157
|
-
const env =
|
|
553
|
+
const env = readPodCover(bytes);
|
|
158
554
|
const entry = {
|
|
159
555
|
handle: header.handle,
|
|
160
556
|
innerBytes: bytes.length,
|
|
@@ -169,7 +565,7 @@ function readMultiVaultBundleCompartment(bytes, selector) {
|
|
|
169
565
|
if (typeof selector === "number" && !Number.isInteger(selector)) {
|
|
170
566
|
throw new Error(`readMultiVaultBundleCompartment: numeric selector must be an integer, got ${selector}.`);
|
|
171
567
|
}
|
|
172
|
-
if (
|
|
568
|
+
if (hasPodMagic(bytes) && !hasMultiMagic(bytes)) {
|
|
173
569
|
const header = readPodHeader(bytes);
|
|
174
570
|
if (selector === 0 || selector === header.handle) return bytes;
|
|
175
571
|
throw new Error(`readMultiVaultBundleCompartment: single v1 bundle has only compartment "${header.handle}".`);
|
|
@@ -202,7 +598,7 @@ import {
|
|
|
202
598
|
walkClosure,
|
|
203
599
|
extractPartition,
|
|
204
600
|
describeExtraction
|
|
205
|
-
} from "@noy-db/hub/
|
|
601
|
+
} from "@noy-db/hub/cargo";
|
|
206
602
|
import { readPodHeader as readPodHeader2 } from "@noy-db/hub/pod";
|
|
207
603
|
import { sha256Hex as sha256Hex2, generateULID as generateULID2 } from "@noy-db/hub/cargo";
|
|
208
604
|
function asIdArray(v) {
|
|
@@ -430,7 +826,7 @@ var init_field_authority = __esm({
|
|
|
430
826
|
|
|
431
827
|
// src/interchange/merge-compartment.ts
|
|
432
828
|
import { diffVault } from "@noy-db/hub/cargo";
|
|
433
|
-
import { decryptExtractedPartition } from "@noy-db/hub/
|
|
829
|
+
import { decryptExtractedPartition } from "@noy-db/hub/cargo";
|
|
434
830
|
function strategyFor(opts, collection) {
|
|
435
831
|
if (typeof opts === "string") return opts;
|
|
436
832
|
return opts[collection] ?? opts.default ?? "manual-queue";
|
|
@@ -725,7 +1121,7 @@ __export(surface_exports, {
|
|
|
725
1121
|
proposeSurface: () => proposeSurface
|
|
726
1122
|
});
|
|
727
1123
|
import { generateULID as generateULID3 } from "@noy-db/hub/cargo";
|
|
728
|
-
import { extractPartition as extractPartition2 } from "@noy-db/hub/
|
|
1124
|
+
import { extractPartition as extractPartition2 } from "@noy-db/hub/cargo";
|
|
729
1125
|
async function proposeSurface(smv, def, proposedBy, now) {
|
|
730
1126
|
const row = {
|
|
731
1127
|
...def,
|
|
@@ -1203,85 +1599,6 @@ var init_cross_vault_live = __esm({
|
|
|
1203
1599
|
}
|
|
1204
1600
|
});
|
|
1205
1601
|
|
|
1206
|
-
// src/federation/insight-auto-push.ts
|
|
1207
|
-
var InsightAutoPush;
|
|
1208
|
-
var init_insight_auto_push = __esm({
|
|
1209
|
-
"src/federation/insight-auto-push.ts"() {
|
|
1210
|
-
"use strict";
|
|
1211
|
-
InsightAutoPush = class {
|
|
1212
|
-
constructor(recompute, isSource, onError = (err, pk) => console.warn(`[klum-db] insight auto-push failed for shard "${pk}":`, err), debounceMs) {
|
|
1213
|
-
this.recompute = recompute;
|
|
1214
|
-
this.isSource = isSource;
|
|
1215
|
-
this.onError = onError;
|
|
1216
|
-
this.debounceMs = debounceMs;
|
|
1217
|
-
}
|
|
1218
|
-
recompute;
|
|
1219
|
-
isSource;
|
|
1220
|
-
onError;
|
|
1221
|
-
debounceMs;
|
|
1222
|
-
dirty = /* @__PURE__ */ new Set();
|
|
1223
|
-
pending = null;
|
|
1224
|
-
timer = null;
|
|
1225
|
-
resolvePending = null;
|
|
1226
|
-
flushing = false;
|
|
1227
|
-
/** Called from a shard's onAfterWrite hook. Marks the shard dirty + schedules a flush. */
|
|
1228
|
-
noteWrite(partitionKey, collection) {
|
|
1229
|
-
if (!this.isSource(collection)) return;
|
|
1230
|
-
this.dirty.add(partitionKey);
|
|
1231
|
-
if (this.debounceMs !== void 0) this.scheduleDebounced();
|
|
1232
|
-
else this.schedule();
|
|
1233
|
-
}
|
|
1234
|
-
/** Resolve once no flush is pending (drains rescheduled flushes too). */
|
|
1235
|
-
async whenSettled() {
|
|
1236
|
-
while (this.pending) await this.pending;
|
|
1237
|
-
}
|
|
1238
|
-
// ── microtask path (unchanged default) ──
|
|
1239
|
-
schedule() {
|
|
1240
|
-
if (this.pending) return;
|
|
1241
|
-
this.pending = this.runFlush().finally(() => {
|
|
1242
|
-
this.pending = null;
|
|
1243
|
-
if (this.dirty.size > 0) this.schedule();
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
// ── debounce path (#13) ──
|
|
1247
|
-
scheduleDebounced() {
|
|
1248
|
-
if (!this.pending) this.pending = new Promise((r) => {
|
|
1249
|
-
this.resolvePending = r;
|
|
1250
|
-
});
|
|
1251
|
-
if (this.timer) clearTimeout(this.timer);
|
|
1252
|
-
this.timer = setTimeout(() => {
|
|
1253
|
-
this.timer = null;
|
|
1254
|
-
if (this.flushing) return;
|
|
1255
|
-
this.flushing = true;
|
|
1256
|
-
void this.runFlush().finally(() => {
|
|
1257
|
-
this.flushing = false;
|
|
1258
|
-
if (this.dirty.size > 0) {
|
|
1259
|
-
this.scheduleDebounced();
|
|
1260
|
-
} else {
|
|
1261
|
-
const resolve = this.resolvePending;
|
|
1262
|
-
this.resolvePending = null;
|
|
1263
|
-
this.pending = null;
|
|
1264
|
-
resolve?.();
|
|
1265
|
-
}
|
|
1266
|
-
});
|
|
1267
|
-
}, this.debounceMs);
|
|
1268
|
-
}
|
|
1269
|
-
async runFlush() {
|
|
1270
|
-
await Promise.resolve();
|
|
1271
|
-
const pks = [...this.dirty];
|
|
1272
|
-
this.dirty.clear();
|
|
1273
|
-
for (const pk of pks) {
|
|
1274
|
-
try {
|
|
1275
|
-
await this.recompute(pk);
|
|
1276
|
-
} catch (err) {
|
|
1277
|
-
this.onError(err, pk);
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
});
|
|
1284
|
-
|
|
1285
1602
|
// src/federation/partial-reduce.ts
|
|
1286
1603
|
function canPartialReduce(spec) {
|
|
1287
1604
|
return Object.values(spec).every((r) => typeof r.merge === "function");
|
|
@@ -1505,25 +1822,25 @@ __export(vault_group_exports, {
|
|
|
1505
1822
|
ShardedQuery: () => ShardedQuery,
|
|
1506
1823
|
VaultGroup: () => VaultGroup
|
|
1507
1824
|
});
|
|
1508
|
-
import { CrossShardJoinError, DataResidencyError, ReservedVaultNameError, ShardProvisioningError, UnknownShardError, ValidationError } from "@noy-db/hub/cargo";
|
|
1825
|
+
import { CrossShardJoinError, DataResidencyError, ReservedVaultNameError, ShardProvisioningError, UnknownShardError, ValidationError as ValidationError2 } from "@noy-db/hub/cargo";
|
|
1509
1826
|
function autoPushConfig(spec) {
|
|
1510
1827
|
if (!spec.autoPush) return null;
|
|
1511
1828
|
return spec.autoPush === true ? {} : spec.autoPush;
|
|
1512
1829
|
}
|
|
1513
1830
|
function assertSafePartitionKey(partitionKey) {
|
|
1514
1831
|
if (partitionKey.length === 0) {
|
|
1515
|
-
throw new
|
|
1832
|
+
throw new ValidationError2("partitionKey must be a non-empty string");
|
|
1516
1833
|
}
|
|
1517
1834
|
if (partitionKey === STATE_VAULT_NAME) {
|
|
1518
1835
|
throw new ReservedVaultNameError(partitionKey);
|
|
1519
1836
|
}
|
|
1520
1837
|
if (!SAFE_PARTITION_KEY.test(partitionKey)) {
|
|
1521
|
-
throw new
|
|
1838
|
+
throw new ValidationError2(
|
|
1522
1839
|
`partitionKey "${partitionKey}" contains characters outside [A-Za-z0-9._-]. Map your records to a store-safe key in sharding.keyOf.`
|
|
1523
1840
|
);
|
|
1524
1841
|
}
|
|
1525
1842
|
if (partitionKey.includes(SHARD_SEPARATOR)) {
|
|
1526
|
-
throw new
|
|
1843
|
+
throw new ValidationError2(
|
|
1527
1844
|
`partitionKey "${partitionKey}" must not contain "--" \u2014 it is reserved as the shard vault-id separator and would risk shard-id collisions.`
|
|
1528
1845
|
);
|
|
1529
1846
|
}
|
|
@@ -1535,6 +1852,7 @@ var init_vault_group = __esm({
|
|
|
1535
1852
|
init_state_vault();
|
|
1536
1853
|
init_constants();
|
|
1537
1854
|
init_classify_skip();
|
|
1855
|
+
init_read_model();
|
|
1538
1856
|
init_cross_shard_join();
|
|
1539
1857
|
init_cross_vault_live();
|
|
1540
1858
|
init_insight_auto_push();
|
|
@@ -1553,7 +1871,7 @@ var init_vault_group = __esm({
|
|
|
1553
1871
|
this.cutoverOnOpen = cutoverOnOpen;
|
|
1554
1872
|
this.meta = meta;
|
|
1555
1873
|
if (name.includes(SHARD_SEPARATOR)) {
|
|
1556
|
-
throw new
|
|
1874
|
+
throw new ValidationError2(
|
|
1557
1875
|
`VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
|
|
1558
1876
|
);
|
|
1559
1877
|
}
|
|
@@ -1767,7 +2085,7 @@ var init_vault_group = __esm({
|
|
|
1767
2085
|
withCrossVaultDerivation(spec) {
|
|
1768
2086
|
const target = spec.target.vault;
|
|
1769
2087
|
if (target === this.name || target.startsWith(`${this.name}${SHARD_SEPARATOR}`)) {
|
|
1770
|
-
throw new
|
|
2088
|
+
throw new ValidationError2(
|
|
1771
2089
|
`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.`
|
|
1772
2090
|
);
|
|
1773
2091
|
}
|
|
@@ -1827,12 +2145,7 @@ var init_vault_group = __esm({
|
|
|
1827
2145
|
skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
|
|
1828
2146
|
continue;
|
|
1829
2147
|
}
|
|
1830
|
-
const
|
|
1831
|
-
vaultId: row.vaultId,
|
|
1832
|
-
partitionKey: row.partitionKey,
|
|
1833
|
-
schemaVersion: row.schemaVersion
|
|
1834
|
-
};
|
|
1835
|
-
const summary = spec.derive(res.result, ctx);
|
|
2148
|
+
const summary = deriveShardSummary(spec, res.result, row);
|
|
1836
2149
|
await out.put(row.partitionKey, summary);
|
|
1837
2150
|
written++;
|
|
1838
2151
|
}
|
|
@@ -1852,17 +2165,12 @@ var init_vault_group = __esm({
|
|
|
1852
2165
|
const row = await this.registry.get(this.registryId(partitionKey));
|
|
1853
2166
|
if (!row) return;
|
|
1854
2167
|
const shard = await this.openShard(partitionKey);
|
|
1855
|
-
const ctx = {
|
|
1856
|
-
vaultId: row.vaultId,
|
|
1857
|
-
partitionKey,
|
|
1858
|
-
schemaVersion: row.schemaVersion
|
|
1859
|
-
};
|
|
1860
2168
|
for (const spec of this.crossVaultDerivations) {
|
|
1861
2169
|
if (!spec.autoPush) continue;
|
|
1862
2170
|
const min = autoPushConfig(spec)?.minVersion;
|
|
1863
2171
|
if (min !== void 0 && row.schemaVersion < min) continue;
|
|
1864
2172
|
const records = await shard.collection(spec.source).list();
|
|
1865
|
-
const summary = spec
|
|
2173
|
+
const summary = deriveShardSummary(spec, records, row);
|
|
1866
2174
|
const insight = await this.db.openVault(spec.target.vault);
|
|
1867
2175
|
await insight.collection(spec.target.collection).put(partitionKey, summary);
|
|
1868
2176
|
}
|
|
@@ -2221,8 +2529,111 @@ var init_vault_group = __esm({
|
|
|
2221
2529
|
}
|
|
2222
2530
|
});
|
|
2223
2531
|
|
|
2532
|
+
// src/portal/provision.ts
|
|
2533
|
+
var provision_exports = {};
|
|
2534
|
+
__export(provision_exports, {
|
|
2535
|
+
provisionPortal: () => provisionPortal,
|
|
2536
|
+
revokePortalInvite: () => revokePortalInvite
|
|
2537
|
+
});
|
|
2538
|
+
import { generateULID as generateULID5 } from "@noy-db/hub/cargo";
|
|
2539
|
+
async function requireRow(group, client) {
|
|
2540
|
+
const row = await group.registry.get(group.registryId(client));
|
|
2541
|
+
if (!row) throw new Error(`provisionPortal: client "${client}" has no registry row in group.`);
|
|
2542
|
+
return row;
|
|
2543
|
+
}
|
|
2544
|
+
async function provisionPortal(db, group, opts) {
|
|
2545
|
+
const existing = await group.registry.get(group.registryId(opts.client));
|
|
2546
|
+
if (!existing) await group.createShard(opts.client);
|
|
2547
|
+
const row = existing ?? await requireRow(group, opts.client);
|
|
2548
|
+
const vaultId = row.vaultId;
|
|
2549
|
+
for (const g of opts.firmGrants ?? []) {
|
|
2550
|
+
await db.grant(vaultId, {
|
|
2551
|
+
userId: g.userId,
|
|
2552
|
+
displayName: g.displayName,
|
|
2553
|
+
role: g.role,
|
|
2554
|
+
passphrase: g.passphrase
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
const { issueInvite, issuePeerRecovery } = await import("@noy-db/on-magic-link");
|
|
2558
|
+
const mode = opts.mode ?? "invite";
|
|
2559
|
+
const issued = mode === "rebind" ? await issuePeerRecovery(db, vaultId, {
|
|
2560
|
+
userId: opts.invite.userId,
|
|
2561
|
+
displayName: opts.invite.displayName,
|
|
2562
|
+
...opts.invite.role !== void 0 ? { role: opts.invite.role } : {},
|
|
2563
|
+
...opts.invite.ttlMs !== void 0 ? { ttlMs: opts.invite.ttlMs } : {},
|
|
2564
|
+
...opts.invite.tempPhrase !== void 0 ? { tempPhrase: opts.invite.tempPhrase } : {}
|
|
2565
|
+
}) : await issueInvite(db, vaultId, {
|
|
2566
|
+
userId: opts.invite.userId,
|
|
2567
|
+
displayName: opts.invite.displayName,
|
|
2568
|
+
role: opts.invite.role ?? "client",
|
|
2569
|
+
...opts.invite.ttlMs !== void 0 ? { ttlMs: opts.invite.ttlMs } : {},
|
|
2570
|
+
...opts.invite.tempPhrase !== void 0 ? { tempPhrase: opts.invite.tempPhrase } : {}
|
|
2571
|
+
});
|
|
2572
|
+
const auditRef = {
|
|
2573
|
+
tokenId: issued.payload.tokenId,
|
|
2574
|
+
userId: opts.invite.userId,
|
|
2575
|
+
kind: mode,
|
|
2576
|
+
issuedAt: Date.now(),
|
|
2577
|
+
expiresAt: issued.payload.expiresAt
|
|
2578
|
+
};
|
|
2579
|
+
const handle = row.handle ?? generateULID5();
|
|
2580
|
+
const updated = {
|
|
2581
|
+
...row,
|
|
2582
|
+
handle,
|
|
2583
|
+
portal: {
|
|
2584
|
+
enabledAt: row.portal?.enabledAt ?? Date.now(),
|
|
2585
|
+
invites: [...row.portal?.invites ?? [], auditRef]
|
|
2586
|
+
}
|
|
2587
|
+
};
|
|
2588
|
+
await group.registry.put(group.registryId(opts.client), updated);
|
|
2589
|
+
return {
|
|
2590
|
+
vaultId,
|
|
2591
|
+
handle,
|
|
2592
|
+
invite: {
|
|
2593
|
+
encoded: issued.encoded,
|
|
2594
|
+
tokenId: issued.payload.tokenId,
|
|
2595
|
+
userId: opts.invite.userId,
|
|
2596
|
+
expiresAt: issued.payload.expiresAt
|
|
2597
|
+
},
|
|
2598
|
+
row: updated
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
async function revokePortalInvite(db, group, opts) {
|
|
2602
|
+
const row = await requireRow(group, opts.client);
|
|
2603
|
+
const invites = row.portal?.invites ?? [];
|
|
2604
|
+
const ref = invites.find((i) => i.tokenId === opts.tokenId);
|
|
2605
|
+
if (!ref) {
|
|
2606
|
+
throw new Error(`revokePortalInvite: no invite with tokenId "${opts.tokenId}" for client "${opts.client}".`);
|
|
2607
|
+
}
|
|
2608
|
+
if (ref.revokedAt !== void 0) return row;
|
|
2609
|
+
const { revokeInvite } = await import("@noy-db/on-magic-link");
|
|
2610
|
+
await revokeInvite(db, row.vaultId, {
|
|
2611
|
+
tokenId: ref.tokenId,
|
|
2612
|
+
vault: row.vaultId,
|
|
2613
|
+
userId: ref.userId,
|
|
2614
|
+
kind: ref.kind === "rebind" ? "peer-recovery" : "invite",
|
|
2615
|
+
issuer: "",
|
|
2616
|
+
tempPhrase: "",
|
|
2617
|
+
expiresAt: ref.expiresAt
|
|
2618
|
+
});
|
|
2619
|
+
const updated = {
|
|
2620
|
+
...row,
|
|
2621
|
+
portal: {
|
|
2622
|
+
enabledAt: row.portal?.enabledAt ?? Date.now(),
|
|
2623
|
+
invites: invites.map((i) => i.tokenId === opts.tokenId ? { ...i, revokedAt: Date.now() } : i)
|
|
2624
|
+
}
|
|
2625
|
+
};
|
|
2626
|
+
await group.registry.put(group.registryId(opts.client), updated);
|
|
2627
|
+
return updated;
|
|
2628
|
+
}
|
|
2629
|
+
var init_provision = __esm({
|
|
2630
|
+
"src/portal/provision.ts"() {
|
|
2631
|
+
"use strict";
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2224
2635
|
// src/index.ts
|
|
2225
|
-
import { ValidationError as
|
|
2636
|
+
import { ValidationError as ValidationError3, ReservedVaultNameError as ReservedVaultNameError2, VaultTemplateNotFoundError, STATE_VAULT_NAME as STATE_VAULT_NAME2 } from "@noy-db/hub/cargo";
|
|
2226
2637
|
|
|
2227
2638
|
// src/dock/docked-unit.ts
|
|
2228
2639
|
var DockedUnit = class {
|
|
@@ -2241,6 +2652,10 @@ var DockedUnit = class {
|
|
|
2241
2652
|
}
|
|
2242
2653
|
};
|
|
2243
2654
|
|
|
2655
|
+
// src/index.ts
|
|
2656
|
+
init_share_link();
|
|
2657
|
+
init_read_model();
|
|
2658
|
+
|
|
2244
2659
|
// src/federation/group-inspector.ts
|
|
2245
2660
|
function groupInspector(group) {
|
|
2246
2661
|
let shardIds = /* @__PURE__ */ new Set();
|
|
@@ -2320,7 +2735,7 @@ import {
|
|
|
2320
2735
|
init_merge_compartment();
|
|
2321
2736
|
init_stage_records();
|
|
2322
2737
|
init_stage_records();
|
|
2323
|
-
import { decryptExtractedPartition as decryptExtractedPartition2 } from "@noy-db/hub/
|
|
2738
|
+
import { decryptExtractedPartition as decryptExtractedPartition2 } from "@noy-db/hub/cargo";
|
|
2324
2739
|
var MinVersionError = class extends Error {
|
|
2325
2740
|
constructor(fromVersion, toVersion) {
|
|
2326
2741
|
super(
|
|
@@ -2399,7 +2814,7 @@ var Lobby = class {
|
|
|
2399
2814
|
}
|
|
2400
2815
|
async openVaultGroup(name, opts) {
|
|
2401
2816
|
const db = this.noydb;
|
|
2402
|
-
if (db.isClosed) throw new
|
|
2817
|
+
if (db.isClosed) throw new ValidationError3("Instance is closed");
|
|
2403
2818
|
if (name === STATE_VAULT_NAME2) throw new ReservedVaultNameError2(name);
|
|
2404
2819
|
const template = this.vaultTemplates.get(opts.sharding.vaultTemplate);
|
|
2405
2820
|
if (!template) throw new VaultTemplateNotFoundError(opts.sharding.vaultTemplate);
|
|
@@ -2421,7 +2836,7 @@ var Lobby = class {
|
|
|
2421
2836
|
}
|
|
2422
2837
|
async openStateManagementVault() {
|
|
2423
2838
|
const db = this.noydb;
|
|
2424
|
-
if (db.isClosed) throw new
|
|
2839
|
+
if (db.isClosed) throw new ValidationError3("Instance is closed");
|
|
2425
2840
|
const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
|
|
2426
2841
|
return StateManagementVault2.open(db);
|
|
2427
2842
|
}
|
|
@@ -2441,6 +2856,64 @@ var Lobby = class {
|
|
|
2441
2856
|
const { graduate: graduateFn } = await Promise.resolve().then(() => (init_graduate(), graduate_exports));
|
|
2442
2857
|
return graduateFn(this.noydb, docked, opts);
|
|
2443
2858
|
}
|
|
2859
|
+
// ─── Portal deep-link addressing (#43) ────────────────────────────────────
|
|
2860
|
+
/**
|
|
2861
|
+
* Resolve a portal share link (raw URL/path or a parsed `ShareLink`)
|
|
2862
|
+
* against the fleet registry: `vaultHandle → registry row` plus a
|
|
2863
|
+
* console-route descriptor the firm app turns into its own URL.
|
|
2864
|
+
* Unknown/foreign handles fail closed with `ShareLinkResolutionError`.
|
|
2865
|
+
* Delegates to `resolveShareLinkAgainstRegistry` in `portal/share-link.ts`.
|
|
2866
|
+
*/
|
|
2867
|
+
async resolveShareLink(link, opts) {
|
|
2868
|
+
const { resolveShareLinkAgainstRegistry: resolveShareLinkAgainstRegistry2 } = await Promise.resolve().then(() => (init_share_link(), share_link_exports));
|
|
2869
|
+
return resolveShareLinkAgainstRegistry2(link, opts.registry);
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* Mint a portal share link for a fleet client (`client` partition key +
|
|
2873
|
+
* `group`), delegating the link grammar to `@noy-db/hub/share-link`.
|
|
2874
|
+
* Fails closed when the client is unknown or has no portal handle.
|
|
2875
|
+
* Delegates to `buildShareLinkForClient` in `portal/share-link.ts`.
|
|
2876
|
+
*/
|
|
2877
|
+
async buildShareLink(parts, opts) {
|
|
2878
|
+
const { buildShareLinkForClient: buildShareLinkForClient2 } = await Promise.resolve().then(() => (init_share_link(), share_link_exports));
|
|
2879
|
+
return buildShareLinkForClient2(parts, opts.registry);
|
|
2880
|
+
}
|
|
2881
|
+
// ─── Portal provisioning (#42) ────────────────────────────────────────────
|
|
2882
|
+
/**
|
|
2883
|
+
* Provision (or re-invite, `mode: 'rebind'`) a client portal vault in
|
|
2884
|
+
* one audited operation: ensure the shard, assign the portal handle
|
|
2885
|
+
* (#43's link target), apply firm grants, issue the magic-link invite,
|
|
2886
|
+
* stamp the audit trail on the fleet registry row.
|
|
2887
|
+
* Needs `@noy-db/on-magic-link` (optional peer) and a session created
|
|
2888
|
+
* with `teamStrategy: withTeam()`.
|
|
2889
|
+
* Delegates to `provisionPortal` in `portal/provision.ts`.
|
|
2890
|
+
*/
|
|
2891
|
+
async provisionPortal(group, opts) {
|
|
2892
|
+
const { provisionPortal: provisionPortalFn } = await Promise.resolve().then(() => (init_provision(), provision_exports));
|
|
2893
|
+
return provisionPortalFn(this.noydb, group, opts);
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Revoke an outstanding portal invite and stamp `revokedAt` on the
|
|
2897
|
+
* fleet audit trail. Idempotent; fails closed on an unknown tokenId.
|
|
2898
|
+
* Delegates to `revokePortalInvite` in `portal/provision.ts`.
|
|
2899
|
+
*/
|
|
2900
|
+
async revokePortalInvite(group, opts) {
|
|
2901
|
+
const { revokePortalInvite: revokePortalInviteFn } = await Promise.resolve().then(() => (init_provision(), provision_exports));
|
|
2902
|
+
return revokePortalInviteFn(this.noydb, group, opts);
|
|
2903
|
+
}
|
|
2904
|
+
// ─── Federated read-model (#44) ───────────────────────────────────────────
|
|
2905
|
+
/**
|
|
2906
|
+
* Open a maintained federated read-model over `group` (#44 S1): rollup
|
|
2907
|
+
* models reduce each shard's `source` into one summary row per shard in
|
|
2908
|
+
* the read-model vault — deterministic ids, `_shard`/`_sourceVersion`
|
|
2909
|
+
* provenance, fail-closed `posture.surface` allowlist, explicit
|
|
2910
|
+
* `refresh()`. Spec: docs/federated-read-model.md.
|
|
2911
|
+
* Delegates to `openReadModel` in `federation/read-model.ts`.
|
|
2912
|
+
*/
|
|
2913
|
+
async openReadModel(group, opts) {
|
|
2914
|
+
const { openReadModel: openReadModelFn } = await Promise.resolve().then(() => (init_read_model(), read_model_exports));
|
|
2915
|
+
return openReadModelFn(group, opts);
|
|
2916
|
+
}
|
|
2444
2917
|
// ─── FR-7 Surface API ─────────────────────────────────────────────────────
|
|
2445
2918
|
/**
|
|
2446
2919
|
* Export a scoped partition from `vaultName` bounded to the given surface.
|
|
@@ -2522,8 +2995,10 @@ export {
|
|
|
2522
2995
|
NOYDB_MULTI_BUNDLE_MAGIC,
|
|
2523
2996
|
NOYDB_MULTI_BUNDLE_PREFIX_BYTES,
|
|
2524
2997
|
NOYDB_MULTI_BUNDLE_VERSION,
|
|
2998
|
+
PostureViolationError,
|
|
2525
2999
|
ReservedVaultNameError3 as ReservedVaultNameError,
|
|
2526
3000
|
ShardProvisioningError2 as ShardProvisioningError,
|
|
3001
|
+
ShareLinkResolutionError,
|
|
2527
3002
|
SurfaceCadenceScheduler,
|
|
2528
3003
|
SurfaceNotFoundError,
|
|
2529
3004
|
SurfaceStateError,
|