@mulmoclaude/core 3.6.0 → 3.10.0
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/collection/core/itemId.d.ts +28 -0
- package/dist/collection/firestore.cjs +9 -1
- package/dist/collection/firestore.cjs.map +1 -1
- package/dist/collection/firestore.js +10 -2
- package/dist/collection/firestore.js.map +1 -1
- package/dist/collection/index.cjs +55 -66
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.d.ts +1 -1
- package/dist/collection/index.js +3 -15
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/firestoreDocs.d.ts +18 -0
- package/dist/collection/server/host.d.ts +40 -0
- package/dist/collection/server/index.cjs +25 -4
- package/dist/collection/server/index.d.ts +4 -1
- package/dist/collection/server/index.js +4 -4
- package/dist/collection/server/io.d.ts +5 -3
- package/dist/collection/server/publishChecks.d.ts +29 -0
- package/dist/collection/server/publishManifest.d.ts +181 -0
- package/dist/collection/server/publishProject.d.ts +255 -0
- package/dist/collection/server/validate.d.ts +12 -0
- package/dist/collection-watchers/index.cjs +70 -26
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +67 -23
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/{discovery-CdkaURVY.js → discovery-B9zdNkgW.js} +189 -18
- package/dist/discovery-B9zdNkgW.js.map +1 -0
- package/dist/{discovery-Cw5xWzZD.cjs → discovery-BsXiDZBR.cjs} +226 -43
- package/dist/discovery-BsXiDZBR.cjs.map +1 -0
- package/dist/feeds/index.cjs +4 -4
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +6 -6
- package/dist/feeds/server/index.js +4 -4
- package/dist/firestore/listen.d.ts +4 -0
- package/dist/google/index.cjs +5 -5
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.js +2 -2
- package/dist/{ingestTypes-CEi-Ot7F.js → ingestTypes-B312BwXg.js} +2 -2
- package/dist/{ingestTypes-CEi-Ot7F.js.map → ingestTypes-B312BwXg.js.map} +1 -1
- package/dist/{ingestTypes-iaiq33Ph.cjs → ingestTypes-B4XfrUoq.cjs} +3 -3
- package/dist/{ingestTypes-iaiq33Ph.cjs.map → ingestTypes-B4XfrUoq.cjs.map} +1 -1
- package/dist/{calendarGrid-CQ8MVSRb.js → itemId-DfTm08jm.js} +34 -2
- package/dist/itemId-DfTm08jm.js.map +1 -0
- package/dist/{calendarGrid-DGILaVxI.cjs → itemId-Dzrr3vs5.cjs} +45 -1
- package/dist/itemId-Dzrr3vs5.cjs.map +1 -0
- package/dist/listen-D5av7GzB.js +22 -0
- package/dist/listen-D5av7GzB.js.map +1 -0
- package/dist/listen-UZA68Upt.cjs +45 -0
- package/dist/listen-UZA68Upt.cjs.map +1 -0
- package/dist/{promptSafety-CpiME8pj.js → promptSafety-CSdyp7RF.js} +2 -2
- package/dist/{promptSafety-CpiME8pj.js.map → promptSafety-CSdyp7RF.js.map} +1 -1
- package/dist/{promptSafety-NNGViiCr.cjs → promptSafety-CWqYq-NS.cjs} +8 -8
- package/dist/{promptSafety-NNGViiCr.cjs.map → promptSafety-CWqYq-NS.cjs.map} +1 -1
- package/dist/remote-host/server/hostRunner.d.ts +2 -4
- package/dist/remote-host/server/index.cjs +4 -19
- package/dist/remote-host/server/index.cjs.map +1 -1
- package/dist/remote-host/server/index.js +1 -16
- package/dist/remote-host/server/index.js.map +1 -1
- package/dist/{server-DdHaX_Uu.js → server-BNe8XMtu.js} +677 -6
- package/dist/server-BNe8XMtu.js.map +1 -0
- package/dist/{server-9mFaGLdq.cjs → server-CgkkWWBd.cjs} +807 -22
- package/dist/server-CgkkWWBd.cjs.map +1 -0
- package/package.json +1 -1
- package/dist/calendarGrid-CQ8MVSRb.js.map +0 -1
- package/dist/calendarGrid-DGILaVxI.cjs.map +0 -1
- package/dist/collection/core/shortHexId.d.ts +0 -8
- package/dist/discovery-CdkaURVY.js.map +0 -1
- package/dist/discovery-Cw5xWzZD.cjs.map +0 -1
- package/dist/server-9mFaGLdq.cjs.map +0 -1
- package/dist/server-DdHaX_Uu.js.map +0 -1
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
|
|
2
2
|
const require_dist = require("./dist-CsgSfWwR.cjs");
|
|
3
3
|
const require_root = require("./root-rPH6FGDT.cjs");
|
|
4
|
-
const
|
|
4
|
+
const require_itemId = require("./itemId-Dzrr3vs5.cjs");
|
|
5
5
|
const require_project = require("./project-C1ep9pvo.cjs");
|
|
6
|
+
const require_listen = require("./listen-UZA68Upt.cjs");
|
|
6
7
|
const require_templatePath = require("./templatePath-27vUZowm.cjs");
|
|
7
8
|
let node_fs = require("node:fs");
|
|
8
9
|
let node_path = require("node:path");
|
|
9
10
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
10
11
|
let node_crypto = require("node:crypto");
|
|
11
12
|
let node_fs_promises = require("node:fs/promises");
|
|
13
|
+
let zod = require("zod");
|
|
12
14
|
let node_os = require("node:os");
|
|
13
15
|
let iconv_lite = require("iconv-lite");
|
|
14
16
|
iconv_lite = require_rolldown_runtime.__toESM(iconv_lite, 1);
|
|
15
|
-
let zod = require("zod");
|
|
16
17
|
//#region src/host/hostSlot.ts
|
|
17
18
|
function createHostSlot(name) {
|
|
18
19
|
let current = null;
|
|
@@ -54,7 +55,7 @@ var COLLECTION_ROOT_REQUIRED = "COLLECTION_ROOT_REQUIRED";
|
|
|
54
55
|
/** Build a local {@link CollectionKey}, canonicalising the root. The identity
|
|
55
56
|
* type itself is isomorphic and cannot canonicalise (that needs `node:path`),
|
|
56
57
|
* so this is the constructor server code should use. */
|
|
57
|
-
var localCollectionKey = (root, slug) =>
|
|
58
|
+
var localCollectionKey = (root, slug) => require_itemId.localCollectionKeyOf(require_root.canonicalRoot(root), slug);
|
|
58
59
|
/** Build a change payload, attaching `root` only when the engine call carried
|
|
59
60
|
* an explicit one. Centralised so every publish site states the root the same
|
|
60
61
|
* way, and so a single-workspace host's payload shape stays byte-identical to
|
|
@@ -69,7 +70,7 @@ function collectionChangePayload(base, root) {
|
|
|
69
70
|
* which is what it is called inside its app; `aid` is what makes it an
|
|
70
71
|
* identity. Never stamps a `root` — a shared collection does not have one. */
|
|
71
72
|
function sharedCollectionChangePayload(base, aid) {
|
|
72
|
-
|
|
73
|
+
require_itemId.sharedCollectionKey(aid, base.slug);
|
|
73
74
|
return {
|
|
74
75
|
...base,
|
|
75
76
|
aid
|
|
@@ -102,11 +103,12 @@ function requireOneScope(payload) {
|
|
|
102
103
|
}
|
|
103
104
|
function collectionChangeKey(payload, fallbackRoot) {
|
|
104
105
|
requireOneScope(payload);
|
|
105
|
-
return payload.aid === void 0 ? localCollectionKey(payload.root ?? fallbackRoot, payload.slug) :
|
|
106
|
+
return payload.aid === void 0 ? localCollectionKey(payload.root ?? fallbackRoot, payload.slug) : require_itemId.sharedCollectionKey(payload.aid, payload.slug);
|
|
106
107
|
}
|
|
107
108
|
var hostSlot = createHostSlot("@mulmoclaude/core/collection/server: configureCollectionHost()");
|
|
108
109
|
var changePublisher = null;
|
|
109
110
|
var firestoreAccessor = null;
|
|
111
|
+
var sharedCollectionsSupported = false;
|
|
110
112
|
/** Wire the engine to a host. Call once at server startup, before any
|
|
111
113
|
* collection storage operation. Re-binding to a *different* host throws —
|
|
112
114
|
* silently redirecting later filesystem operations to another workspace
|
|
@@ -130,6 +132,39 @@ function setCollectionChangePublisher(publish) {
|
|
|
130
132
|
function publishCollectionChange(payload) {
|
|
131
133
|
changePublisher?.(payload);
|
|
132
134
|
}
|
|
135
|
+
/** Does this host serve SHARED (firestore-backed) collections at all?
|
|
136
|
+
*
|
|
137
|
+
* Opt-in, and `false` until a host says otherwise, because the default has to
|
|
138
|
+
* be the safe one: a host that cannot reach Firestore must not accept a
|
|
139
|
+
* shared schema and then report the collection as empty.
|
|
140
|
+
*
|
|
141
|
+
* WHY A CAPABILITY AND NOT A HOST CHECK. The engine must not know which host
|
|
142
|
+
* it is running in. The rule being expressed — shared collections live in a
|
|
143
|
+
* PROJECT REPOSITORY, not in a single managed workspace where one roster
|
|
144
|
+
* would govern every unrelated collection beside it — is a property the host
|
|
145
|
+
* knows about ITSELF. Asking the engine to test for a particular host's
|
|
146
|
+
* workspace would put a host's name in shared code and make every change to
|
|
147
|
+
* a one-host feature a change to this package.
|
|
148
|
+
*
|
|
149
|
+
* WHY NOT A FIELD ON `CollectionHost`. Same reason the accessor below is not
|
|
150
|
+
* one: `configureCollectionHost` is a ONE-SHOT binding a host sets at startup
|
|
151
|
+
* and cannot re-bind, so a suite exercising this engine could not turn the
|
|
152
|
+
* capability on for itself without owning the whole host. This is the same
|
|
153
|
+
* concern as the accessor and belongs beside it.
|
|
154
|
+
*
|
|
155
|
+
* NOT DERIVED FROM THE ACCESSOR. "Has a session right now" and "serves shared
|
|
156
|
+
* collections at all" are different questions: the accessor answers null
|
|
157
|
+
* between connections, and a collection must not stop being ACCEPTABLE
|
|
158
|
+
* because nobody is signed in — that would turn "connect first" into "this
|
|
159
|
+
* schema is invalid". */
|
|
160
|
+
function setSharedCollectionsSupport(supported) {
|
|
161
|
+
sharedCollectionsSupported = supported;
|
|
162
|
+
}
|
|
163
|
+
/** Whether the host declared support. Consulted from the schema ACCEPTANCE
|
|
164
|
+
* gate, whose contract is to return a reason rather than raise. */
|
|
165
|
+
function hostSupportsSharedCollections() {
|
|
166
|
+
return sharedCollectionsSupported;
|
|
167
|
+
}
|
|
133
168
|
/** Wire the accessor for the host's authenticated Firestore session.
|
|
134
169
|
*
|
|
135
170
|
* Separate from `configureCollectionHost` for the same reason
|
|
@@ -234,7 +269,7 @@ var SCHEMA_FILE = "schema.json";
|
|
|
234
269
|
* `js/path-injection` sanitiser. */
|
|
235
270
|
function safeSlugName(slug) {
|
|
236
271
|
if (typeof slug !== "string") return null;
|
|
237
|
-
if (!
|
|
272
|
+
if (!require_itemId.SAFE_SLUG_PATTERN.test(slug)) return null;
|
|
238
273
|
const basename = node_path.default.basename(slug);
|
|
239
274
|
if (basename !== slug) return null;
|
|
240
275
|
return basename;
|
|
@@ -246,7 +281,7 @@ function safeSlugName(slug) {
|
|
|
246
281
|
* `js/path-injection` sanitiser CodeQL recognises on `safeSlugName`. */
|
|
247
282
|
function safeRecordId(recordId) {
|
|
248
283
|
if (typeof recordId !== "string") return null;
|
|
249
|
-
if (!
|
|
284
|
+
if (!require_itemId.SAFE_RECORD_ID_PATTERN.test(recordId)) return null;
|
|
250
285
|
if (recordId.includes("..")) return null;
|
|
251
286
|
const basename = node_path.default.basename(recordId);
|
|
252
287
|
if (basename !== recordId) return null;
|
|
@@ -378,7 +413,7 @@ function sharedItemsPath(key) {
|
|
|
378
413
|
* missing `appId` — see NO_APP. */
|
|
379
414
|
function keyOf(collection) {
|
|
380
415
|
if (collection.appId === void 0) throw new Error(NO_APP);
|
|
381
|
-
return
|
|
416
|
+
return require_itemId.sharedCollectionKey(collection.appId, collection.slug);
|
|
382
417
|
}
|
|
383
418
|
function requireHandle() {
|
|
384
419
|
const handle = firestoreHandle();
|
|
@@ -420,8 +455,32 @@ async function guarded(key, email, run) {
|
|
|
420
455
|
* older version) can hold anything, so a non-object is dropped rather than
|
|
421
456
|
* surfaced as a broken record — the same fail-soft the file store applies to
|
|
422
457
|
* an unparseable `.json`. */
|
|
423
|
-
|
|
424
|
-
|
|
458
|
+
/** A stored document as a record — with its identity taken from the DOCUMENT
|
|
459
|
+
* ID, never from the document's own fields.
|
|
460
|
+
*
|
|
461
|
+
* This is the one place a shared record's identity is decided, and it is
|
|
462
|
+
* decided against the field the writer supplied on purpose.
|
|
463
|
+
*
|
|
464
|
+
* The rules can constrain the document id (`idFrom` pins it to the
|
|
465
|
+
* submitter's uid, or to uid+field) and they CANNOT constrain the value of a
|
|
466
|
+
* field: `validateOk` checks which keys are present, `keyFieldsOk` checks a
|
|
467
|
+
* declared enum, and nothing compares `request.resource.data[primaryKey]`
|
|
468
|
+
* with the path being written. So a public submitter writing at their one
|
|
469
|
+
* permitted document id could put ANY value in the primary-key field —
|
|
470
|
+
* another member's record id, or a duplicate — and every reader would take it
|
|
471
|
+
* as the record's identity. Overwriting it here makes that unreachable rather
|
|
472
|
+
* than merely discouraged, and it costs nothing: for a record written through
|
|
473
|
+
* this store the two already agree, because `firestoreWrite` writes at the id
|
|
474
|
+
* it was given.
|
|
475
|
+
*
|
|
476
|
+
* It also removes the reason a submit path would have had to accept the
|
|
477
|
+
* primary key as a `createField` at all — a submission that cannot name its
|
|
478
|
+
* own id is exactly right when the id is the thing being assigned. */
|
|
479
|
+
function toItem(data, docId, primaryKey) {
|
|
480
|
+
return require_dist.isRecord(data) ? {
|
|
481
|
+
...data,
|
|
482
|
+
[primaryKey]: docId
|
|
483
|
+
} : null;
|
|
425
484
|
}
|
|
426
485
|
/** Record ids are validated with the SAME helper every other backend uses.
|
|
427
486
|
* Firestore would accept ids the file store refuses, but a record should stay
|
|
@@ -432,9 +491,9 @@ function withSafeId(itemId, onInvalid, run) {
|
|
|
432
491
|
if (safeId === null) return onInvalid();
|
|
433
492
|
return run(safeId, requireHandle());
|
|
434
493
|
}
|
|
435
|
-
async function firestoreList(key) {
|
|
494
|
+
async function firestoreList(key, primaryKey) {
|
|
436
495
|
const { docs, email } = requireHandle();
|
|
437
|
-
return (await guarded(key, email, () => docs.list(sharedItemsPath(key)))).map((entry) => toItem(entry.data)).filter((item) => item !== null);
|
|
496
|
+
return (await guarded(key, email, () => docs.list(sharedItemsPath(key)))).map((entry) => toItem(entry.data, entry.id, primaryKey)).filter((item) => item !== null);
|
|
438
497
|
}
|
|
439
498
|
/** Paging is emulated over a full ordered read rather than pushed into
|
|
440
499
|
* Firestore: `offset` has no server-side form there (the cursor API needs the
|
|
@@ -442,7 +501,7 @@ async function firestoreList(key) {
|
|
|
442
501
|
* `total` needs the full count anyway. Hence `nativePaging: false` — the
|
|
443
502
|
* capability is honest about the cost. */
|
|
444
503
|
async function firestorePage(key, primaryKey, opts) {
|
|
445
|
-
const items = await firestoreList(key);
|
|
504
|
+
const items = await firestoreList(key, primaryKey);
|
|
446
505
|
const offset = Math.max(0, opts.offset ?? 0);
|
|
447
506
|
return {
|
|
448
507
|
items: projectItemFields(opts.limit === void 0 ? items.slice(offset) : items.slice(offset, offset + Math.max(0, opts.limit)), opts.fields, primaryKey),
|
|
@@ -450,8 +509,8 @@ async function firestorePage(key, primaryKey, opts) {
|
|
|
450
509
|
truncated: false
|
|
451
510
|
};
|
|
452
511
|
}
|
|
453
|
-
async function firestoreRead(key, itemId) {
|
|
454
|
-
return withSafeId(itemId, () => Promise.resolve(null), async (safeId, { docs, email }) => toItem(await guarded(key, email, () => docs.get(sharedItemsPath(key), safeId))));
|
|
512
|
+
async function firestoreRead(key, itemId, primaryKey) {
|
|
513
|
+
return withSafeId(itemId, () => Promise.resolve(null), async (safeId, { docs, email }) => toItem(await guarded(key, email, () => docs.get(sharedItemsPath(key), safeId)), safeId, primaryKey));
|
|
455
514
|
}
|
|
456
515
|
/** Publish the "records changed" ping for a shared collection.
|
|
457
516
|
*
|
|
@@ -502,6 +561,111 @@ async function firestoreDelete(key, itemId, opts) {
|
|
|
502
561
|
};
|
|
503
562
|
});
|
|
504
563
|
}
|
|
564
|
+
/** Deliver one snapshot's changes.
|
|
565
|
+
*
|
|
566
|
+
* THE FIRST SNAPSHOT IS ONE COLLECTION-LEVEL REPORT, not N per-record ones.
|
|
567
|
+
* `onSnapshot` hands over the collection's current contents immediately, as a
|
|
568
|
+
* snapshot in which every existing document reads as `added`.
|
|
569
|
+
*
|
|
570
|
+
* Dropping it is wrong. There is a GAP on either side of a subscription — the
|
|
571
|
+
* boot reconcile finishes before the listener arms, and a listener that died
|
|
572
|
+
* is re-armed after a backoff — and a record that moved inside one of those
|
|
573
|
+
* gaps appears only in that first snapshot. Since this backend now HAS a
|
|
574
|
+
* `watch`, the watcher's periodic re-derivation no longer covers it, so a
|
|
575
|
+
* dropped first snapshot means stale bells and stale views until that record
|
|
576
|
+
* happens to change again. That is exactly the failure this step was supposed
|
|
577
|
+
* to remove.
|
|
578
|
+
*
|
|
579
|
+
* Announcing every record individually is also wrong: it is a refresh storm
|
|
580
|
+
* to every open view, on every mount and every reconnect. `{ kind: "collection" }`
|
|
581
|
+
* says the same thing in one event — the watcher answers it with a full
|
|
582
|
+
* re-derivation plus a sweep, which is precisely "work out what changed while
|
|
583
|
+
* I was not listening".
|
|
584
|
+
*
|
|
585
|
+
* A change this process made itself also arrives here — the write path has
|
|
586
|
+
* already published for it, so the record is reconciled twice. Harmless
|
|
587
|
+
* (reconciling is idempotent) and left alone: suppressing it would mean
|
|
588
|
+
* tracking our own in-flight writes, and getting that wrong loses a real
|
|
589
|
+
* change rather than a duplicate one. */
|
|
590
|
+
function deliverSnapshot(run, ids, initial) {
|
|
591
|
+
if (run.stopped) return;
|
|
592
|
+
if (initial) {
|
|
593
|
+
run.onChange({ kind: "collection" });
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
for (const itemId of ids) run.onChange({
|
|
597
|
+
kind: "item",
|
|
598
|
+
itemId
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
/** A listener died. Firestore never revives one on its own, so the choice is
|
|
602
|
+
* re-subscribe or go dark.
|
|
603
|
+
*
|
|
604
|
+
* NO OVERALL RETRY WINDOW, unlike `hostRunner`'s listener. There, giving up
|
|
605
|
+
* escalates to a lifecycle owner that can re-authenticate; here there is
|
|
606
|
+
* nobody above to escalate to — the watcher registers a mounted collection
|
|
607
|
+
* once and never re-arms it, so "give up" means this collection serves stale
|
|
608
|
+
* data until the server restarts. A capped 30s re-subscribe is cheap and
|
|
609
|
+
* makes recovery automatic when the session comes back.
|
|
610
|
+
*
|
|
611
|
+
* A FATAL error still stops: `permission-denied` is what a revoked membership
|
|
612
|
+
* looks like, and re-listening cannot restore a grant. It is logged as such
|
|
613
|
+
* rather than retried in silence. */
|
|
614
|
+
function handleWatchError(run, error) {
|
|
615
|
+
if (run.stopped) return;
|
|
616
|
+
if (require_listen.classifyListenerError(error) === "fatal") {
|
|
617
|
+
log.warn("collections", "shared collection listener stopped", {
|
|
618
|
+
aid: run.key.aid,
|
|
619
|
+
cid: run.key.cid,
|
|
620
|
+
error: require_dist.isRecord(error) && typeof error.message === "string" ? error.message : String(error),
|
|
621
|
+
detail: "live updates for this collection are off until the server re-syncs; a revoked membership looks exactly like this"
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
run.retryTimer = setTimeout(() => subscribeShared(run), require_listen.backoffDelayMs(run.attempt));
|
|
626
|
+
run.attempt += 1;
|
|
627
|
+
}
|
|
628
|
+
/** (Re-)arm the listener. The handle is fetched HERE rather than captured: a
|
|
629
|
+
* retry may land after the session was replaced, and listening through a
|
|
630
|
+
* closed session's handle would fail forever. */
|
|
631
|
+
function subscribeShared(run) {
|
|
632
|
+
run.retryTimer = null;
|
|
633
|
+
if (run.stopped) return;
|
|
634
|
+
const handle = firestoreHandle();
|
|
635
|
+
if (handle === null) {
|
|
636
|
+
run.retryTimer = setTimeout(() => subscribeShared(run), require_listen.backoffDelayMs(run.attempt));
|
|
637
|
+
run.attempt += 1;
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
run.detach = handle.docs.watch(sharedItemsPath(run.key), (ids, meta) => {
|
|
641
|
+
run.attempt = 0;
|
|
642
|
+
deliverSnapshot(run, ids, meta.initial);
|
|
643
|
+
}, (error) => handleWatchError(run, error));
|
|
644
|
+
}
|
|
645
|
+
/** Subscribe to a shared collection's records.
|
|
646
|
+
*
|
|
647
|
+
* Returns `null` when there is no session: the watcher reads that as "has a
|
|
648
|
+
* watch but could not arm it", leaves the collection unmounted and retries on
|
|
649
|
+
* the next sync tick — which is the right channel for a connection that has
|
|
650
|
+
* not happened yet. Arming a dead listener instead would mark the slug mounted
|
|
651
|
+
* forever. */
|
|
652
|
+
function armSharedWatch(key, onChange) {
|
|
653
|
+
if (firestoreHandle() === null) return null;
|
|
654
|
+
const run = {
|
|
655
|
+
key,
|
|
656
|
+
onChange,
|
|
657
|
+
stopped: false,
|
|
658
|
+
detach: () => {},
|
|
659
|
+
retryTimer: null,
|
|
660
|
+
attempt: 0
|
|
661
|
+
};
|
|
662
|
+
subscribeShared(run);
|
|
663
|
+
return () => {
|
|
664
|
+
run.stopped = true;
|
|
665
|
+
if (run.retryTimer !== null) clearTimeout(run.retryTimer);
|
|
666
|
+
run.detach();
|
|
667
|
+
};
|
|
668
|
+
}
|
|
505
669
|
/** The store factory registered for `storage.type === "firestore"`.
|
|
506
670
|
* Synchronous and connection-agnostic by contract — see the header. */
|
|
507
671
|
function firestoreStoreFor(collection, opts) {
|
|
@@ -516,14 +680,15 @@ function firestoreStoreFor(collection, opts) {
|
|
|
516
680
|
nativeQuery: false,
|
|
517
681
|
nativePaging: false
|
|
518
682
|
},
|
|
519
|
-
list: async () => firestoreList(keyOf(collection)),
|
|
683
|
+
list: async () => firestoreList(keyOf(collection), primaryKey),
|
|
520
684
|
page: async (pageOpts = {}) => firestorePage(keyOf(collection), primaryKey, pageOpts),
|
|
521
|
-
read: async (itemId) => firestoreRead(keyOf(collection), itemId),
|
|
685
|
+
read: async (itemId) => firestoreRead(keyOf(collection), itemId, primaryKey),
|
|
522
686
|
write: async (itemId, item, writeOpts = {}) => firestoreWrite(keyOf(collection), itemId, item, {
|
|
523
687
|
...ioOpts,
|
|
524
688
|
refuseOverwrite: writeOpts.refuseOverwrite
|
|
525
689
|
}),
|
|
526
|
-
delete: async (itemId) => firestoreDelete(keyOf(collection), itemId, ioOpts)
|
|
690
|
+
delete: async (itemId) => firestoreDelete(keyOf(collection), itemId, ioOpts),
|
|
691
|
+
watch: async (onChange) => armSharedWatch(keyOf(collection), onChange)
|
|
527
692
|
};
|
|
528
693
|
}
|
|
529
694
|
//#endregion
|
|
@@ -591,7 +756,7 @@ function parseAppManifest(raw) {
|
|
|
591
756
|
kind: "malformed",
|
|
592
757
|
detail: "declares no `aid` string"
|
|
593
758
|
};
|
|
594
|
-
if (!
|
|
759
|
+
if (!require_itemId.isValidCollectionName(aid)) return {
|
|
595
760
|
ok: false,
|
|
596
761
|
kind: "malformed",
|
|
597
762
|
detail: `\`aid\` '${aid}' is not a valid app id`
|
|
@@ -813,11 +978,13 @@ async function deleteItem(dataDir, itemId, opts = {}) {
|
|
|
813
978
|
throw err;
|
|
814
979
|
}
|
|
815
980
|
}
|
|
816
|
-
/** Generate a
|
|
817
|
-
*
|
|
818
|
-
*
|
|
981
|
+
/** Generate a record id. Used by POST when the form doesn't carry a
|
|
982
|
+
* primary-key value (UI shortcut — Claude normally derives a semantic
|
|
983
|
+
* id from the record's name). Delegates to the isomorphic generator, so
|
|
984
|
+
* the id the UI pre-fills and the id a blank-id POST mints stay the
|
|
985
|
+
* same thing rather than two implementations that have to agree. */
|
|
819
986
|
function generateItemId() {
|
|
820
|
-
return
|
|
987
|
+
return require_itemId.newItemId();
|
|
821
988
|
}
|
|
822
989
|
/** The item id a CREATE should use for `schema`, or null when the
|
|
823
990
|
* caller should generate one. A singleton collection pins every
|
|
@@ -1095,7 +1262,7 @@ function normalizeCsvValue(value) {
|
|
|
1095
1262
|
function csvRowToItem(row, primaryKey) {
|
|
1096
1263
|
const normalized = Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeCsvValue(value)]));
|
|
1097
1264
|
const rawKey = normalized[primaryKey];
|
|
1098
|
-
const keyText =
|
|
1265
|
+
const keyText = require_itemId.fieldTextOrNull(rawKey);
|
|
1099
1266
|
if (keyText === null || keyText === "") return null;
|
|
1100
1267
|
return {
|
|
1101
1268
|
...normalized,
|
|
@@ -1703,8 +1870,8 @@ function sqliteStoreFor(collection, opts) {
|
|
|
1703
1870
|
* is filesystem-dependent; paging needs determinism. */
|
|
1704
1871
|
function sortByRecordId(items, primaryKey) {
|
|
1705
1872
|
return [...items].sort((left, right) => {
|
|
1706
|
-
const leftId =
|
|
1707
|
-
const rightId =
|
|
1873
|
+
const leftId = require_itemId.fieldText(left[primaryKey]);
|
|
1874
|
+
const rightId = require_itemId.fieldText(right[primaryKey]);
|
|
1708
1875
|
if (leftId < rightId) return -1;
|
|
1709
1876
|
return leftId > rightId ? 1 : 0;
|
|
1710
1877
|
});
|
|
@@ -1714,7 +1881,7 @@ function sortByRecordId(items, primaryKey) {
|
|
|
1714
1881
|
* data file itself. Every write entry point checks this BEFORE calling
|
|
1715
1882
|
* `writeItem`/`deleteItem` — server-enforced, not just UI-hidden. */
|
|
1716
1883
|
function collectionWritable(collection) {
|
|
1717
|
-
return !
|
|
1884
|
+
return !require_itemId.isReadOnlySchema(collection.schema);
|
|
1718
1885
|
}
|
|
1719
1886
|
/** The one-line refusal write paths surface (HTTP 405 / MCP error text). */
|
|
1720
1887
|
function readOnlyRefusal(slug) {
|
|
@@ -1782,7 +1949,7 @@ var storeFactories = /* @__PURE__ */ new Map([
|
|
|
1782
1949
|
* schema's `StorageZ` union gates it), so the throw is a loud invariant
|
|
1783
1950
|
* breach, not a user-facing path. */
|
|
1784
1951
|
function storeFor(collection, opts = {}) {
|
|
1785
|
-
const kind =
|
|
1952
|
+
const kind = require_itemId.storageKindFor(collection.schema);
|
|
1786
1953
|
const factory = storeFactories.get(kind);
|
|
1787
1954
|
if (!factory) throw new Error(`no store factory registered for storage kind '${kind}'`);
|
|
1788
1955
|
return factory(collection, opts);
|
|
@@ -1825,7 +1992,7 @@ var CODE_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
|
1825
1992
|
]);
|
|
1826
1993
|
var namesStoredField = (fields, name, primaryKey) => {
|
|
1827
1994
|
const target = declaredField(fields, name);
|
|
1828
|
-
return target !== void 0 && !
|
|
1995
|
+
return target !== void 0 && !require_itemId.COMPUTED_TYPES.has(target.type) && name !== primaryKey;
|
|
1829
1996
|
};
|
|
1830
1997
|
var hasUniqueIds = (entries) => entries === void 0 || new Set(entries.map((entry) => entry.id)).size === entries.length;
|
|
1831
1998
|
/** Exactly one storage declaration: native records need `dataPath`, an external
|
|
@@ -1884,7 +2051,7 @@ function collectionActionsAreNotMutate(schema) {
|
|
|
1884
2051
|
* otherwise the create form would lock the primary key to a value the POST
|
|
1885
2052
|
* route then rejects, making the collection impossible to initialize. */
|
|
1886
2053
|
function singletonIsAValidRecordId(schema) {
|
|
1887
|
-
return schema.singleton === void 0 ||
|
|
2054
|
+
return schema.singleton === void 0 || require_itemId.isSafeRecordId(schema.singleton);
|
|
1888
2055
|
}
|
|
1889
2056
|
function collectCurrencyFieldRefs(fields) {
|
|
1890
2057
|
const refs = [];
|
|
@@ -1929,7 +2096,7 @@ function completionFlagReadsOnlyStoredFields(schema) {
|
|
|
1929
2096
|
if (spec?.type !== "flag") return true;
|
|
1930
2097
|
return spec.where.every((cond) => [cond.field, ...cond.valueFrom ? [cond.valueFrom.field] : []].every((name) => {
|
|
1931
2098
|
const target = declaredField(schema.fields, name);
|
|
1932
|
-
return target !== void 0 && !
|
|
2099
|
+
return target !== void 0 && !require_itemId.COMPUTED_TYPES.has(target.type);
|
|
1933
2100
|
}));
|
|
1934
2101
|
}
|
|
1935
2102
|
/** `displayField`, like `completionField`, must name a real top-level field —
|
|
@@ -2077,7 +2244,7 @@ function fieldDrivenFromFieldCarried(schema) {
|
|
|
2077
2244
|
if (set && Object.prototype.hasOwnProperty.call(set, driven.fromField)) {
|
|
2078
2245
|
const raw = set[driven.fromField];
|
|
2079
2246
|
if (raw === void 0 || raw === null || raw === "") return false;
|
|
2080
|
-
const key =
|
|
2247
|
+
const key = require_itemId.fieldTextOrNull(raw);
|
|
2081
2248
|
return key !== null && Object.prototype.hasOwnProperty.call(driven.map, key);
|
|
2082
2249
|
}
|
|
2083
2250
|
return (carry ?? []).includes(driven.fromField);
|
|
@@ -2131,7 +2298,7 @@ function notifyWhenFieldIsDeclared(schema) {
|
|
|
2131
2298
|
* selector key (`custom:<id>`) and the capability-token clamp key, both of
|
|
2132
2299
|
* which expect a path-safe token. */
|
|
2133
2300
|
function viewIdsAreSlugs(schema) {
|
|
2134
|
-
return schema.views === void 0 || schema.views.every((view) =>
|
|
2301
|
+
return schema.views === void 0 || schema.views.every((view) => require_itemId.isSafeSlug(view.id));
|
|
2135
2302
|
}
|
|
2136
2303
|
/** Custom view ids must be unique so the selector + token clamp resolve
|
|
2137
2304
|
* unambiguously. */
|
|
@@ -2231,7 +2398,7 @@ var RefFieldZ = zod.z.object({
|
|
|
2231
2398
|
type: zod.z.literal("ref"),
|
|
2232
2399
|
...fieldBase,
|
|
2233
2400
|
to: zod.z.string().min(1)
|
|
2234
|
-
}).refine((spec) =>
|
|
2401
|
+
}).refine((spec) => require_itemId.isSafeSlug(spec.to), slugMessage("to"));
|
|
2235
2402
|
/** A money amount. See `currencyKeys` for the currency-source contract. */
|
|
2236
2403
|
var MoneyFieldZ = zod.z.object({
|
|
2237
2404
|
type: zod.z.literal("money"),
|
|
@@ -2266,7 +2433,7 @@ var SubRefFieldZ = zod.z.object({
|
|
|
2266
2433
|
type: zod.z.literal("ref"),
|
|
2267
2434
|
...subFieldBase,
|
|
2268
2435
|
to: zod.z.string().min(1)
|
|
2269
|
-
}).refine((spec) =>
|
|
2436
|
+
}).refine((spec) => require_itemId.isSafeSlug(spec.to), slugMessage("to"));
|
|
2270
2437
|
var SubMoneyFieldZ = zod.z.object({
|
|
2271
2438
|
type: zod.z.literal("money"),
|
|
2272
2439
|
...subFieldBase,
|
|
@@ -2329,7 +2496,7 @@ var EmbedFieldZ = zod.z.object({
|
|
|
2329
2496
|
to: zod.z.string().min(1),
|
|
2330
2497
|
id: zod.z.string().trim().min(1).optional(),
|
|
2331
2498
|
idField: zod.z.string().trim().min(1).optional()
|
|
2332
|
-
}).refine((spec) =>
|
|
2499
|
+
}).refine((spec) => require_itemId.isSafeSlug(spec.to) && spec.id !== void 0 !== (spec.idField !== void 0), {
|
|
2333
2500
|
message: "fields with type 'embed' must declare a `to` (valid collection slug) and exactly one of `id` (a fixed record's primary key) or `idField` (a sibling field naming the per-record target)",
|
|
2334
2501
|
path: ["id"]
|
|
2335
2502
|
});
|
|
@@ -2350,7 +2517,7 @@ var BacklinksFieldZ = zod.z.object({
|
|
|
2350
2517
|
via: zod.z.string().trim().min(1),
|
|
2351
2518
|
display: zod.z.array(zod.z.string().trim().min(1)).min(1),
|
|
2352
2519
|
filter: WhenZ.optional()
|
|
2353
|
-
}).refine((spec) =>
|
|
2520
|
+
}).refine((spec) => require_itemId.isSafeSlug(spec.from), slugMessage("from"));
|
|
2354
2521
|
/** A cross-collection AGGREGATE over a backlink relation (plan step ⑤ of
|
|
2355
2522
|
* plans/done/collection-ontology.md): a computed number — never stored — that
|
|
2356
2523
|
* sums a source column (or counts rows) over the records in `from` whose
|
|
@@ -2371,7 +2538,7 @@ var RollupFieldZ = zod.z.object({
|
|
|
2371
2538
|
op: zod.z.enum(["sum", "count"]),
|
|
2372
2539
|
column: zod.z.string().trim().min(1).optional(),
|
|
2373
2540
|
filter: WhenZ.optional()
|
|
2374
|
-
}).refine((spec) =>
|
|
2541
|
+
}).refine((spec) => require_itemId.isSafeSlug(spec.from), slugMessage("from")).refine((spec) => spec.op === "sum" === (spec.column !== void 0), {
|
|
2375
2542
|
message: "a rollup's `column` names the source column to aggregate: required for op \"sum\", meaningless for op \"count\"",
|
|
2376
2543
|
path: ["column"]
|
|
2377
2544
|
});
|
|
@@ -2545,9 +2712,9 @@ var SpawnZ = zod.z.object({
|
|
|
2545
2712
|
* array) only when the response body isn't itself the array; rss/atom yield
|
|
2546
2713
|
* items natively and ignore it — so no kind-specific requirement here. */
|
|
2547
2714
|
var DeclarativeIngestZ = zod.z.object({
|
|
2548
|
-
kind: zod.z.enum(
|
|
2715
|
+
kind: zod.z.enum(require_itemId.INGEST_KINDS),
|
|
2549
2716
|
url: zod.z.url(),
|
|
2550
|
-
schedule: zod.z.enum(
|
|
2717
|
+
schedule: zod.z.enum(require_itemId.FEED_SCHEDULES),
|
|
2551
2718
|
atHour: zod.z.number().int().min(0).max(23).optional(),
|
|
2552
2719
|
itemsAt: zod.z.string().trim().min(1).optional(),
|
|
2553
2720
|
map: zod.z.record(zod.z.string().trim().min(1), zod.z.string().trim().min(1)),
|
|
@@ -2561,8 +2728,8 @@ var DeclarativeIngestZ = zod.z.object({
|
|
|
2561
2728
|
* validated the SAME way an action's template is (safe path under
|
|
2562
2729
|
* `templates/`), so the skill-bridge mirrors it identically. */
|
|
2563
2730
|
var AgentIngestZ = zod.z.object({
|
|
2564
|
-
kind: zod.z.literal(
|
|
2565
|
-
schedule: zod.z.enum(
|
|
2731
|
+
kind: zod.z.literal(require_itemId.AGENT_INGEST_KIND),
|
|
2732
|
+
schedule: zod.z.enum(require_itemId.FEED_SCHEDULES),
|
|
2566
2733
|
atHour: zod.z.number().int().min(0).max(23).optional(),
|
|
2567
2734
|
role: zod.z.string().trim().min(1),
|
|
2568
2735
|
template: zod.z.string().trim().min(1).refine(require_templatePath.isSafeActionTemplatePath, "must be a safe path under `templates/` (e.g. `templates/refresh.md`; no `..`, no leading `/`, no backslash)")
|
|
@@ -2975,6 +3142,10 @@ function acceptStorageSchema(storage, opts) {
|
|
|
2975
3142
|
storageFile
|
|
2976
3143
|
};
|
|
2977
3144
|
}
|
|
3145
|
+
if (!hostSupportsSharedCollections()) return {
|
|
3146
|
+
ok: false,
|
|
3147
|
+
reason: "this host does not support shared collections — they live in a project repository, not a managed workspace"
|
|
3148
|
+
};
|
|
2978
3149
|
const manifest = loadAppManifest(opts.workspaceRoot);
|
|
2979
3150
|
if (!manifest.ok) return {
|
|
2980
3151
|
ok: false,
|
|
@@ -3325,6 +3496,12 @@ Object.defineProperty(exports, "getWorkspaceRoot", {
|
|
|
3325
3496
|
return getWorkspaceRoot;
|
|
3326
3497
|
}
|
|
3327
3498
|
});
|
|
3499
|
+
Object.defineProperty(exports, "hostSupportsSharedCollections", {
|
|
3500
|
+
enumerable: true,
|
|
3501
|
+
get: function() {
|
|
3502
|
+
return hostSupportsSharedCollections;
|
|
3503
|
+
}
|
|
3504
|
+
});
|
|
3328
3505
|
Object.defineProperty(exports, "isBackendUnavailable", {
|
|
3329
3506
|
enumerable: true,
|
|
3330
3507
|
get: function() {
|
|
@@ -3499,6 +3676,12 @@ Object.defineProperty(exports, "setFirestoreAccessor", {
|
|
|
3499
3676
|
return setFirestoreAccessor;
|
|
3500
3677
|
}
|
|
3501
3678
|
});
|
|
3679
|
+
Object.defineProperty(exports, "setSharedCollectionsSupport", {
|
|
3680
|
+
enumerable: true,
|
|
3681
|
+
get: function() {
|
|
3682
|
+
return setSharedCollectionsSupport;
|
|
3683
|
+
}
|
|
3684
|
+
});
|
|
3502
3685
|
Object.defineProperty(exports, "sharedCollectionChangePayload", {
|
|
3503
3686
|
enumerable: true,
|
|
3504
3687
|
get: function() {
|
|
@@ -3548,4 +3731,4 @@ Object.defineProperty(exports, "writeItem", {
|
|
|
3548
3731
|
}
|
|
3549
3732
|
});
|
|
3550
3733
|
|
|
3551
|
-
//# sourceMappingURL=discovery-
|
|
3734
|
+
//# sourceMappingURL=discovery-BsXiDZBR.cjs.map
|