@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
|
@@ -2,9 +2,9 @@ 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
4
|
const require_relPath = require("./relPath-CTAjGdCL.cjs");
|
|
5
|
-
const
|
|
6
|
-
const require_promptSafety = require("./promptSafety-
|
|
7
|
-
const require_discovery = require("./discovery-
|
|
5
|
+
const require_itemId = require("./itemId-Dzrr3vs5.cjs");
|
|
6
|
+
const require_promptSafety = require("./promptSafety-CWqYq-NS.cjs");
|
|
7
|
+
const require_discovery = require("./discovery-BsXiDZBR.cjs");
|
|
8
8
|
const require_feeds_paths = require("./feeds/paths.cjs");
|
|
9
9
|
const require_skill_bridge_index = require("./skill-bridge/index.cjs");
|
|
10
10
|
let node_path = require("node:path");
|
|
@@ -12,6 +12,665 @@ node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
|
12
12
|
let node_crypto = require("node:crypto");
|
|
13
13
|
let node_fs_promises = require("node:fs/promises");
|
|
14
14
|
let zod = require("zod");
|
|
15
|
+
//#region src/collection/server/publishManifest.ts
|
|
16
|
+
/** A collection id / app id, held to the one name rule (`SAFE_SLUG_PATTERN`)
|
|
17
|
+
* that `sharedCollectionKey` applies. Stated once so a path built later
|
|
18
|
+
* cannot be a way around it. */
|
|
19
|
+
var NameZ = zod.z.string().refine(require_itemId.isValidCollectionName, { message: "is not a valid id (letters, digits, '-' and '_' only)" });
|
|
20
|
+
/** An address on the roster. Not validated as an email beyond "has an @":
|
|
21
|
+
* the rules compare it to `request.auth.token.email` verbatim, so any
|
|
22
|
+
* narrowing here would refuse addresses Firebase itself accepts. */
|
|
23
|
+
var EmailZ = zod.z.string().trim().min(3).includes("@");
|
|
24
|
+
/** The four roles the deployed rules understand. `participant` is the layer
|
|
25
|
+
* that is NAMED but reads only its own rows — see `readerOf` vs `listedIn`. */
|
|
26
|
+
var APP_ROLES = [
|
|
27
|
+
"owner",
|
|
28
|
+
"editor",
|
|
29
|
+
"viewer",
|
|
30
|
+
"participant"
|
|
31
|
+
];
|
|
32
|
+
var RoleZ = zod.z.enum(APP_ROLES);
|
|
33
|
+
/** `{ email: { "*" | cid: role } }`. The `"*"` key is the app-wide role; a
|
|
34
|
+
* member may hold per-collection roles only (the stylist who is editor of
|
|
35
|
+
* bookings and viewer of everything else). */
|
|
36
|
+
var MembersZ = zod.z.record(EmailZ, zod.z.record(zod.z.union([zod.z.literal("*"), NameZ]), RoleZ));
|
|
37
|
+
/** The declarative mail queue, as the rules re-derive it: a transition of the
|
|
38
|
+
* status field, a recipient read off the RECORD, and a fixed template. */
|
|
39
|
+
var MailZ = zod.z.object({
|
|
40
|
+
toField: zod.z.string().trim().min(1),
|
|
41
|
+
on: zod.z.record(zod.z.string().trim().min(1), zod.z.object({
|
|
42
|
+
from: zod.z.array(zod.z.string().trim().min(1)).min(1),
|
|
43
|
+
to: zod.z.string().trim().min(1)
|
|
44
|
+
}).strict()),
|
|
45
|
+
dataFields: zod.z.array(zod.z.string().trim().min(1)).optional()
|
|
46
|
+
}).strict();
|
|
47
|
+
/** What the rules read out of `collections[cid]`. NOT the schema — the schema
|
|
48
|
+
* is published beside it, untouched, for clients to render from. */
|
|
49
|
+
var CollectionConfigZ = zod.z.object({
|
|
50
|
+
statusField: zod.z.string().trim().min(1).optional(),
|
|
51
|
+
/** `{ initial: [...], <status>: [<status>...] }`. Binds writers too, and
|
|
52
|
+
* binds `create` — that is the point of publishing it. */
|
|
53
|
+
transitions: zod.z.record(zod.z.string().trim().min(1), zod.z.array(zod.z.string().trim().min(1))).optional(),
|
|
54
|
+
immutable: zod.z.boolean().optional(),
|
|
55
|
+
submitOnly: zod.z.boolean().optional(),
|
|
56
|
+
peerVisibility: zod.z.enum(["public", "hidden"]).optional(),
|
|
57
|
+
revealGated: zod.z.boolean().optional(),
|
|
58
|
+
gatedFrom: NameZ.optional(),
|
|
59
|
+
revealBy: zod.z.string().trim().min(1).optional(),
|
|
60
|
+
mail: MailZ.optional(),
|
|
61
|
+
/** Which fields an aggregate groups by. Declared here rather than in the
|
|
62
|
+
* schema for the same reason as everything else in this file — the schema
|
|
63
|
+
* has no `aggregate` key yet — and it is here at all because the
|
|
64
|
+
* invariant that guards it ("every aggregation key is a CHECKED field")
|
|
65
|
+
* is about `public.submit`, which is an app-level declaration. Published
|
|
66
|
+
* as-is; the rules never read it. */
|
|
67
|
+
aggregate: zod.z.object({ by: zod.z.array(zod.z.string().trim().min(1)).min(1) }).strict().optional()
|
|
68
|
+
}).strict();
|
|
69
|
+
/** An authored submit window. ISO strings, because `app.json` is JSON and a
|
|
70
|
+
* Firestore `Timestamp` has no JSON form. Publish lowers it to epoch millis —
|
|
71
|
+
* the rules do not coerce strings, so an ISO string reaching Firestore is a
|
|
72
|
+
* type error that fails CLOSED (`inWindow` refuses every submission and the
|
|
73
|
+
* author sees "nobody can submit", not an error). */
|
|
74
|
+
var WindowZ = zod.z.object({
|
|
75
|
+
from: zod.z.iso.datetime().optional(),
|
|
76
|
+
until: zod.z.iso.datetime().optional()
|
|
77
|
+
}).strict();
|
|
78
|
+
var ValidateZ = zod.z.object({
|
|
79
|
+
required: zod.z.array(zod.z.string().trim().min(1)).optional(),
|
|
80
|
+
/** Capped at two by the rules themselves: rules have no iteration, so
|
|
81
|
+
* `keyFieldsOk` is unrolled. A third would be accepted here and silently
|
|
82
|
+
* unchecked there. */
|
|
83
|
+
keyFields: zod.z.array(zod.z.object({
|
|
84
|
+
field: zod.z.string().trim().min(1),
|
|
85
|
+
values: zod.z.array(zod.z.union([
|
|
86
|
+
zod.z.string(),
|
|
87
|
+
zod.z.number(),
|
|
88
|
+
zod.z.boolean()
|
|
89
|
+
])).min(1)
|
|
90
|
+
}).strict()).optional()
|
|
91
|
+
}).strict();
|
|
92
|
+
var SubmitZ = zod.z.object({
|
|
93
|
+
auth: zod.z.enum([
|
|
94
|
+
"none",
|
|
95
|
+
"anonymous",
|
|
96
|
+
"verifiedEmail"
|
|
97
|
+
]),
|
|
98
|
+
emailField: zod.z.string().trim().min(1).optional(),
|
|
99
|
+
createFields: zod.z.array(zod.z.string().trim().min(1)).min(1),
|
|
100
|
+
initialStatus: zod.z.string().trim().min(1).optional(),
|
|
101
|
+
idFrom: zod.z.enum([
|
|
102
|
+
"auto",
|
|
103
|
+
"auth.uid",
|
|
104
|
+
"auth.uid+field"
|
|
105
|
+
]).optional(),
|
|
106
|
+
idField: zod.z.string().trim().min(1).optional(),
|
|
107
|
+
validate: ValidateZ.optional(),
|
|
108
|
+
window: WindowZ.optional(),
|
|
109
|
+
/** Per CURRENT STATUS, never a flat list: a flat list lets a customer move
|
|
110
|
+
* an approved booking's `startAt` without anyone re-approving it. */
|
|
111
|
+
selfUpdate: zod.z.record(zod.z.string().trim().min(1), zod.z.array(zod.z.string().trim().min(1))).optional(),
|
|
112
|
+
selfTransitions: zod.z.record(zod.z.string().trim().min(1), zod.z.array(zod.z.string().trim().min(1))).optional(),
|
|
113
|
+
finalize: zod.z.boolean().optional(),
|
|
114
|
+
audience: zod.z.literal("participant").optional(),
|
|
115
|
+
gateOn: zod.z.object({
|
|
116
|
+
phase: zod.z.string().trim().min(1),
|
|
117
|
+
match: zod.z.string().trim().min(1)
|
|
118
|
+
}).strict().optional()
|
|
119
|
+
}).strict();
|
|
120
|
+
var PublicZ = zod.z.object({
|
|
121
|
+
/** The master switch. Anonymous submission (`auth: "none"`) needs it as
|
|
122
|
+
* well as its own declaration. */
|
|
123
|
+
enabled: zod.z.boolean().optional(),
|
|
124
|
+
read: zod.z.array(NameZ).optional(),
|
|
125
|
+
submit: zod.z.record(NameZ, SubmitZ).optional()
|
|
126
|
+
}).strict();
|
|
127
|
+
/** The whole authored declaration.
|
|
128
|
+
*
|
|
129
|
+
* `owner` is accepted but is NOT the published value — publish stamps the
|
|
130
|
+
* publisher's uid (or carries the existing one forward, which is what the
|
|
131
|
+
* rules require on update) and refuses a declaration that disagrees. It is
|
|
132
|
+
* accepted rather than banned because the sample app.json in the design note
|
|
133
|
+
* shows it, and a hard refusal on a key the samples contain would be a worse
|
|
134
|
+
* first experience than a message naming the mismatch. */
|
|
135
|
+
var AuthoredAppZ = zod.z.object({
|
|
136
|
+
aid: NameZ,
|
|
137
|
+
name: zod.z.string().trim().min(1).optional(),
|
|
138
|
+
/** Per-worktree app id (design D6, implementation order 7). Accepted so a
|
|
139
|
+
* repository already carrying it parses; nothing reads it yet. */
|
|
140
|
+
aidEnv: zod.z.string().trim().min(1).optional(),
|
|
141
|
+
owner: zod.z.string().trim().min(1).optional(),
|
|
142
|
+
members: MembersZ,
|
|
143
|
+
collections: zod.z.record(NameZ, CollectionConfigZ).optional(),
|
|
144
|
+
participantRead: zod.z.array(NameZ).optional(),
|
|
145
|
+
public: PublicZ.optional()
|
|
146
|
+
}).strict();
|
|
147
|
+
/** Parse the authored declaration out of `app.json`'s text.
|
|
148
|
+
*
|
|
149
|
+
* Returns a LIST of problems rather than throwing, for the same reason
|
|
150
|
+
* `loadAppManifest` returns a failure: the caller is a gate whose entire job
|
|
151
|
+
* is to hand the author something to act on. Every problem is reported at
|
|
152
|
+
* once — publish is a manual step, and a parser that stops at the first key
|
|
153
|
+
* makes it N round trips. */
|
|
154
|
+
function parseAuthoredApp(raw) {
|
|
155
|
+
const manifest = require_discovery.parseAppManifest(raw);
|
|
156
|
+
if (!manifest.ok) return {
|
|
157
|
+
ok: false,
|
|
158
|
+
problems: [manifest.kind === "missing" ? "app.json is missing" : manifest.detail]
|
|
159
|
+
};
|
|
160
|
+
const parsed = AuthoredAppZ.safeParse(JSON.parse(raw));
|
|
161
|
+
if (!parsed.success) return {
|
|
162
|
+
ok: false,
|
|
163
|
+
problems: authoredProblems(parsed.error)
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
ok: true,
|
|
167
|
+
app: parsed.data
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** zod issues as one actionable line each: `public.submit.responses.auth: …`. */
|
|
171
|
+
function authoredProblems(error) {
|
|
172
|
+
return error.issues.map((issue) => {
|
|
173
|
+
return `${issue.path.length > 0 ? issue.path.join(".") : "app.json"}: ${issue.message}`;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/collection/server/publishProject.ts
|
|
178
|
+
/** The document id under `apps/{aid}/config`. One document, named, rather than
|
|
179
|
+
* a spread of them: a second public document is a second thing to keep in
|
|
180
|
+
* step, and nothing yet needs one. */
|
|
181
|
+
var PUBLIC_CONFIG_DOC = "public";
|
|
182
|
+
/** Drop keys whose value is `undefined`. Firestore rejects an undefined field
|
|
183
|
+
* value outright, and `"k" in c` — which every optional key in the rules is
|
|
184
|
+
* read through — must mean "the author declared it". */
|
|
185
|
+
function compact(entries) {
|
|
186
|
+
return Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== void 0));
|
|
187
|
+
}
|
|
188
|
+
/** ISO → epoch millis, the one conversion the rules cannot do for themselves.
|
|
189
|
+
* The caller has already refused an unparseable string (the authored parser
|
|
190
|
+
* requires `z.iso.datetime()`), so a NaN here would be a programming error;
|
|
191
|
+
* it is still checked, because a NaN written to Firestore fails closed in the
|
|
192
|
+
* same silent way an ISO string does. */
|
|
193
|
+
function windowMillis(window) {
|
|
194
|
+
if (!window) return void 0;
|
|
195
|
+
const out = {};
|
|
196
|
+
if (window.from !== void 0) out.fromMs = Date.parse(window.from);
|
|
197
|
+
if (window.until !== void 0) out.untilMs = Date.parse(window.until);
|
|
198
|
+
if (Object.values(out).some((value) => !Number.isFinite(value))) throw new Error(`publish: window bound is not a parseable timestamp (${JSON.stringify(window)})`);
|
|
199
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
200
|
+
}
|
|
201
|
+
/** One `public.submit[cid]`, with its window lowered. Everything else passes
|
|
202
|
+
* through: the rules read these keys by the names the author wrote. */
|
|
203
|
+
function projectSubmit(submit) {
|
|
204
|
+
const { window, ...rest } = submit;
|
|
205
|
+
return compact({
|
|
206
|
+
...rest,
|
|
207
|
+
window: windowMillis(window)
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/** The roster's addresses as a set, in a stable order.
|
|
211
|
+
*
|
|
212
|
+
* Sorted so two publishes of the same declaration produce the same document
|
|
213
|
+
* — idempotence is a property this step is tested for, and Firestore compares
|
|
214
|
+
* arrays by ORDER. `membersConsistent()` compares as sets and would accept
|
|
215
|
+
* any order; the test that would notice is the one asserting a second publish
|
|
216
|
+
* changes nothing but `publishedAt`. */
|
|
217
|
+
function memberEmailsOf(members) {
|
|
218
|
+
return Object.keys(members).sort();
|
|
219
|
+
}
|
|
220
|
+
/** The previous document, kept for rollback, with its OWN `previousPublished`
|
|
221
|
+
* stripped.
|
|
222
|
+
*
|
|
223
|
+
* One level, deliberately. Chaining would make every publish carry the entire
|
|
224
|
+
* history of the app inside a single document, which grows without bound and
|
|
225
|
+
* meets Firestore's 1 MiB document limit as a permission-shaped failure at
|
|
226
|
+
* some unpredictable publish. One level answers the question rollback
|
|
227
|
+
* actually asks — "put back what was there before I broke it" — and the
|
|
228
|
+
* further history is in git, which is where the declaration came from. */
|
|
229
|
+
function previousOf(existing) {
|
|
230
|
+
if (!existing) return void 0;
|
|
231
|
+
const { previousPublished: __dropped, ...rest } = existing;
|
|
232
|
+
return rest;
|
|
233
|
+
}
|
|
234
|
+
/** Project the authored declaration into the documents publish writes.
|
|
235
|
+
*
|
|
236
|
+
* `existing` is the app document as it is in Firestore right now, or null on
|
|
237
|
+
* a first publish. Two things need it, both required by the rules:
|
|
238
|
+
* - `owner` must be UNCHANGED on update. Re-stamping the publisher's uid
|
|
239
|
+
* would be refused for any app whose owner ever signed in as a different
|
|
240
|
+
* account, and would silently transfer ownership if it were not.
|
|
241
|
+
* - `previousPublished` is that document, so a rollback has something to
|
|
242
|
+
* put back.
|
|
243
|
+
*
|
|
244
|
+
* Pure: no clock, no filesystem, no Firestore. Everything variable arrives as
|
|
245
|
+
* a parameter, which is what makes the conversion table testable as a table. */
|
|
246
|
+
function projectApp(authored, schemas, stamp, existing) {
|
|
247
|
+
const owner = typeof existing?.owner === "string" ? existing.owner : stamp.uid;
|
|
248
|
+
const submit = Object.fromEntries(Object.entries(authored.public?.submit ?? {}).map(([cid, spec]) => [cid, projectSubmit(spec)]));
|
|
249
|
+
const publicBlock = authored.public ? compact({
|
|
250
|
+
enabled: authored.public.enabled,
|
|
251
|
+
read: authored.public.read,
|
|
252
|
+
submit: Object.keys(submit).length > 0 ? submit : void 0
|
|
253
|
+
}) : void 0;
|
|
254
|
+
const app = compact({
|
|
255
|
+
aid: authored.aid,
|
|
256
|
+
name: authored.name,
|
|
257
|
+
owner,
|
|
258
|
+
members: authored.members,
|
|
259
|
+
memberEmails: memberEmailsOf(authored.members),
|
|
260
|
+
collections: authored.collections,
|
|
261
|
+
participantRead: authored.participantRead,
|
|
262
|
+
public: publicBlock,
|
|
263
|
+
publishedAt: stamp.publishedAt,
|
|
264
|
+
publishedBy: stamp.email,
|
|
265
|
+
publishedCommit: stamp.commit,
|
|
266
|
+
publishedDirty: stamp.dirty === true ? true : void 0,
|
|
267
|
+
previousPublished: previousOf(existing)
|
|
268
|
+
});
|
|
269
|
+
const config = {
|
|
270
|
+
enabled: authored.public?.enabled === true,
|
|
271
|
+
read: authored.public?.read ?? [],
|
|
272
|
+
submit,
|
|
273
|
+
publishedAt: stamp.publishedAt
|
|
274
|
+
};
|
|
275
|
+
if (authored.name !== void 0) config.name = authored.name;
|
|
276
|
+
return {
|
|
277
|
+
app,
|
|
278
|
+
schemas: schemas.map(({ cid, schema }) => ({
|
|
279
|
+
cid,
|
|
280
|
+
doc: schemaDoc(schema, stamp)
|
|
281
|
+
})),
|
|
282
|
+
config
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** Keys on the app document that publish owns: what is PUBLIC right now, and
|
|
286
|
+
* the rule-facing configuration anonymous access is judged against. Deploy
|
|
287
|
+
* carries them through from the existing document and never authors them. */
|
|
288
|
+
var PUBLISH_OWNED_KEYS = [
|
|
289
|
+
"public",
|
|
290
|
+
"collections",
|
|
291
|
+
"participantRead",
|
|
292
|
+
"publishedAt",
|
|
293
|
+
"publishedBy",
|
|
294
|
+
"publishedCommit",
|
|
295
|
+
"publishedDirty",
|
|
296
|
+
"previousPublished"
|
|
297
|
+
];
|
|
298
|
+
var isPublishOwned = (key) => PUBLISH_OWNED_KEYS.includes(key);
|
|
299
|
+
function projectDeploy(authored, schemas, stamp, existing) {
|
|
300
|
+
const { app } = projectApp(authored, schemas, stamp, existing);
|
|
301
|
+
const deployed = Object.fromEntries(Object.entries(app).filter(([key]) => !isPublishOwned(key)));
|
|
302
|
+
for (const key of PUBLISH_OWNED_KEYS) {
|
|
303
|
+
const live = existing?.[key];
|
|
304
|
+
if (live !== void 0) deployed[key] = live;
|
|
305
|
+
}
|
|
306
|
+
deployed.deployedAt = stamp.publishedAt;
|
|
307
|
+
deployed.deployedBy = stamp.email;
|
|
308
|
+
if (stamp.commit !== void 0) deployed.deployedCommit = stamp.commit;
|
|
309
|
+
return {
|
|
310
|
+
app: deployed,
|
|
311
|
+
staging: schemas.map(({ cid, schema }) => ({
|
|
312
|
+
cid,
|
|
313
|
+
doc: stagedDoc(schema, stamp, authored, cid)
|
|
314
|
+
}))
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** One staged schema document, carrying this cid's rule-facing configuration
|
|
318
|
+
* alongside the schema so publish can promote them together. */
|
|
319
|
+
function stagedDoc(schema, stamp, authored, cid) {
|
|
320
|
+
const doc = {
|
|
321
|
+
publishedSchema: schema,
|
|
322
|
+
deployedAt: stamp.publishedAt,
|
|
323
|
+
deployedBy: stamp.email
|
|
324
|
+
};
|
|
325
|
+
const config = authored.collections?.[cid];
|
|
326
|
+
if (config !== void 0) doc.config = config;
|
|
327
|
+
if (authored.participantRead?.includes(cid) === true) doc.participantRead = true;
|
|
328
|
+
if (stamp.commit !== void 0) doc.deployedCommit = stamp.commit;
|
|
329
|
+
return doc;
|
|
330
|
+
}
|
|
331
|
+
/** The rule-facing configuration to promote, read from the STAGED documents
|
|
332
|
+
* rather than from the manifest as it reads right now.
|
|
333
|
+
*
|
|
334
|
+
* Otherwise: deploy revision A, edit `app.json` to revision B, publish — and
|
|
335
|
+
* the promoted schema is A's while the authorization behaviour is B's, a
|
|
336
|
+
* combination nobody exercised through `/staging/{aid}`.
|
|
337
|
+
*
|
|
338
|
+
* (`public` is deliberately NOT part of this: it is not staged, because it is
|
|
339
|
+
* the decision being made AT publish rather than something under test.) */
|
|
340
|
+
function stagedRuleConfig(staged) {
|
|
341
|
+
const entries = [];
|
|
342
|
+
const participantRead = [];
|
|
343
|
+
for (const entry of staged) {
|
|
344
|
+
const { config } = entry.doc;
|
|
345
|
+
if (config !== void 0) entries.push([entry.cid, config]);
|
|
346
|
+
if (entry.doc.participantRead === true) participantRead.push(entry.cid);
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
collections: entries.length > 0 ? Object.fromEntries(entries) : void 0,
|
|
350
|
+
participantRead: participantRead.length > 0 ? participantRead : void 0
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function projectPublish(authored, staged, stamp, existing) {
|
|
354
|
+
const { app, config } = projectApp(authored, [], stamp, existing);
|
|
355
|
+
const staging = stagedRuleConfig(staged);
|
|
356
|
+
app.collections = staging.collections;
|
|
357
|
+
app.participantRead = staging.participantRead;
|
|
358
|
+
const published = Object.fromEntries(Object.entries(existing ?? app).filter(([key]) => !isPublishOwned(key)));
|
|
359
|
+
for (const key of PUBLISH_OWNED_KEYS) if (key !== "public" && app[key] !== void 0) published[key] = app[key];
|
|
360
|
+
const publicBlock = app.public;
|
|
361
|
+
return {
|
|
362
|
+
app: published,
|
|
363
|
+
config,
|
|
364
|
+
public: require_dist.isRecord(publicBlock) ? publicBlock : void 0
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/** Re-stamp a staged schema document as it is promoted to `collections/{cid}`.
|
|
368
|
+
*
|
|
369
|
+
* The stamp answers "which version is PUBLIC right now, and who made it so",
|
|
370
|
+
* so it is written by the operation that changes the answer — publish — not
|
|
371
|
+
* carried over from the deploy that staged it. */
|
|
372
|
+
function promoteSchema(staged, stamp) {
|
|
373
|
+
const doc = {
|
|
374
|
+
publishedSchema: staged.publishedSchema,
|
|
375
|
+
publishedAt: stamp.publishedAt,
|
|
376
|
+
publishedBy: stamp.email
|
|
377
|
+
};
|
|
378
|
+
if (stamp.commit !== void 0) doc.publishedCommit = stamp.commit;
|
|
379
|
+
return doc;
|
|
380
|
+
}
|
|
381
|
+
/** One published schema document. Written key by key rather than through
|
|
382
|
+
* `compact`, so the declared type is the type — an optional commit is the
|
|
383
|
+
* only variable part. */
|
|
384
|
+
function schemaDoc(schema, stamp) {
|
|
385
|
+
const doc = {
|
|
386
|
+
publishedSchema: schema,
|
|
387
|
+
publishedAt: stamp.publishedAt,
|
|
388
|
+
publishedBy: stamp.email
|
|
389
|
+
};
|
|
390
|
+
if (stamp.commit !== void 0) doc.publishedCommit = stamp.commit;
|
|
391
|
+
return doc;
|
|
392
|
+
}
|
|
393
|
+
/** The app documents' parent path — the `FirestoreDocs` seam takes a
|
|
394
|
+
* collection path plus a document id, and the app document's id is the aid. */
|
|
395
|
+
var APPS_COLLECTION = "apps";
|
|
396
|
+
/** The collection (schema) documents' parent path — what the PUBLIC page
|
|
397
|
+
* reads, written only by publish (promotion). */
|
|
398
|
+
var appSchemasPath = (aid) => `apps/${aid}/collections`;
|
|
399
|
+
/** The URL-slug reservations — `appSlugs/{slug}` → `{ aid, published }`.
|
|
400
|
+
*
|
|
401
|
+
* A TOP-LEVEL collection, not a field on the app: the public page resolves a
|
|
402
|
+
* slug to an aid BEFORE it can read anything under `apps/{aid}`, and a slug
|
|
403
|
+
* has to be claimable atomically (create-if-absent) so two apps cannot hold
|
|
404
|
+
* the same URL.
|
|
405
|
+
*
|
|
406
|
+
* `published` is what makes the reservation invisible until publish. The slug
|
|
407
|
+
* is human-readable, so a readable reservation would let anyone guess the URL
|
|
408
|
+
* and get the aid — and the aid is the `/staging/{aid}` entrance. The rule is
|
|
409
|
+
* `allow read: if resource.data.published == true`, which needs no `get()` and
|
|
410
|
+
* so costs nothing against the rules' expression budget. */
|
|
411
|
+
var APP_SLUGS_COLLECTION = "appSlugs";
|
|
412
|
+
var appSlugDoc = (aid, published) => ({
|
|
413
|
+
aid,
|
|
414
|
+
published
|
|
415
|
+
});
|
|
416
|
+
/** The staged schema documents' parent path — what `/staging/{aid}` reads,
|
|
417
|
+
* written by deploy. A separate DOCUMENT rather than a field beside
|
|
418
|
+
* `publishedSchema`, because the rules cannot hide a field: anything inside a
|
|
419
|
+
* document the public page may read is public. */
|
|
420
|
+
var appStagingPath = (aid) => `apps/${aid}/staging`;
|
|
421
|
+
/** The public-config documents' parent path. */
|
|
422
|
+
var appConfigPath = (aid) => `apps/${aid}/config`;
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/collection/server/publishChecks.ts
|
|
425
|
+
/** Does this submit declaration bind a record to the submitter's identity?
|
|
426
|
+
*
|
|
427
|
+
* The condition for requiring `submitOnly`, and deliberately NOT "declares an
|
|
428
|
+
* `audience`": `audience` appears only in the rules' public-create branch, so
|
|
429
|
+
* an owner or editor never meets it and can add records freely. `immutable`
|
|
430
|
+
* is the wrong condition too — a survey's responses are not immutable and
|
|
431
|
+
* can be padded exactly the same way.
|
|
432
|
+
*
|
|
433
|
+
* What these four have in common is that each one makes the record MEAN "the
|
|
434
|
+
* person who submitted it said this": a per-uid id, a per-uid+field id, a
|
|
435
|
+
* row stamped with the submitter's verified address, or a submission
|
|
436
|
+
* restricted to a named participant. A record created through the writer
|
|
437
|
+
* branch carries the same shape and none of that meaning. */
|
|
438
|
+
function bindsSubmitterIdentity(submit) {
|
|
439
|
+
return submit.idFrom === "auth.uid" || submit.idFrom === "auth.uid+field" || submit.emailField !== void 0 || submit.audience === "participant";
|
|
440
|
+
}
|
|
441
|
+
/** The fields a rule actually CHECKS the value of, for one collection.
|
|
442
|
+
*
|
|
443
|
+
* `keyFields` pins a value against a declared set, `gateOn.match` pins it
|
|
444
|
+
* against the session's current question, and the status field is pinned by
|
|
445
|
+
* the transition machine. An aggregation grouped by anything else is grouped
|
|
446
|
+
* by a field any submitter may write anything into — so the published
|
|
447
|
+
* aggregate is whatever the noisiest respondent decided it should be. */
|
|
448
|
+
function checkedFields(collection, submit) {
|
|
449
|
+
const fields = /* @__PURE__ */ new Set();
|
|
450
|
+
for (const keyField of submit?.validate?.keyFields ?? []) fields.add(keyField.field);
|
|
451
|
+
if (submit?.gateOn) fields.add(submit.gateOn.match);
|
|
452
|
+
if (collection?.statusField) fields.add(collection.statusField);
|
|
453
|
+
return fields;
|
|
454
|
+
}
|
|
455
|
+
/** INVARIANT 1 — a submission bound to its submitter needs `submitOnly`. */
|
|
456
|
+
function submitOnlyProblems(app) {
|
|
457
|
+
const problems = [];
|
|
458
|
+
for (const [cid, submit] of Object.entries(app.public?.submit ?? {})) {
|
|
459
|
+
if (!bindsSubmitterIdentity(submit)) continue;
|
|
460
|
+
if (app.collections?.[cid]?.submitOnly === true) continue;
|
|
461
|
+
problems.push(`collections.${cid}.submitOnly must be true: public.submit.${cid} binds each record to its submitter (${identityBindings(submit).join(", ")}), so a record created any other way would carry that meaning without having earned it. Without submitOnly the rules let an owner or editor write rows directly into ${cid}.`);
|
|
462
|
+
}
|
|
463
|
+
return problems;
|
|
464
|
+
}
|
|
465
|
+
function identityBindings(submit) {
|
|
466
|
+
const bindings = [];
|
|
467
|
+
if (submit.idFrom === "auth.uid" || submit.idFrom === "auth.uid+field") bindings.push(`idFrom: "${submit.idFrom}"`);
|
|
468
|
+
if (submit.emailField !== void 0) bindings.push(`emailField: "${submit.emailField}"`);
|
|
469
|
+
if (submit.audience === "participant") bindings.push(`audience: "participant"`);
|
|
470
|
+
return bindings;
|
|
471
|
+
}
|
|
472
|
+
/** INVARIANT 2 — every aggregation key is a field some rule checks. */
|
|
473
|
+
function aggregateProblems(app) {
|
|
474
|
+
const problems = [];
|
|
475
|
+
for (const [cid, collection] of Object.entries(app.collections ?? {})) {
|
|
476
|
+
const keys = collection.aggregate?.by;
|
|
477
|
+
if (!keys) continue;
|
|
478
|
+
const checked = checkedFields(collection, app.public?.submit?.[cid]);
|
|
479
|
+
const loose = keys.filter((field) => !checked.has(field));
|
|
480
|
+
const spelled = loose.map((field) => `'${field}'`).join(", ");
|
|
481
|
+
if (loose.length > 0) problems.push(`collections.${cid}.aggregate.by names ${spelled}, which no rule checks the value of. An aggregation key must appear in public.submit.${cid}.validate.keyFields, in gateOn.match, or be the statusField — otherwise a submitter chooses their own bucket and the published aggregate is not a count of anything.`);
|
|
482
|
+
}
|
|
483
|
+
return problems;
|
|
484
|
+
}
|
|
485
|
+
/** INVARIANT 3 — `auth: "verifiedEmail"` only.
|
|
486
|
+
*
|
|
487
|
+
* A product decision, not a rules limitation: the rules keep all three stages
|
|
488
|
+
* and the emulator tests keep exercising them, because deleting a stage from
|
|
489
|
+
* the rules turns a change of mind into a cross-repo deploy. Publish is where
|
|
490
|
+
* the current decision is expressed, and it is one line to move. */
|
|
491
|
+
function authProblems(app) {
|
|
492
|
+
return Object.entries(app.public?.submit ?? {}).filter(([, submit]) => submit.auth !== "verifiedEmail").map(([cid, submit]) => `public.submit.${cid}.auth is "${submit.auth}": only "verifiedEmail" may be published. The rules still implement "none" and "anonymous" — this is a product decision, and lifting it is a change here, not a rules deploy.`);
|
|
493
|
+
}
|
|
494
|
+
/** INVARIANT 5 — a mail transition's origins and destination must be disjoint.
|
|
495
|
+
*
|
|
496
|
+
* Overlap means the same write can satisfy the same template twice over, and
|
|
497
|
+
* the deterministic mail id is the only other thing stopping a duplicate
|
|
498
|
+
* send. The rules also require the status to have CHANGED, so an overlapping
|
|
499
|
+
* declaration is not merely redundant: `from` containing `to` is a transition
|
|
500
|
+
* that can never fire, which is a mail nobody ever receives. */
|
|
501
|
+
function mailProblems(app) {
|
|
502
|
+
return Object.entries(app.collections ?? {}).flatMap(([cid, collection]) => collectionMailProblems(cid, collection));
|
|
503
|
+
}
|
|
504
|
+
function collectionMailProblems(cid, collection) {
|
|
505
|
+
const { mail } = collection;
|
|
506
|
+
if (!mail) return [];
|
|
507
|
+
const problems = [];
|
|
508
|
+
if (!collection.statusField) problems.push(`collections.${cid}.mail needs collections.${cid}.statusField: the rules read the status before and after the write to decide the mail is warranted.`);
|
|
509
|
+
for (const [template, transition] of Object.entries(mail.on)) problems.push(...templateMailProblems(cid, collection, template, transition));
|
|
510
|
+
return problems;
|
|
511
|
+
}
|
|
512
|
+
function templateMailProblems(cid, collection, template, transition) {
|
|
513
|
+
const problems = [];
|
|
514
|
+
if (transition.from.includes(transition.to)) problems.push(`collections.${cid}.mail.on.${template} lists "${transition.to}" in both \`from\` and \`to\`. The rules require the status to CHANGE in the same write, so this template can never send.`);
|
|
515
|
+
const allowed = collection.transitions;
|
|
516
|
+
if (allowed) {
|
|
517
|
+
const unreachable = transition.from.filter((from) => !(allowed[from] ?? []).includes(transition.to));
|
|
518
|
+
const spelled = unreachable.map((from) => `'${from}' -> '${transition.to}'`).join(", ");
|
|
519
|
+
if (unreachable.length > 0) problems.push(`collections.${cid}.mail.on.${template} sends on ${spelled}, which collections.${cid}.transitions does not allow. The record write is refused first, so the mail never fires.`);
|
|
520
|
+
}
|
|
521
|
+
return problems;
|
|
522
|
+
}
|
|
523
|
+
/** INVARIANTS 6 and 7 — the window is a real interval, and `keyFields` fits
|
|
524
|
+
* the unrolled check in the rules. */
|
|
525
|
+
function submitShapeProblems(app) {
|
|
526
|
+
return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => [...windowProblems(cid, submit), ...keyFieldCountProblems(cid, submit)]);
|
|
527
|
+
}
|
|
528
|
+
function windowProblems(cid, submit) {
|
|
529
|
+
const { window } = submit;
|
|
530
|
+
if (window?.from === void 0 || window.until === void 0) return [];
|
|
531
|
+
if (Date.parse(window.until) > Date.parse(window.from)) return [];
|
|
532
|
+
return [`public.submit.${cid}.window closes at or before it opens (${window.from} -> ${window.until}): nothing could ever be submitted.`];
|
|
533
|
+
}
|
|
534
|
+
function keyFieldCountProblems(cid, submit) {
|
|
535
|
+
const keyFields = submit.validate?.keyFields ?? [];
|
|
536
|
+
if (keyFields.length <= 2) return [];
|
|
537
|
+
return [`public.submit.${cid}.validate.keyFields declares ${keyFields.length}; the rules check at most 2. Rules have no iteration, so the check is unrolled — a third would be published and never enforced.`];
|
|
538
|
+
}
|
|
539
|
+
/** The fail-closed traps: declarations the rules read together, where the
|
|
540
|
+
* missing half denies every write instead of loosening one. */
|
|
541
|
+
function coherenceProblems(app) {
|
|
542
|
+
const fromSubmits = Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => submitCoherenceProblems(app, cid, submit));
|
|
543
|
+
const fromCollections = Object.entries(app.collections ?? {}).flatMap(([cid, collection]) => gateCoherenceProblems(cid, collection));
|
|
544
|
+
return [...fromSubmits, ...fromCollections];
|
|
545
|
+
}
|
|
546
|
+
/** `initialStatus` is read together with the collection's `statusField` and
|
|
547
|
+
* with `createFields`; miss either and every submission is refused. */
|
|
548
|
+
function statusCoherenceProblems(cid, submit, collection) {
|
|
549
|
+
if (submit.initialStatus === void 0) return [];
|
|
550
|
+
if (!collection?.statusField) return [`public.submit.${cid}.initialStatus needs collections.${cid}.statusField: the rules look the status up by that name, and refuse every submission without it.`];
|
|
551
|
+
if (new Set(submit.createFields).has(collection.statusField)) return [];
|
|
552
|
+
return [`public.submit.${cid}.createFields must include "${collection.statusField}": a submission may carry ONLY the createFields, and the rules also require the status field to be present and equal to initialStatus. As written, every submission is refused.`];
|
|
553
|
+
}
|
|
554
|
+
/** Every field a RULE reads off a submitted record, other than the status
|
|
555
|
+
* field (which `statusCoherenceProblems` words for itself).
|
|
556
|
+
*
|
|
557
|
+
* `emailField` and `idField` belong here for exactly the reason `required`
|
|
558
|
+
* and `keyFields` do, and forgetting them was the same oversight twice: the
|
|
559
|
+
* rules read `request.resource.data[s.emailField]` and rebuild the document
|
|
560
|
+
* id from `s.idField`, while `hasOnly(createFields)` decides what a
|
|
561
|
+
* submission may carry at all. A field in one list and not the other is a
|
|
562
|
+
* contradiction the submitter cannot resolve — including it is refused,
|
|
563
|
+
* omitting it fails the check. */
|
|
564
|
+
function ruleReadFields(submit) {
|
|
565
|
+
const fields = [];
|
|
566
|
+
if (submit.emailField !== void 0) fields.push({
|
|
567
|
+
field: submit.emailField,
|
|
568
|
+
why: `public.submit.<cid>.emailField — the rules compare it to the submitter's verified address`
|
|
569
|
+
});
|
|
570
|
+
if (submit.idFrom === "auth.uid+field" && submit.idField !== void 0) fields.push({
|
|
571
|
+
field: submit.idField,
|
|
572
|
+
why: `public.submit.<cid>.idField — the rules rebuild the document id from it`
|
|
573
|
+
});
|
|
574
|
+
return fields;
|
|
575
|
+
}
|
|
576
|
+
/** A checked field a submission is not allowed to carry can never be
|
|
577
|
+
* satisfied: carrying it fails `hasOnly`, omitting it fails the check. */
|
|
578
|
+
function createFieldProblems(cid, submit) {
|
|
579
|
+
const createFields = new Set(submit.createFields);
|
|
580
|
+
const ruleRead = ruleReadFields(submit).filter((entry) => !createFields.has(entry.field)).map((entry) => `public.submit.${cid}.createFields must include "${entry.field}" (${entry.why.replace("<cid>", cid)}): a submission may carry only the createFields, so as written every submission is refused whether or not it carries the field.`);
|
|
581
|
+
const required = (submit.validate?.required ?? []).filter((field) => !createFields.has(field)).map((field) => `public.submit.${cid}.validate.required names "${field}", which is not in createFields: a submission may carry only the createFields, so the requirement can never be met.`);
|
|
582
|
+
const keyFields = (submit.validate?.keyFields ?? []).filter((keyField) => !createFields.has(keyField.field)).map((keyField) => `public.submit.${cid}.validate.keyFields checks "${keyField.field}", which is not in createFields: a submission carrying it is refused, and one omitting it fails the check.`);
|
|
583
|
+
return [
|
|
584
|
+
...ruleRead,
|
|
585
|
+
...required,
|
|
586
|
+
...keyFields
|
|
587
|
+
];
|
|
588
|
+
}
|
|
589
|
+
function submitCoherenceProblems(app, cid, submit) {
|
|
590
|
+
const collection = app.collections?.[cid];
|
|
591
|
+
const problems = [...statusCoherenceProblems(cid, submit, collection), ...createFieldProblems(cid, submit)];
|
|
592
|
+
if (submit.idFrom === "auth.uid+field" && submit.idField === void 0) problems.push(`public.submit.${cid}.idFrom is "auth.uid+field" but no idField is declared: the rules rebuild the document id from that field and refuse every create.`);
|
|
593
|
+
if ((submit.selfUpdate !== void 0 || submit.selfTransitions !== void 0) && !collection?.statusField) problems.push(`public.submit.${cid}.selfUpdate / selfTransitions are declared per CURRENT STATUS, but collections.${cid} declares no statusField: the rules read the current status first and refuse every self-edit without it.`);
|
|
594
|
+
if (submit.audience === "participant" && Object.keys(app.members).length === 0) problems.push(`public.submit.${cid}.audience is "participant" but the roster is empty: the rules resolve the submitter's role from members, so every submission is refused.`);
|
|
595
|
+
return problems;
|
|
596
|
+
}
|
|
597
|
+
/** The staged reveal reads its flag off the PARENT record, so the path to that
|
|
598
|
+
* parent is not optional decoration — without it the gate never opens. */
|
|
599
|
+
function gateCoherenceProblems(cid, collection) {
|
|
600
|
+
if (collection.revealGated !== true) return [];
|
|
601
|
+
if (collection.gatedFrom !== void 0 && collection.revealBy !== void 0) return [];
|
|
602
|
+
return [`collections.${cid}.revealGated needs both gatedFrom and revealBy: the flag is read off the PARENT record, and without the path the gate never opens.`];
|
|
603
|
+
}
|
|
604
|
+
/** The publisher must be able to write what they are about to write.
|
|
605
|
+
*
|
|
606
|
+
* On a first publish the rules require the creator to name themselves owner,
|
|
607
|
+
* in the roster, under `'*'`. Getting this wrong produces a bare permission
|
|
608
|
+
* error from Firestore with nothing in it about rosters — worth one line
|
|
609
|
+
* here instead. */
|
|
610
|
+
function publisherProblems(app, publisherEmail) {
|
|
611
|
+
if (app.members[publisherEmail]?.["*"] === "owner") return [];
|
|
612
|
+
return [`members must give you app-wide owner: add "${publisherEmail}": { "*": "owner" }. The rules require the publisher to hold that role (and to name themselves owner when the app is first created); otherwise the write is refused with no explanation.`];
|
|
613
|
+
}
|
|
614
|
+
/** Every cid the declaration mentions must be a collection that exists.
|
|
615
|
+
*
|
|
616
|
+
* A typo'd cid is not an error anywhere else: the app document simply carries
|
|
617
|
+
* a configuration for a collection nobody publishes, and the collection the
|
|
618
|
+
* author meant is published with no configuration at all — i.e. with the
|
|
619
|
+
* status machine and the submit path silently absent. */
|
|
620
|
+
function unknownCidProblems(app, collections) {
|
|
621
|
+
const known = new Set(collections.map((collection) => collection.cid));
|
|
622
|
+
return [
|
|
623
|
+
["collections", Object.keys(app.collections ?? {})],
|
|
624
|
+
["public.read", app.public?.read ?? []],
|
|
625
|
+
["public.submit", Object.keys(app.public?.submit ?? {})],
|
|
626
|
+
["participantRead", app.participantRead ?? []]
|
|
627
|
+
].flatMap(([where, cids]) => cids.filter((cid) => !known.has(cid)).map((cid) => `${where} names '${cid}', which is not a shared collection in this repository. Shared collections here: ${known.size > 0 ? [...known].sort().join(", ") : "(none - a schema needs storage.type \"firestore\")"}.`));
|
|
628
|
+
}
|
|
629
|
+
/** Everything publish refuses, as lines the author can act on.
|
|
630
|
+
*
|
|
631
|
+
* All of them, every time. Publish is a manual step with a human waiting on
|
|
632
|
+
* it; stopping at the first problem turns one review into five. */
|
|
633
|
+
function publishProblems(app, collections, publisherEmail) {
|
|
634
|
+
return [
|
|
635
|
+
...unknownCidProblems(app, collections),
|
|
636
|
+
...publisherProblems(app, publisherEmail),
|
|
637
|
+
...submitOnlyProblems(app),
|
|
638
|
+
...aggregateProblems(app),
|
|
639
|
+
...authProblems(app),
|
|
640
|
+
...mailProblems(app),
|
|
641
|
+
...submitShapeProblems(app),
|
|
642
|
+
...coherenceProblems(app),
|
|
643
|
+
...primaryKeyProblems(app, collections)
|
|
644
|
+
];
|
|
645
|
+
}
|
|
646
|
+
/** A public submission must NOT be allowed to name its own primary key.
|
|
647
|
+
*
|
|
648
|
+
* The rules constrain the DOCUMENT ID (`idFrom`) and cannot constrain the
|
|
649
|
+
* value of a field — nothing compares `request.resource.data[primaryKey]`
|
|
650
|
+
* with the path being written. So a submit path that accepts the primary key
|
|
651
|
+
* as a `createField` lets a submitter write at their one permitted document
|
|
652
|
+
* id while CLAIMING another record's identity, or a duplicate.
|
|
653
|
+
*
|
|
654
|
+
* It is refused rather than tolerated because there is nothing for the field
|
|
655
|
+
* to do: `firestoreStore` takes a shared record's identity from the document
|
|
656
|
+
* id and overwrites the field on read, so a submitted value is either equal
|
|
657
|
+
* to the id (noise) or a lie (silently discarded). Publishing a form field
|
|
658
|
+
* whose value is thrown away is worse than not having it — the author will
|
|
659
|
+
* believe submitters choose their ids.
|
|
660
|
+
*
|
|
661
|
+
* This is the second answer to the same question. The first was the reverse —
|
|
662
|
+
* REQUIRE the key, because a record without one was rejected by every reader
|
|
663
|
+
* — and it was right about the symptom and wrong about the cure: the identity
|
|
664
|
+
* belongs to the id the rules can pin, not to a field they cannot. */
|
|
665
|
+
function primaryKeyProblems(app, collections) {
|
|
666
|
+
const primaryKeyOf = new Map(collections.map((collection) => [collection.cid, collection.primaryKey]));
|
|
667
|
+
return Object.entries(app.public?.submit ?? {}).flatMap(([cid, submit]) => {
|
|
668
|
+
const primaryKey = primaryKeyOf.get(cid);
|
|
669
|
+
if (primaryKey === void 0 || !submit.createFields.includes(primaryKey)) return [];
|
|
670
|
+
return [`public.submit.${cid}.createFields must NOT include "${primaryKey}", the schema's primaryKey: the rules can pin the document id but not the value of a field, so a submitter could write at their own id while claiming another record's. A shared record's identity is its document id — the store fills the field from it, and a submitted value is either the same thing or a lie that is thrown away.`];
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
//#endregion
|
|
15
674
|
//#region src/collection/server/skillAssets.ts
|
|
16
675
|
/** Read a collection's custom-view HTML, path-safely. `viewFile` is a
|
|
17
676
|
* schema-validated `views/*.html` path, resolved with realpath containment.
|
|
@@ -320,7 +979,7 @@ function toRefRecords(linked) {
|
|
|
320
979
|
function projectBacklinks(field, schema, enriched, linked) {
|
|
321
980
|
const source = linked[field.from];
|
|
322
981
|
if (!source) return [];
|
|
323
|
-
const selfId =
|
|
982
|
+
const selfId = require_itemId.fieldText(enriched[schema.primaryKey]);
|
|
324
983
|
return require_promptSafety.backlinkRows(field, selfId, Object.values(source.byId)).map((row) => require_promptSafety.projectBacklinkRow(row, field.display, source.schema.primaryKey));
|
|
325
984
|
}
|
|
326
985
|
/** Project the computed (never-stored) field kinds onto one derived
|
|
@@ -329,9 +988,9 @@ function projectBacklinks(field, schema, enriched, linked) {
|
|
|
329
988
|
* `backlinks` → the matching source rows (see `projectBacklinks`). */
|
|
330
989
|
function projectComputed(schema, enriched, linked) {
|
|
331
990
|
for (const [key, field] of Object.entries(schema.fields)) {
|
|
332
|
-
if (field.type === "toggle" && field.field) enriched[key] =
|
|
991
|
+
if (field.type === "toggle" && field.field) enriched[key] = require_itemId.fieldText(enriched[field.field]) === field.onValue;
|
|
333
992
|
if (field.type === "embed" && field.to) {
|
|
334
|
-
const targetId =
|
|
993
|
+
const targetId = require_itemId.embedTargetId(field, enriched);
|
|
335
994
|
const target = require_promptSafety.ownProp(linked, field.to);
|
|
336
995
|
enriched[key] = targetId && target && require_promptSafety.ownProp(target.byId, targetId) || null;
|
|
337
996
|
}
|
|
@@ -352,7 +1011,7 @@ function projectRollups(schema, record, linked) {
|
|
|
352
1011
|
if (field.type !== "rollup") continue;
|
|
353
1012
|
if (out === record) out = { ...record };
|
|
354
1013
|
const source = linked[field.from];
|
|
355
|
-
const selfId =
|
|
1014
|
+
const selfId = require_itemId.fieldText(record[schema.primaryKey]);
|
|
356
1015
|
out[key] = source ? require_promptSafety.rollupValue(field, selfId, Object.values(source.byId)) : null;
|
|
357
1016
|
}
|
|
358
1017
|
return out;
|
|
@@ -399,8 +1058,8 @@ function strictTypeProblem(key, spec, value) {
|
|
|
399
1058
|
case "number":
|
|
400
1059
|
case "money": return Number.isFinite(require_promptSafety.coerceNumeric(value)) ? null : `'${key}' = '${String(value)}' is not numeric (a '${spec.type}' field stores a plain number)`;
|
|
401
1060
|
case "boolean": return value === true || value === false ? null : `'${key}' = '${String(value)}' is not a boolean (store true or false, unquoted)`;
|
|
402
|
-
case "date": return
|
|
403
|
-
case "datetime": return
|
|
1061
|
+
case "date": return require_itemId.parseIsoDate(value) !== null ? null : `'${key}' = '${String(value)}' is not a real YYYY-MM-DD date`;
|
|
1062
|
+
case "datetime": return require_itemId.parseIsoDateTime(value) !== null ? null : `'${key}' = '${String(value)}' is not a YYYY-MM-DDTHH:MM datetime (seconds optional, no timezone suffix — the shape the calendar parses)`;
|
|
404
1063
|
default: return null;
|
|
405
1064
|
}
|
|
406
1065
|
}
|
|
@@ -445,7 +1104,7 @@ var compiled = /* @__PURE__ */ new WeakMap();
|
|
|
445
1104
|
function compileRecordZ(schema, tier) {
|
|
446
1105
|
const cached = compiled.get(schema)?.[tier];
|
|
447
1106
|
if (cached) return cached;
|
|
448
|
-
const stored = Object.entries(schema.fields).filter(([, spec]) => !
|
|
1107
|
+
const stored = Object.entries(schema.fields).filter(([, spec]) => !require_itemId.COMPUTED_TYPES.has(spec.type));
|
|
449
1108
|
const validator = zod.z.looseObject({}).superRefine((record, ctx) => {
|
|
450
1109
|
for (const [key, spec] of stored) {
|
|
451
1110
|
const problem = recordFieldProblem(key, spec, record[key], tier);
|
|
@@ -471,6 +1130,18 @@ function firstRecordProblem(record, schema, tier) {
|
|
|
471
1130
|
}
|
|
472
1131
|
//#endregion
|
|
473
1132
|
//#region src/collection/server/validate.ts
|
|
1133
|
+
/** Don't flood the result; the first batch is enough to act on. Exported
|
|
1134
|
+
* because a caller that REPORTS a count has to know the count is a floor —
|
|
1135
|
+
* `publish` presents a full batch as "at least N" rather than as a total. */
|
|
1136
|
+
var MAX_RECORD_ISSUES = 25;
|
|
1137
|
+
/** The `file` of the pseudo-issue reported when the backend could not be read
|
|
1138
|
+
* at all.
|
|
1139
|
+
*
|
|
1140
|
+
* Exported because it is a DIFFERENT KIND of answer from "this record is
|
|
1141
|
+
* invalid", and a caller that treats the two alike gets it wrong in the
|
|
1142
|
+
* direction that matters: `publish` lets the user override invalid records,
|
|
1143
|
+
* and overriding this one would mean publishing without ever having looked. */
|
|
1144
|
+
var STORE_UNREADABLE = "(store)";
|
|
474
1145
|
var MAX_ISSUES = 25;
|
|
475
1146
|
/** Read every `<id>.json` under the collection's dataDir and report the
|
|
476
1147
|
* ones that won't load or violate the schema. An empty list means every
|
|
@@ -516,14 +1187,14 @@ async function validateStoreRecords(collection, opts) {
|
|
|
516
1187
|
items = await require_discovery.storeFor(collection, { workspaceRoot: opts.workspaceRoot }).list();
|
|
517
1188
|
} catch (err) {
|
|
518
1189
|
return [{
|
|
519
|
-
file:
|
|
1190
|
+
file: STORE_UNREADABLE,
|
|
520
1191
|
problem: `records could not be read from the storage backend: ${err instanceof Error ? err.message : String(err)}`
|
|
521
1192
|
}];
|
|
522
1193
|
}
|
|
523
1194
|
const issues = [];
|
|
524
1195
|
for (const item of items) {
|
|
525
1196
|
if (issues.length >= MAX_ISSUES) break;
|
|
526
|
-
const itemId =
|
|
1197
|
+
const itemId = require_itemId.fieldText(item[collection.schema.primaryKey]);
|
|
527
1198
|
const problem = validateRecordObject(item, itemId, collection.schema, "strict");
|
|
528
1199
|
if (problem) issues.push({
|
|
529
1200
|
file: itemId,
|
|
@@ -638,7 +1309,7 @@ async function applyMutateAction(collection, action, itemId, params, opts = {})
|
|
|
638
1309
|
};
|
|
639
1310
|
const stored = Object.entries(existing).filter(([key]) => {
|
|
640
1311
|
const spec = collection.schema.fields[key];
|
|
641
|
-
return !spec || !
|
|
1312
|
+
return !spec || !require_itemId.COMPUTED_TYPES.has(spec.type);
|
|
642
1313
|
});
|
|
643
1314
|
const merged = {
|
|
644
1315
|
...Object.fromEntries(stored),
|
|
@@ -715,7 +1386,7 @@ async function countRecordFiles(dataDir, workspaceRoot) {
|
|
|
715
1386
|
* `countRecordFiles` (an unreadable file / missing engine must not
|
|
716
1387
|
* break the whole ontology). */
|
|
717
1388
|
async function countRecords(collection, workspaceRoot) {
|
|
718
|
-
if (
|
|
1389
|
+
if (require_itemId.storageKindFor(collection.schema) !== "file") try {
|
|
719
1390
|
return (await require_discovery.storeFor(collection, { workspaceRoot }).page({ limit: 0 })).total;
|
|
720
1391
|
} catch (err) {
|
|
721
1392
|
if (require_discovery.isBackendUnavailable(err)) return null;
|
|
@@ -760,7 +1431,7 @@ function buildRecordsById(items, primaryKey) {
|
|
|
760
1431
|
* ties pick a different record — and thus a different icon — between
|
|
761
1432
|
* reconciles. A stable id sort pins one answer. */
|
|
762
1433
|
function sortByPrimaryKey(items, primaryKey) {
|
|
763
|
-
return [...items].sort((left, right) =>
|
|
1434
|
+
return [...items].sort((left, right) => require_itemId.fieldText(left[primaryKey]).localeCompare(require_itemId.fieldText(right[primaryKey])));
|
|
764
1435
|
}
|
|
765
1436
|
/** Compute the effective launcher icon for `collection`: its static
|
|
766
1437
|
* `schema.icon` when it declares no `dynamicIcon`, else the icon
|
|
@@ -897,7 +1568,7 @@ function successorId(sourceId, next) {
|
|
|
897
1568
|
function matchesWhen(when, schema, item) {
|
|
898
1569
|
if (when) {
|
|
899
1570
|
const raw = item[when.field];
|
|
900
|
-
const text =
|
|
1571
|
+
const text = require_itemId.fieldTextOrNull(raw);
|
|
901
1572
|
return text !== null && when.in.includes(text);
|
|
902
1573
|
}
|
|
903
1574
|
return require_promptSafety.itemIsDone(schema, item);
|
|
@@ -909,10 +1580,10 @@ function matchesWhen(when, schema, item) {
|
|
|
909
1580
|
* Discovery rejects a map that doesn't cover the enum's values, so null
|
|
910
1581
|
* here means a record that predates a map/enum edit. */
|
|
911
1582
|
function resolveEvery(every, sourceItem) {
|
|
912
|
-
if (!
|
|
1583
|
+
if (!require_itemId.isFieldDrivenEvery(every)) return every;
|
|
913
1584
|
const raw = sourceItem[every.fromField];
|
|
914
1585
|
if (raw === void 0 || raw === null || raw === "") return null;
|
|
915
|
-
const key =
|
|
1586
|
+
const key = require_itemId.fieldTextOrNull(raw);
|
|
916
1587
|
if (key === null || !Object.hasOwn(every.map, key)) return null;
|
|
917
1588
|
return every.map[key] ?? null;
|
|
918
1589
|
}
|
|
@@ -951,7 +1622,7 @@ function logSpawnSkip(slug, triggerField, every, sourceItem, sourceId) {
|
|
|
951
1622
|
});
|
|
952
1623
|
return;
|
|
953
1624
|
}
|
|
954
|
-
const fromField =
|
|
1625
|
+
const fromField = require_itemId.isFieldDrivenEvery(every) ? every.fromField : void 0;
|
|
955
1626
|
require_discovery.log.warn("collections", "spawn skipped: no `every` mapping for frequency value", {
|
|
956
1627
|
slug,
|
|
957
1628
|
sourceId,
|
|
@@ -1653,7 +2324,7 @@ async function handleGetItems(collection, args, deps) {
|
|
|
1653
2324
|
function computedKeyProblem(record, schema) {
|
|
1654
2325
|
for (const key of Object.keys(record)) {
|
|
1655
2326
|
const spec = schema.fields[key];
|
|
1656
|
-
if (!spec || !
|
|
2327
|
+
if (!spec || !require_itemId.COMPUTED_TYPES.has(spec.type)) continue;
|
|
1657
2328
|
if (spec.type === "toggle" && spec.field) return `'${key}' is a toggle projection — write the enum field '${spec.field}' instead`;
|
|
1658
2329
|
return `'${key}' is ${{
|
|
1659
2330
|
derived: "derived",
|
|
@@ -1693,7 +2364,7 @@ async function mergeWithExisting(collection, store, record, itemId) {
|
|
|
1693
2364
|
if (!existing) return `'${itemId}' not found — mode "merge" updates an existing record; use "upsert" or "create" to add it`;
|
|
1694
2365
|
const stored = Object.entries(existing).filter(([key]) => {
|
|
1695
2366
|
const spec = collection.schema.fields[key];
|
|
1696
|
-
return !spec || !
|
|
2367
|
+
return !spec || !require_itemId.COMPUTED_TYPES.has(spec.type);
|
|
1697
2368
|
});
|
|
1698
2369
|
return {
|
|
1699
2370
|
...Object.fromEntries(stored),
|
|
@@ -2069,6 +2740,36 @@ function makeManageCollectionTool(deps = {}) {
|
|
|
2069
2740
|
};
|
|
2070
2741
|
}
|
|
2071
2742
|
//#endregion
|
|
2743
|
+
Object.defineProperty(exports, "APPS_COLLECTION", {
|
|
2744
|
+
enumerable: true,
|
|
2745
|
+
get: function() {
|
|
2746
|
+
return APPS_COLLECTION;
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
2749
|
+
Object.defineProperty(exports, "APP_ROLES", {
|
|
2750
|
+
enumerable: true,
|
|
2751
|
+
get: function() {
|
|
2752
|
+
return APP_ROLES;
|
|
2753
|
+
}
|
|
2754
|
+
});
|
|
2755
|
+
Object.defineProperty(exports, "APP_SLUGS_COLLECTION", {
|
|
2756
|
+
enumerable: true,
|
|
2757
|
+
get: function() {
|
|
2758
|
+
return APP_SLUGS_COLLECTION;
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
Object.defineProperty(exports, "AuthoredAppZ", {
|
|
2762
|
+
enumerable: true,
|
|
2763
|
+
get: function() {
|
|
2764
|
+
return AuthoredAppZ;
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2767
|
+
Object.defineProperty(exports, "MAX_RECORD_ISSUES", {
|
|
2768
|
+
enumerable: true,
|
|
2769
|
+
get: function() {
|
|
2770
|
+
return MAX_RECORD_ISSUES;
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2072
2773
|
Object.defineProperty(exports, "MAX_SCHEMA_ISSUES", {
|
|
2073
2774
|
enumerable: true,
|
|
2074
2775
|
get: function() {
|
|
@@ -2087,18 +2788,60 @@ Object.defineProperty(exports, "ONE_SECOND_MS", {
|
|
|
2087
2788
|
return ONE_SECOND_MS;
|
|
2088
2789
|
}
|
|
2089
2790
|
});
|
|
2791
|
+
Object.defineProperty(exports, "PUBLIC_CONFIG_DOC", {
|
|
2792
|
+
enumerable: true,
|
|
2793
|
+
get: function() {
|
|
2794
|
+
return PUBLIC_CONFIG_DOC;
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
Object.defineProperty(exports, "STORE_UNREADABLE", {
|
|
2798
|
+
enumerable: true,
|
|
2799
|
+
get: function() {
|
|
2800
|
+
return STORE_UNREADABLE;
|
|
2801
|
+
}
|
|
2802
|
+
});
|
|
2090
2803
|
Object.defineProperty(exports, "advanceTriggerDate", {
|
|
2091
2804
|
enumerable: true,
|
|
2092
2805
|
get: function() {
|
|
2093
2806
|
return advanceTriggerDate;
|
|
2094
2807
|
}
|
|
2095
2808
|
});
|
|
2809
|
+
Object.defineProperty(exports, "appConfigPath", {
|
|
2810
|
+
enumerable: true,
|
|
2811
|
+
get: function() {
|
|
2812
|
+
return appConfigPath;
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
Object.defineProperty(exports, "appSchemasPath", {
|
|
2816
|
+
enumerable: true,
|
|
2817
|
+
get: function() {
|
|
2818
|
+
return appSchemasPath;
|
|
2819
|
+
}
|
|
2820
|
+
});
|
|
2821
|
+
Object.defineProperty(exports, "appSlugDoc", {
|
|
2822
|
+
enumerable: true,
|
|
2823
|
+
get: function() {
|
|
2824
|
+
return appSlugDoc;
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
2827
|
+
Object.defineProperty(exports, "appStagingPath", {
|
|
2828
|
+
enumerable: true,
|
|
2829
|
+
get: function() {
|
|
2830
|
+
return appStagingPath;
|
|
2831
|
+
}
|
|
2832
|
+
});
|
|
2096
2833
|
Object.defineProperty(exports, "applyMutateAction", {
|
|
2097
2834
|
enumerable: true,
|
|
2098
2835
|
get: function() {
|
|
2099
2836
|
return applyMutateAction;
|
|
2100
2837
|
}
|
|
2101
2838
|
});
|
|
2839
|
+
Object.defineProperty(exports, "bindsSubmitterIdentity", {
|
|
2840
|
+
enumerable: true,
|
|
2841
|
+
get: function() {
|
|
2842
|
+
return bindsSubmitterIdentity;
|
|
2843
|
+
}
|
|
2844
|
+
});
|
|
2102
2845
|
Object.defineProperty(exports, "buildActionSeedPrompt", {
|
|
2103
2846
|
enumerable: true,
|
|
2104
2847
|
get: function() {
|
|
@@ -2195,18 +2938,54 @@ Object.defineProperty(exports, "maybeSpawnSuccessor", {
|
|
|
2195
2938
|
return maybeSpawnSuccessor;
|
|
2196
2939
|
}
|
|
2197
2940
|
});
|
|
2941
|
+
Object.defineProperty(exports, "parseAuthoredApp", {
|
|
2942
|
+
enumerable: true,
|
|
2943
|
+
get: function() {
|
|
2944
|
+
return parseAuthoredApp;
|
|
2945
|
+
}
|
|
2946
|
+
});
|
|
2198
2947
|
Object.defineProperty(exports, "parseCivil", {
|
|
2199
2948
|
enumerable: true,
|
|
2200
2949
|
get: function() {
|
|
2201
2950
|
return parseCivil;
|
|
2202
2951
|
}
|
|
2203
2952
|
});
|
|
2953
|
+
Object.defineProperty(exports, "projectApp", {
|
|
2954
|
+
enumerable: true,
|
|
2955
|
+
get: function() {
|
|
2956
|
+
return projectApp;
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
Object.defineProperty(exports, "projectDeploy", {
|
|
2960
|
+
enumerable: true,
|
|
2961
|
+
get: function() {
|
|
2962
|
+
return projectDeploy;
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
Object.defineProperty(exports, "projectPublish", {
|
|
2966
|
+
enumerable: true,
|
|
2967
|
+
get: function() {
|
|
2968
|
+
return projectPublish;
|
|
2969
|
+
}
|
|
2970
|
+
});
|
|
2971
|
+
Object.defineProperty(exports, "promoteSchema", {
|
|
2972
|
+
enumerable: true,
|
|
2973
|
+
get: function() {
|
|
2974
|
+
return promoteSchema;
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2204
2977
|
Object.defineProperty(exports, "promptPathsFor", {
|
|
2205
2978
|
enumerable: true,
|
|
2206
2979
|
get: function() {
|
|
2207
2980
|
return promptPathsFor;
|
|
2208
2981
|
}
|
|
2209
2982
|
});
|
|
2983
|
+
Object.defineProperty(exports, "publishProblems", {
|
|
2984
|
+
enumerable: true,
|
|
2985
|
+
get: function() {
|
|
2986
|
+
return publishProblems;
|
|
2987
|
+
}
|
|
2988
|
+
});
|
|
2210
2989
|
Object.defineProperty(exports, "readCustomViewHtml", {
|
|
2211
2990
|
enumerable: true,
|
|
2212
2991
|
get: function() {
|
|
@@ -2255,6 +3034,12 @@ Object.defineProperty(exports, "schemaRelations", {
|
|
|
2255
3034
|
return schemaRelations;
|
|
2256
3035
|
}
|
|
2257
3036
|
});
|
|
3037
|
+
Object.defineProperty(exports, "stagedRuleConfig", {
|
|
3038
|
+
enumerable: true,
|
|
3039
|
+
get: function() {
|
|
3040
|
+
return stagedRuleConfig;
|
|
3041
|
+
}
|
|
3042
|
+
});
|
|
2258
3043
|
Object.defineProperty(exports, "successorId", {
|
|
2259
3044
|
enumerable: true,
|
|
2260
3045
|
get: function() {
|
|
@@ -2274,4 +3059,4 @@ Object.defineProperty(exports, "validateRecordObject", {
|
|
|
2274
3059
|
}
|
|
2275
3060
|
});
|
|
2276
3061
|
|
|
2277
|
-
//# sourceMappingURL=server-
|
|
3062
|
+
//# sourceMappingURL=server-CgkkWWBd.cjs.map
|