@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/README.md +1 -1
- package/dist/bin/klum.d.cts +1 -2
- package/dist/bin/klum.d.ts +1 -2
- package/dist/index.cjs +653 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +322 -5
- package/dist/index.d.ts +322 -5
- package/dist/index.js +576 -104
- 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 +14 -9
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;
|
|
@@ -205,7 +598,7 @@ import {
|
|
|
205
598
|
walkClosure,
|
|
206
599
|
extractPartition,
|
|
207
600
|
describeExtraction
|
|
208
|
-
} from "@noy-db/hub/
|
|
601
|
+
} from "@noy-db/hub/cargo";
|
|
209
602
|
import { readPodHeader as readPodHeader2 } from "@noy-db/hub/pod";
|
|
210
603
|
import { sha256Hex as sha256Hex2, generateULID as generateULID2 } from "@noy-db/hub/cargo";
|
|
211
604
|
function asIdArray(v) {
|
|
@@ -433,7 +826,7 @@ var init_field_authority = __esm({
|
|
|
433
826
|
|
|
434
827
|
// src/interchange/merge-compartment.ts
|
|
435
828
|
import { diffVault } from "@noy-db/hub/cargo";
|
|
436
|
-
import { decryptExtractedPartition } from "@noy-db/hub/
|
|
829
|
+
import { decryptExtractedPartition } from "@noy-db/hub/cargo";
|
|
437
830
|
function strategyFor(opts, collection) {
|
|
438
831
|
if (typeof opts === "string") return opts;
|
|
439
832
|
return opts[collection] ?? opts.default ?? "manual-queue";
|
|
@@ -728,7 +1121,7 @@ __export(surface_exports, {
|
|
|
728
1121
|
proposeSurface: () => proposeSurface
|
|
729
1122
|
});
|
|
730
1123
|
import { generateULID as generateULID3 } from "@noy-db/hub/cargo";
|
|
731
|
-
import { extractPartition as extractPartition2 } from "@noy-db/hub/
|
|
1124
|
+
import { extractPartition as extractPartition2 } from "@noy-db/hub/cargo";
|
|
732
1125
|
async function proposeSurface(smv, def, proposedBy, now) {
|
|
733
1126
|
const row = {
|
|
734
1127
|
...def,
|
|
@@ -1206,85 +1599,6 @@ var init_cross_vault_live = __esm({
|
|
|
1206
1599
|
}
|
|
1207
1600
|
});
|
|
1208
1601
|
|
|
1209
|
-
// src/federation/insight-auto-push.ts
|
|
1210
|
-
var InsightAutoPush;
|
|
1211
|
-
var init_insight_auto_push = __esm({
|
|
1212
|
-
"src/federation/insight-auto-push.ts"() {
|
|
1213
|
-
"use strict";
|
|
1214
|
-
InsightAutoPush = class {
|
|
1215
|
-
constructor(recompute, isSource, onError = (err, pk) => console.warn(`[klum-db] insight auto-push failed for shard "${pk}":`, err), debounceMs) {
|
|
1216
|
-
this.recompute = recompute;
|
|
1217
|
-
this.isSource = isSource;
|
|
1218
|
-
this.onError = onError;
|
|
1219
|
-
this.debounceMs = debounceMs;
|
|
1220
|
-
}
|
|
1221
|
-
recompute;
|
|
1222
|
-
isSource;
|
|
1223
|
-
onError;
|
|
1224
|
-
debounceMs;
|
|
1225
|
-
dirty = /* @__PURE__ */ new Set();
|
|
1226
|
-
pending = null;
|
|
1227
|
-
timer = null;
|
|
1228
|
-
resolvePending = null;
|
|
1229
|
-
flushing = false;
|
|
1230
|
-
/** Called from a shard's onAfterWrite hook. Marks the shard dirty + schedules a flush. */
|
|
1231
|
-
noteWrite(partitionKey, collection) {
|
|
1232
|
-
if (!this.isSource(collection)) return;
|
|
1233
|
-
this.dirty.add(partitionKey);
|
|
1234
|
-
if (this.debounceMs !== void 0) this.scheduleDebounced();
|
|
1235
|
-
else this.schedule();
|
|
1236
|
-
}
|
|
1237
|
-
/** Resolve once no flush is pending (drains rescheduled flushes too). */
|
|
1238
|
-
async whenSettled() {
|
|
1239
|
-
while (this.pending) await this.pending;
|
|
1240
|
-
}
|
|
1241
|
-
// ── microtask path (unchanged default) ──
|
|
1242
|
-
schedule() {
|
|
1243
|
-
if (this.pending) return;
|
|
1244
|
-
this.pending = this.runFlush().finally(() => {
|
|
1245
|
-
this.pending = null;
|
|
1246
|
-
if (this.dirty.size > 0) this.schedule();
|
|
1247
|
-
});
|
|
1248
|
-
}
|
|
1249
|
-
// ── debounce path (#13) ──
|
|
1250
|
-
scheduleDebounced() {
|
|
1251
|
-
if (!this.pending) this.pending = new Promise((r) => {
|
|
1252
|
-
this.resolvePending = r;
|
|
1253
|
-
});
|
|
1254
|
-
if (this.timer) clearTimeout(this.timer);
|
|
1255
|
-
this.timer = setTimeout(() => {
|
|
1256
|
-
this.timer = null;
|
|
1257
|
-
if (this.flushing) return;
|
|
1258
|
-
this.flushing = true;
|
|
1259
|
-
void this.runFlush().finally(() => {
|
|
1260
|
-
this.flushing = false;
|
|
1261
|
-
if (this.dirty.size > 0) {
|
|
1262
|
-
this.scheduleDebounced();
|
|
1263
|
-
} else {
|
|
1264
|
-
const resolve = this.resolvePending;
|
|
1265
|
-
this.resolvePending = null;
|
|
1266
|
-
this.pending = null;
|
|
1267
|
-
resolve?.();
|
|
1268
|
-
}
|
|
1269
|
-
});
|
|
1270
|
-
}, this.debounceMs);
|
|
1271
|
-
}
|
|
1272
|
-
async runFlush() {
|
|
1273
|
-
await Promise.resolve();
|
|
1274
|
-
const pks = [...this.dirty];
|
|
1275
|
-
this.dirty.clear();
|
|
1276
|
-
for (const pk of pks) {
|
|
1277
|
-
try {
|
|
1278
|
-
await this.recompute(pk);
|
|
1279
|
-
} catch (err) {
|
|
1280
|
-
this.onError(err, pk);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
});
|
|
1287
|
-
|
|
1288
1602
|
// src/federation/partial-reduce.ts
|
|
1289
1603
|
function canPartialReduce(spec) {
|
|
1290
1604
|
return Object.values(spec).every((r) => typeof r.merge === "function");
|
|
@@ -1508,25 +1822,25 @@ __export(vault_group_exports, {
|
|
|
1508
1822
|
ShardedQuery: () => ShardedQuery,
|
|
1509
1823
|
VaultGroup: () => VaultGroup
|
|
1510
1824
|
});
|
|
1511
|
-
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";
|
|
1512
1826
|
function autoPushConfig(spec) {
|
|
1513
1827
|
if (!spec.autoPush) return null;
|
|
1514
1828
|
return spec.autoPush === true ? {} : spec.autoPush;
|
|
1515
1829
|
}
|
|
1516
1830
|
function assertSafePartitionKey(partitionKey) {
|
|
1517
1831
|
if (partitionKey.length === 0) {
|
|
1518
|
-
throw new
|
|
1832
|
+
throw new ValidationError2("partitionKey must be a non-empty string");
|
|
1519
1833
|
}
|
|
1520
1834
|
if (partitionKey === STATE_VAULT_NAME) {
|
|
1521
1835
|
throw new ReservedVaultNameError(partitionKey);
|
|
1522
1836
|
}
|
|
1523
1837
|
if (!SAFE_PARTITION_KEY.test(partitionKey)) {
|
|
1524
|
-
throw new
|
|
1838
|
+
throw new ValidationError2(
|
|
1525
1839
|
`partitionKey "${partitionKey}" contains characters outside [A-Za-z0-9._-]. Map your records to a store-safe key in sharding.keyOf.`
|
|
1526
1840
|
);
|
|
1527
1841
|
}
|
|
1528
1842
|
if (partitionKey.includes(SHARD_SEPARATOR)) {
|
|
1529
|
-
throw new
|
|
1843
|
+
throw new ValidationError2(
|
|
1530
1844
|
`partitionKey "${partitionKey}" must not contain "--" \u2014 it is reserved as the shard vault-id separator and would risk shard-id collisions.`
|
|
1531
1845
|
);
|
|
1532
1846
|
}
|
|
@@ -1538,6 +1852,7 @@ var init_vault_group = __esm({
|
|
|
1538
1852
|
init_state_vault();
|
|
1539
1853
|
init_constants();
|
|
1540
1854
|
init_classify_skip();
|
|
1855
|
+
init_read_model();
|
|
1541
1856
|
init_cross_shard_join();
|
|
1542
1857
|
init_cross_vault_live();
|
|
1543
1858
|
init_insight_auto_push();
|
|
@@ -1556,7 +1871,7 @@ var init_vault_group = __esm({
|
|
|
1556
1871
|
this.cutoverOnOpen = cutoverOnOpen;
|
|
1557
1872
|
this.meta = meta;
|
|
1558
1873
|
if (name.includes(SHARD_SEPARATOR)) {
|
|
1559
|
-
throw new
|
|
1874
|
+
throw new ValidationError2(
|
|
1560
1875
|
`VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
|
|
1561
1876
|
);
|
|
1562
1877
|
}
|
|
@@ -1770,7 +2085,7 @@ var init_vault_group = __esm({
|
|
|
1770
2085
|
withCrossVaultDerivation(spec) {
|
|
1771
2086
|
const target = spec.target.vault;
|
|
1772
2087
|
if (target === this.name || target.startsWith(`${this.name}${SHARD_SEPARATOR}`)) {
|
|
1773
|
-
throw new
|
|
2088
|
+
throw new ValidationError2(
|
|
1774
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.`
|
|
1775
2090
|
);
|
|
1776
2091
|
}
|
|
@@ -1830,12 +2145,7 @@ var init_vault_group = __esm({
|
|
|
1830
2145
|
skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
|
|
1831
2146
|
continue;
|
|
1832
2147
|
}
|
|
1833
|
-
const
|
|
1834
|
-
vaultId: row.vaultId,
|
|
1835
|
-
partitionKey: row.partitionKey,
|
|
1836
|
-
schemaVersion: row.schemaVersion
|
|
1837
|
-
};
|
|
1838
|
-
const summary = spec.derive(res.result, ctx);
|
|
2148
|
+
const summary = deriveShardSummary(spec, res.result, row);
|
|
1839
2149
|
await out.put(row.partitionKey, summary);
|
|
1840
2150
|
written++;
|
|
1841
2151
|
}
|
|
@@ -1855,17 +2165,12 @@ var init_vault_group = __esm({
|
|
|
1855
2165
|
const row = await this.registry.get(this.registryId(partitionKey));
|
|
1856
2166
|
if (!row) return;
|
|
1857
2167
|
const shard = await this.openShard(partitionKey);
|
|
1858
|
-
const ctx = {
|
|
1859
|
-
vaultId: row.vaultId,
|
|
1860
|
-
partitionKey,
|
|
1861
|
-
schemaVersion: row.schemaVersion
|
|
1862
|
-
};
|
|
1863
2168
|
for (const spec of this.crossVaultDerivations) {
|
|
1864
2169
|
if (!spec.autoPush) continue;
|
|
1865
2170
|
const min = autoPushConfig(spec)?.minVersion;
|
|
1866
2171
|
if (min !== void 0 && row.schemaVersion < min) continue;
|
|
1867
2172
|
const records = await shard.collection(spec.source).list();
|
|
1868
|
-
const summary = spec
|
|
2173
|
+
const summary = deriveShardSummary(spec, records, row);
|
|
1869
2174
|
const insight = await this.db.openVault(spec.target.vault);
|
|
1870
2175
|
await insight.collection(spec.target.collection).put(partitionKey, summary);
|
|
1871
2176
|
}
|
|
@@ -2224,8 +2529,111 @@ var init_vault_group = __esm({
|
|
|
2224
2529
|
}
|
|
2225
2530
|
});
|
|
2226
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
|
+
|
|
2227
2635
|
// src/index.ts
|
|
2228
|
-
import { ValidationError as
|
|
2636
|
+
import { ValidationError as ValidationError3, ReservedVaultNameError as ReservedVaultNameError2, VaultTemplateNotFoundError, STATE_VAULT_NAME as STATE_VAULT_NAME2 } from "@noy-db/hub/cargo";
|
|
2229
2637
|
|
|
2230
2638
|
// src/dock/docked-unit.ts
|
|
2231
2639
|
var DockedUnit = class {
|
|
@@ -2244,6 +2652,10 @@ var DockedUnit = class {
|
|
|
2244
2652
|
}
|
|
2245
2653
|
};
|
|
2246
2654
|
|
|
2655
|
+
// src/index.ts
|
|
2656
|
+
init_share_link();
|
|
2657
|
+
init_read_model();
|
|
2658
|
+
|
|
2247
2659
|
// src/federation/group-inspector.ts
|
|
2248
2660
|
function groupInspector(group) {
|
|
2249
2661
|
let shardIds = /* @__PURE__ */ new Set();
|
|
@@ -2323,7 +2735,7 @@ import {
|
|
|
2323
2735
|
init_merge_compartment();
|
|
2324
2736
|
init_stage_records();
|
|
2325
2737
|
init_stage_records();
|
|
2326
|
-
import { decryptExtractedPartition as decryptExtractedPartition2 } from "@noy-db/hub/
|
|
2738
|
+
import { decryptExtractedPartition as decryptExtractedPartition2 } from "@noy-db/hub/cargo";
|
|
2327
2739
|
var MinVersionError = class extends Error {
|
|
2328
2740
|
constructor(fromVersion, toVersion) {
|
|
2329
2741
|
super(
|
|
@@ -2402,7 +2814,7 @@ var Lobby = class {
|
|
|
2402
2814
|
}
|
|
2403
2815
|
async openVaultGroup(name, opts) {
|
|
2404
2816
|
const db = this.noydb;
|
|
2405
|
-
if (db.isClosed) throw new
|
|
2817
|
+
if (db.isClosed) throw new ValidationError3("Instance is closed");
|
|
2406
2818
|
if (name === STATE_VAULT_NAME2) throw new ReservedVaultNameError2(name);
|
|
2407
2819
|
const template = this.vaultTemplates.get(opts.sharding.vaultTemplate);
|
|
2408
2820
|
if (!template) throw new VaultTemplateNotFoundError(opts.sharding.vaultTemplate);
|
|
@@ -2424,7 +2836,7 @@ var Lobby = class {
|
|
|
2424
2836
|
}
|
|
2425
2837
|
async openStateManagementVault() {
|
|
2426
2838
|
const db = this.noydb;
|
|
2427
|
-
if (db.isClosed) throw new
|
|
2839
|
+
if (db.isClosed) throw new ValidationError3("Instance is closed");
|
|
2428
2840
|
const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
|
|
2429
2841
|
return StateManagementVault2.open(db);
|
|
2430
2842
|
}
|
|
@@ -2444,6 +2856,64 @@ var Lobby = class {
|
|
|
2444
2856
|
const { graduate: graduateFn } = await Promise.resolve().then(() => (init_graduate(), graduate_exports));
|
|
2445
2857
|
return graduateFn(this.noydb, docked, opts);
|
|
2446
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
|
+
}
|
|
2447
2917
|
// ─── FR-7 Surface API ─────────────────────────────────────────────────────
|
|
2448
2918
|
/**
|
|
2449
2919
|
* Export a scoped partition from `vaultName` bounded to the given surface.
|
|
@@ -2525,8 +2995,10 @@ export {
|
|
|
2525
2995
|
NOYDB_MULTI_BUNDLE_MAGIC,
|
|
2526
2996
|
NOYDB_MULTI_BUNDLE_PREFIX_BYTES,
|
|
2527
2997
|
NOYDB_MULTI_BUNDLE_VERSION,
|
|
2998
|
+
PostureViolationError,
|
|
2528
2999
|
ReservedVaultNameError3 as ReservedVaultNameError,
|
|
2529
3000
|
ShardProvisioningError2 as ShardProvisioningError,
|
|
3001
|
+
ShareLinkResolutionError,
|
|
2530
3002
|
SurfaceCadenceScheduler,
|
|
2531
3003
|
SurfaceNotFoundError,
|
|
2532
3004
|
SurfaceStateError,
|