@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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** The four roles the deployed rules understand. `participant` is the layer
|
|
3
|
+
* that is NAMED but reads only its own rows — see `readerOf` vs `listedIn`. */
|
|
4
|
+
export declare const APP_ROLES: readonly ["owner", "editor", "viewer", "participant"];
|
|
5
|
+
/** The declarative mail queue, as the rules re-derive it: a transition of the
|
|
6
|
+
* status field, a recipient read off the RECORD, and a fixed template. */
|
|
7
|
+
declare const MailZ: z.ZodObject<{
|
|
8
|
+
toField: z.ZodString;
|
|
9
|
+
on: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
10
|
+
from: z.ZodArray<z.ZodString>;
|
|
11
|
+
to: z.ZodString;
|
|
12
|
+
}, z.core.$strict>>;
|
|
13
|
+
dataFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
14
|
+
}, z.core.$strict>;
|
|
15
|
+
/** What the rules read out of `collections[cid]`. NOT the schema — the schema
|
|
16
|
+
* is published beside it, untouched, for clients to render from. */
|
|
17
|
+
declare const CollectionConfigZ: z.ZodObject<{
|
|
18
|
+
statusField: z.ZodOptional<z.ZodString>;
|
|
19
|
+
transitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
20
|
+
immutable: z.ZodOptional<z.ZodBoolean>;
|
|
21
|
+
submitOnly: z.ZodOptional<z.ZodBoolean>;
|
|
22
|
+
peerVisibility: z.ZodOptional<z.ZodEnum<{
|
|
23
|
+
public: "public";
|
|
24
|
+
hidden: "hidden";
|
|
25
|
+
}>>;
|
|
26
|
+
revealGated: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
gatedFrom: z.ZodOptional<z.ZodString>;
|
|
28
|
+
revealBy: z.ZodOptional<z.ZodString>;
|
|
29
|
+
mail: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
toField: z.ZodString;
|
|
31
|
+
on: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
32
|
+
from: z.ZodArray<z.ZodString>;
|
|
33
|
+
to: z.ZodString;
|
|
34
|
+
}, z.core.$strict>>;
|
|
35
|
+
dataFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
36
|
+
}, z.core.$strict>>;
|
|
37
|
+
aggregate: z.ZodOptional<z.ZodObject<{
|
|
38
|
+
by: z.ZodArray<z.ZodString>;
|
|
39
|
+
}, z.core.$strict>>;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
declare const SubmitZ: z.ZodObject<{
|
|
42
|
+
auth: z.ZodEnum<{
|
|
43
|
+
none: "none";
|
|
44
|
+
anonymous: "anonymous";
|
|
45
|
+
verifiedEmail: "verifiedEmail";
|
|
46
|
+
}>;
|
|
47
|
+
emailField: z.ZodOptional<z.ZodString>;
|
|
48
|
+
createFields: z.ZodArray<z.ZodString>;
|
|
49
|
+
initialStatus: z.ZodOptional<z.ZodString>;
|
|
50
|
+
idFrom: z.ZodOptional<z.ZodEnum<{
|
|
51
|
+
auto: "auto";
|
|
52
|
+
"auth.uid": "auth.uid";
|
|
53
|
+
"auth.uid+field": "auth.uid+field";
|
|
54
|
+
}>>;
|
|
55
|
+
idField: z.ZodOptional<z.ZodString>;
|
|
56
|
+
validate: z.ZodOptional<z.ZodObject<{
|
|
57
|
+
required: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
58
|
+
keyFields: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
59
|
+
field: z.ZodString;
|
|
60
|
+
values: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
61
|
+
}, z.core.$strict>>>;
|
|
62
|
+
}, z.core.$strict>>;
|
|
63
|
+
window: z.ZodOptional<z.ZodObject<{
|
|
64
|
+
from: z.ZodOptional<z.ZodISODateTime>;
|
|
65
|
+
until: z.ZodOptional<z.ZodISODateTime>;
|
|
66
|
+
}, z.core.$strict>>;
|
|
67
|
+
selfUpdate: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
68
|
+
selfTransitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
69
|
+
finalize: z.ZodOptional<z.ZodBoolean>;
|
|
70
|
+
audience: z.ZodOptional<z.ZodLiteral<"participant">>;
|
|
71
|
+
gateOn: z.ZodOptional<z.ZodObject<{
|
|
72
|
+
phase: z.ZodString;
|
|
73
|
+
match: z.ZodString;
|
|
74
|
+
}, z.core.$strict>>;
|
|
75
|
+
}, z.core.$strict>;
|
|
76
|
+
/** The whole authored declaration.
|
|
77
|
+
*
|
|
78
|
+
* `owner` is accepted but is NOT the published value — publish stamps the
|
|
79
|
+
* publisher's uid (or carries the existing one forward, which is what the
|
|
80
|
+
* rules require on update) and refuses a declaration that disagrees. It is
|
|
81
|
+
* accepted rather than banned because the sample app.json in the design note
|
|
82
|
+
* shows it, and a hard refusal on a key the samples contain would be a worse
|
|
83
|
+
* first experience than a message naming the mismatch. */
|
|
84
|
+
export declare const AuthoredAppZ: z.ZodObject<{
|
|
85
|
+
aid: z.ZodString;
|
|
86
|
+
name: z.ZodOptional<z.ZodString>;
|
|
87
|
+
aidEnv: z.ZodOptional<z.ZodString>;
|
|
88
|
+
owner: z.ZodOptional<z.ZodString>;
|
|
89
|
+
members: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodUnion<readonly [z.ZodLiteral<"*">, z.ZodString]>, z.ZodEnum<{
|
|
90
|
+
owner: "owner";
|
|
91
|
+
editor: "editor";
|
|
92
|
+
viewer: "viewer";
|
|
93
|
+
participant: "participant";
|
|
94
|
+
}>>>;
|
|
95
|
+
collections: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
96
|
+
statusField: z.ZodOptional<z.ZodString>;
|
|
97
|
+
transitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
98
|
+
immutable: z.ZodOptional<z.ZodBoolean>;
|
|
99
|
+
submitOnly: z.ZodOptional<z.ZodBoolean>;
|
|
100
|
+
peerVisibility: z.ZodOptional<z.ZodEnum<{
|
|
101
|
+
public: "public";
|
|
102
|
+
hidden: "hidden";
|
|
103
|
+
}>>;
|
|
104
|
+
revealGated: z.ZodOptional<z.ZodBoolean>;
|
|
105
|
+
gatedFrom: z.ZodOptional<z.ZodString>;
|
|
106
|
+
revealBy: z.ZodOptional<z.ZodString>;
|
|
107
|
+
mail: z.ZodOptional<z.ZodObject<{
|
|
108
|
+
toField: z.ZodString;
|
|
109
|
+
on: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
110
|
+
from: z.ZodArray<z.ZodString>;
|
|
111
|
+
to: z.ZodString;
|
|
112
|
+
}, z.core.$strict>>;
|
|
113
|
+
dataFields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
114
|
+
}, z.core.$strict>>;
|
|
115
|
+
aggregate: z.ZodOptional<z.ZodObject<{
|
|
116
|
+
by: z.ZodArray<z.ZodString>;
|
|
117
|
+
}, z.core.$strict>>;
|
|
118
|
+
}, z.core.$strict>>>;
|
|
119
|
+
participantRead: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
120
|
+
public: z.ZodOptional<z.ZodObject<{
|
|
121
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
122
|
+
read: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
123
|
+
submit: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
124
|
+
auth: z.ZodEnum<{
|
|
125
|
+
none: "none";
|
|
126
|
+
anonymous: "anonymous";
|
|
127
|
+
verifiedEmail: "verifiedEmail";
|
|
128
|
+
}>;
|
|
129
|
+
emailField: z.ZodOptional<z.ZodString>;
|
|
130
|
+
createFields: z.ZodArray<z.ZodString>;
|
|
131
|
+
initialStatus: z.ZodOptional<z.ZodString>;
|
|
132
|
+
idFrom: z.ZodOptional<z.ZodEnum<{
|
|
133
|
+
auto: "auto";
|
|
134
|
+
"auth.uid": "auth.uid";
|
|
135
|
+
"auth.uid+field": "auth.uid+field";
|
|
136
|
+
}>>;
|
|
137
|
+
idField: z.ZodOptional<z.ZodString>;
|
|
138
|
+
validate: z.ZodOptional<z.ZodObject<{
|
|
139
|
+
required: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
140
|
+
keyFields: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
141
|
+
field: z.ZodString;
|
|
142
|
+
values: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
143
|
+
}, z.core.$strict>>>;
|
|
144
|
+
}, z.core.$strict>>;
|
|
145
|
+
window: z.ZodOptional<z.ZodObject<{
|
|
146
|
+
from: z.ZodOptional<z.ZodISODateTime>;
|
|
147
|
+
until: z.ZodOptional<z.ZodISODateTime>;
|
|
148
|
+
}, z.core.$strict>>;
|
|
149
|
+
selfUpdate: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
150
|
+
selfTransitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
151
|
+
finalize: z.ZodOptional<z.ZodBoolean>;
|
|
152
|
+
audience: z.ZodOptional<z.ZodLiteral<"participant">>;
|
|
153
|
+
gateOn: z.ZodOptional<z.ZodObject<{
|
|
154
|
+
phase: z.ZodString;
|
|
155
|
+
match: z.ZodString;
|
|
156
|
+
}, z.core.$strict>>;
|
|
157
|
+
}, z.core.$strict>>>;
|
|
158
|
+
}, z.core.$strict>>;
|
|
159
|
+
}, z.core.$strict>;
|
|
160
|
+
export type AuthoredApp = z.infer<typeof AuthoredAppZ>;
|
|
161
|
+
export type AuthoredCollectionConfig = z.infer<typeof CollectionConfigZ>;
|
|
162
|
+
export type AuthoredSubmit = z.infer<typeof SubmitZ>;
|
|
163
|
+
export type AuthoredMail = z.infer<typeof MailZ>;
|
|
164
|
+
export type AuthoredAppResult = {
|
|
165
|
+
ok: true;
|
|
166
|
+
app: AuthoredApp;
|
|
167
|
+
} | {
|
|
168
|
+
ok: false;
|
|
169
|
+
problems: string[];
|
|
170
|
+
};
|
|
171
|
+
/** Parse the authored declaration out of `app.json`'s text.
|
|
172
|
+
*
|
|
173
|
+
* Returns a LIST of problems rather than throwing, for the same reason
|
|
174
|
+
* `loadAppManifest` returns a failure: the caller is a gate whose entire job
|
|
175
|
+
* is to hand the author something to act on. Every problem is reported at
|
|
176
|
+
* once — publish is a manual step, and a parser that stops at the first key
|
|
177
|
+
* makes it N round trips. */
|
|
178
|
+
export declare function parseAuthoredApp(raw: string): AuthoredAppResult;
|
|
179
|
+
/** zod issues as one actionable line each: `public.submit.responses.auth: …`. */
|
|
180
|
+
export declare function authoredProblems(error: z.ZodError): string[];
|
|
181
|
+
export {};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { CollectionSchema } from '../core/schema';
|
|
2
|
+
import { AuthoredApp, AuthoredCollectionConfig } from './publishManifest';
|
|
3
|
+
/** Who and when. Threaded in rather than read from a clock inside the
|
|
4
|
+
* projection so the projection stays pure and the tests can assert on an
|
|
5
|
+
* exact document. */
|
|
6
|
+
export interface PublishStamp {
|
|
7
|
+
/** The publisher's Firebase uid. The rules require `owner ==
|
|
8
|
+
* request.auth.uid` on CREATE and `owner` unchanged on UPDATE, so this is
|
|
9
|
+
* used only when the app document does not exist yet. */
|
|
10
|
+
uid: string;
|
|
11
|
+
/** The publisher's verified email — the principal the roster is keyed by. */
|
|
12
|
+
email: string;
|
|
13
|
+
/** Wall-clock millis of this publish. */
|
|
14
|
+
publishedAt: number;
|
|
15
|
+
/** The git commit the declaration was read from, when the host could
|
|
16
|
+
* resolve one. Absent is a normal state (a dirty tree, a repository
|
|
17
|
+
* without git), and absent is more honest than a fabricated value. */
|
|
18
|
+
commit?: string | undefined;
|
|
19
|
+
/** Was the working tree modified when the commit was read?
|
|
20
|
+
*
|
|
21
|
+
* Recorded as `publishedDirty` on the app document, because a commit that
|
|
22
|
+
* does not describe what was published is worse than no commit: it looks
|
|
23
|
+
* auditable. Publish-owned like the rest of the `published*` family — a
|
|
24
|
+
* deploy must not drop the marker, and a later CLEAN publish must clear it
|
|
25
|
+
* rather than inherit it forever. */
|
|
26
|
+
dirty?: boolean | undefined;
|
|
27
|
+
}
|
|
28
|
+
/** One collection's published schema document (`apps/{aid}/collections/{cid}`).
|
|
29
|
+
*
|
|
30
|
+
* The rules never read `publishedSchema` — `schemaRead` gates the whole
|
|
31
|
+
* document and stops there. It is here for the CLIENTS: a member's host
|
|
32
|
+
* renders from it, and a public webview has no other way to know the fields.
|
|
33
|
+
* The whole schema is published rather than a projection of it, because
|
|
34
|
+
* every attempt to guess "the part a view needs" is a guess about a view
|
|
35
|
+
* that has not been written yet. */
|
|
36
|
+
export interface PublishedSchemaDoc extends Record<string, unknown> {
|
|
37
|
+
publishedSchema: CollectionSchema;
|
|
38
|
+
publishedAt: number;
|
|
39
|
+
publishedBy: string;
|
|
40
|
+
publishedCommit?: string;
|
|
41
|
+
}
|
|
42
|
+
/** A STAGED schema document (`apps/{aid}/staging/{cid}`) — what deploy writes
|
|
43
|
+
* and what `/staging/{aid}` renders from.
|
|
44
|
+
*
|
|
45
|
+
* Its provenance keys are `deployed*`, not `published*`. The two are different
|
|
46
|
+
* questions with different answers at almost every moment: `published*` says
|
|
47
|
+
* which revision the PUBLIC is looking at, and a deploy must not move it —
|
|
48
|
+
* otherwise staging a draft rewrites the recorded public revision (and, with
|
|
49
|
+
* `previousPublished`, the thing a rollback would restore) before anyone has
|
|
50
|
+
* published anything. */
|
|
51
|
+
export interface StagedSchemaDoc extends Record<string, unknown> {
|
|
52
|
+
publishedSchema: CollectionSchema;
|
|
53
|
+
/** This collection's RULE-FACING configuration (`transitions`, `immutable`,
|
|
54
|
+
* `submitOnly`, `peerVisibility`, …) — staged with the schema, not written
|
|
55
|
+
* straight onto the app document.
|
|
56
|
+
*
|
|
57
|
+
* It has to be staged for the same reason the `public` block is: the rules
|
|
58
|
+
* read `apps/{aid}.collections[cid]` when they authorize a PUBLIC write, so
|
|
59
|
+
* a deploy that landed it would change what anonymous visitors may do
|
|
60
|
+
* before anyone published. The cost is that `/staging/{aid}` exercises the
|
|
61
|
+
* new schema against the CURRENTLY PUBLISHED rule configuration; that is
|
|
62
|
+
* the safe direction to be wrong in. */
|
|
63
|
+
config?: AuthoredCollectionConfig;
|
|
64
|
+
/** Whether this cid is in `participantRead` — same reason, same treatment. */
|
|
65
|
+
participantRead?: boolean;
|
|
66
|
+
deployedAt: number;
|
|
67
|
+
deployedBy: string;
|
|
68
|
+
deployedCommit?: string;
|
|
69
|
+
}
|
|
70
|
+
/** The public settings document (`apps/{aid}/config/public`).
|
|
71
|
+
*
|
|
72
|
+
* `allow read: if true` — this is the one document an anonymous visitor can
|
|
73
|
+
* read, and it is the reason `apps/{aid}` itself is reader-only: a
|
|
74
|
+
* participant reading the app document would see their classmates'
|
|
75
|
+
* addresses. So the roster is NOT here, and neither is `owner`. What is here
|
|
76
|
+
* is what a public form needs in order to render itself and to tell a visitor
|
|
77
|
+
* "this closed yesterday" instead of failing a write with no explanation. */
|
|
78
|
+
export interface PublishedConfigDoc extends Record<string, unknown> {
|
|
79
|
+
name?: string;
|
|
80
|
+
enabled: boolean;
|
|
81
|
+
read: string[];
|
|
82
|
+
submit: Record<string, Record<string, unknown>>;
|
|
83
|
+
publishedAt: number;
|
|
84
|
+
}
|
|
85
|
+
/** Everything one publish writes. Separate documents because they live at
|
|
86
|
+
* separate paths with separate rules — and in the order given: the app
|
|
87
|
+
* document authorizes the other two, so it goes first. */
|
|
88
|
+
export interface PublishedApp {
|
|
89
|
+
app: Record<string, unknown>;
|
|
90
|
+
schemas: {
|
|
91
|
+
cid: string;
|
|
92
|
+
doc: PublishedSchemaDoc;
|
|
93
|
+
}[];
|
|
94
|
+
config: PublishedConfigDoc;
|
|
95
|
+
}
|
|
96
|
+
/** The document id under `apps/{aid}/config`. One document, named, rather than
|
|
97
|
+
* a spread of them: a second public document is a second thing to keep in
|
|
98
|
+
* step, and nothing yet needs one. */
|
|
99
|
+
export declare const PUBLIC_CONFIG_DOC = "public";
|
|
100
|
+
/** Project the authored declaration into the documents publish writes.
|
|
101
|
+
*
|
|
102
|
+
* `existing` is the app document as it is in Firestore right now, or null on
|
|
103
|
+
* a first publish. Two things need it, both required by the rules:
|
|
104
|
+
* - `owner` must be UNCHANGED on update. Re-stamping the publisher's uid
|
|
105
|
+
* would be refused for any app whose owner ever signed in as a different
|
|
106
|
+
* account, and would silently transfer ownership if it were not.
|
|
107
|
+
* - `previousPublished` is that document, so a rollback has something to
|
|
108
|
+
* put back.
|
|
109
|
+
*
|
|
110
|
+
* Pure: no clock, no filesystem, no Firestore. Everything variable arrives as
|
|
111
|
+
* a parameter, which is what makes the conversion table testable as a table. */
|
|
112
|
+
export declare function projectApp(authored: AuthoredApp, schemas: {
|
|
113
|
+
cid: string;
|
|
114
|
+
schema: CollectionSchema;
|
|
115
|
+
}[], stamp: PublishStamp, existing: Record<string, unknown> | null): PublishedApp;
|
|
116
|
+
/** Everything `deploy` writes — the host's staging half of the split (the
|
|
117
|
+
* design's D10).
|
|
118
|
+
*
|
|
119
|
+
* Two things are deliberately ABSENT from the app document here:
|
|
120
|
+
*
|
|
121
|
+
* - **`public`**. That block is what the rules read to authorize anonymous
|
|
122
|
+
* access (`publicOn` / `subOpen` read `apps/{aid}.public`, never the
|
|
123
|
+
* world-readable `config/public`), so writing it on deploy would open the
|
|
124
|
+
* app the moment someone deployed to test. It belongs to `publish`.
|
|
125
|
+
* - **the published schemas**. They go to `staging/{cid}`, which only the
|
|
126
|
+
* roster can read, so a deploy against a LIVE app does not swap the view
|
|
127
|
+
* its visitors are looking at.
|
|
128
|
+
*
|
|
129
|
+
* A host writing this must MERGE the app document rather than replace it:
|
|
130
|
+
* a replace would drop a `public` block a previous publish put there and
|
|
131
|
+
* silently unpublish the app. */
|
|
132
|
+
export interface DeployedApp {
|
|
133
|
+
/** The COMPLETE app document. **Write it with `set`, replacing — never with
|
|
134
|
+
* `{ merge: true }`.**
|
|
135
|
+
*
|
|
136
|
+
* A merge cannot DELETE, and every deletion here is a permission change:
|
|
137
|
+
* removing `members.<email>` revokes access, and a merge would leave the
|
|
138
|
+
* entry live. Worse, the rules require `memberEmails` to equal the keys of
|
|
139
|
+
* `members` (`membersConsistent()`), so a merged member-removal is rejected
|
|
140
|
+
* outright — a deploy that silently does nothing would be the good case.
|
|
141
|
+
*
|
|
142
|
+
* Everything publish owns is carried through from `existing` verbatim, so
|
|
143
|
+
* replacing does not unpublish a live app. */
|
|
144
|
+
app: Record<string, unknown>;
|
|
145
|
+
/** The staged schema documents, for `apps/{aid}/staging/{cid}`. */
|
|
146
|
+
staging: {
|
|
147
|
+
cid: string;
|
|
148
|
+
doc: StagedSchemaDoc;
|
|
149
|
+
}[];
|
|
150
|
+
}
|
|
151
|
+
export declare function projectDeploy(authored: AuthoredApp, schemas: {
|
|
152
|
+
cid: string;
|
|
153
|
+
schema: CollectionSchema;
|
|
154
|
+
}[], stamp: PublishStamp, existing: Record<string, unknown> | null): DeployedApp;
|
|
155
|
+
/** Everything `publish` writes that is NOT a promotion — the public face.
|
|
156
|
+
*
|
|
157
|
+
* The schemas are absent on purpose: publish PROMOTES the documents deploy
|
|
158
|
+
* staged (copy `staging/{cid}` → `collections/{cid}`, re-stamped with
|
|
159
|
+
* {@link promoteSchema}) rather than re-projecting them from git. What the
|
|
160
|
+
* roster tested is then exactly what ships; re-projecting would publish
|
|
161
|
+
* whatever the working tree says at publish time, which nobody has looked at
|
|
162
|
+
* through `/staging/{aid}`.
|
|
163
|
+
*
|
|
164
|
+
* WRITE `public` LAST. It is the only one of the three that grants anything,
|
|
165
|
+
* so a partial failure with it last leaves the app private (fail closed).
|
|
166
|
+
* See the design note's publish ordering. */
|
|
167
|
+
export interface PublishedFace {
|
|
168
|
+
/** The COMPLETE app document **without `public`** — write it with `set`,
|
|
169
|
+
* replacing.
|
|
170
|
+
*
|
|
171
|
+
* Replacing (not merging) is what lets a key DISAPPEAR: withdrawing a
|
|
172
|
+
* collection's rule configuration, or taking `public` out of `app.json`,
|
|
173
|
+
* has to actually remove the field. Everything deploy owns is carried
|
|
174
|
+
* through from `existing`, so publishing does not revert an invitation.
|
|
175
|
+
*
|
|
176
|
+
* `public` is absent HERE on purpose — see {@link PublishedFace.public}. */
|
|
177
|
+
app: Record<string, unknown>;
|
|
178
|
+
/** `apps/{aid}/config/public` — the world-readable projection. */
|
|
179
|
+
config: PublishedConfigDoc;
|
|
180
|
+
/** The `public` block, to be written **LAST, as its own update** — or, when
|
|
181
|
+
* `undefined`, DELETED from the app document (that is how an app becomes
|
|
182
|
+
* private again).
|
|
183
|
+
*
|
|
184
|
+
* Separate from {@link PublishedFace.app} because it is the only one of
|
|
185
|
+
* publish's writes that GRANTS anything: the rules authorize anonymous
|
|
186
|
+
* reads and submissions from `apps/{aid}.public`. Writing it inside the
|
|
187
|
+
* replacement document would open the app before the promoted schemas and
|
|
188
|
+
* the world-readable config exist, so a failure part-way would leave
|
|
189
|
+
* anonymous access live against a half-published surface. Written last, the
|
|
190
|
+
* same failure leaves the app private — which is the direction to fail in.
|
|
191
|
+
*
|
|
192
|
+
* A re-publish therefore passes through a moment with no `public` block.
|
|
193
|
+
* That is a brief denial for visitors, not a brief exposure. */
|
|
194
|
+
public: Record<string, unknown> | undefined;
|
|
195
|
+
}
|
|
196
|
+
/** The rule-facing configuration to promote, read from the STAGED documents
|
|
197
|
+
* rather than from the manifest as it reads right now.
|
|
198
|
+
*
|
|
199
|
+
* Otherwise: deploy revision A, edit `app.json` to revision B, publish — and
|
|
200
|
+
* the promoted schema is A's while the authorization behaviour is B's, a
|
|
201
|
+
* combination nobody exercised through `/staging/{aid}`.
|
|
202
|
+
*
|
|
203
|
+
* (`public` is deliberately NOT part of this: it is not staged, because it is
|
|
204
|
+
* the decision being made AT publish rather than something under test.) */
|
|
205
|
+
export declare function stagedRuleConfig(staged: {
|
|
206
|
+
cid: string;
|
|
207
|
+
doc: StagedSchemaDoc;
|
|
208
|
+
}[]): {
|
|
209
|
+
collections: Record<string, AuthoredCollectionConfig> | undefined;
|
|
210
|
+
participantRead: string[] | undefined;
|
|
211
|
+
};
|
|
212
|
+
export declare function projectPublish(authored: AuthoredApp, staged: {
|
|
213
|
+
cid: string;
|
|
214
|
+
doc: StagedSchemaDoc;
|
|
215
|
+
}[], stamp: PublishStamp, existing: Record<string, unknown> | null): PublishedFace;
|
|
216
|
+
/** Re-stamp a staged schema document as it is promoted to `collections/{cid}`.
|
|
217
|
+
*
|
|
218
|
+
* The stamp answers "which version is PUBLIC right now, and who made it so",
|
|
219
|
+
* so it is written by the operation that changes the answer — publish — not
|
|
220
|
+
* carried over from the deploy that staged it. */
|
|
221
|
+
export declare function promoteSchema(staged: StagedSchemaDoc, stamp: PublishStamp): PublishedSchemaDoc;
|
|
222
|
+
/** The app documents' parent path — the `FirestoreDocs` seam takes a
|
|
223
|
+
* collection path plus a document id, and the app document's id is the aid. */
|
|
224
|
+
export declare const APPS_COLLECTION = "apps";
|
|
225
|
+
/** The collection (schema) documents' parent path — what the PUBLIC page
|
|
226
|
+
* reads, written only by publish (promotion). */
|
|
227
|
+
export declare const appSchemasPath: (aid: string) => string;
|
|
228
|
+
/** The URL-slug reservations — `appSlugs/{slug}` → `{ aid, published }`.
|
|
229
|
+
*
|
|
230
|
+
* A TOP-LEVEL collection, not a field on the app: the public page resolves a
|
|
231
|
+
* slug to an aid BEFORE it can read anything under `apps/{aid}`, and a slug
|
|
232
|
+
* has to be claimable atomically (create-if-absent) so two apps cannot hold
|
|
233
|
+
* the same URL.
|
|
234
|
+
*
|
|
235
|
+
* `published` is what makes the reservation invisible until publish. The slug
|
|
236
|
+
* is human-readable, so a readable reservation would let anyone guess the URL
|
|
237
|
+
* and get the aid — and the aid is the `/staging/{aid}` entrance. The rule is
|
|
238
|
+
* `allow read: if resource.data.published == true`, which needs no `get()` and
|
|
239
|
+
* so costs nothing against the rules' expression budget. */
|
|
240
|
+
export declare const APP_SLUGS_COLLECTION = "appSlugs";
|
|
241
|
+
/** The reservation document. Written by deploy as `{ aid, published: false }`
|
|
242
|
+
* and flipped by publish — never re-pointed at another aid, which the rules
|
|
243
|
+
* enforce on update. */
|
|
244
|
+
export interface AppSlugDoc extends Record<string, unknown> {
|
|
245
|
+
aid: string;
|
|
246
|
+
published: boolean;
|
|
247
|
+
}
|
|
248
|
+
export declare const appSlugDoc: (aid: string, published: boolean) => AppSlugDoc;
|
|
249
|
+
/** The staged schema documents' parent path — what `/staging/{aid}` reads,
|
|
250
|
+
* written by deploy. A separate DOCUMENT rather than a field beside
|
|
251
|
+
* `publishedSchema`, because the rules cannot hide a field: anything inside a
|
|
252
|
+
* document the public page may read is public. */
|
|
253
|
+
export declare const appStagingPath: (aid: string) => string;
|
|
254
|
+
/** The public-config documents' parent path. */
|
|
255
|
+
export declare const appConfigPath: (aid: string) => string;
|
|
@@ -8,6 +8,18 @@ export interface RecordIssue {
|
|
|
8
8
|
/** Human-readable problem, written to be actionable by the LLM. */
|
|
9
9
|
problem: string;
|
|
10
10
|
}
|
|
11
|
+
/** Don't flood the result; the first batch is enough to act on. Exported
|
|
12
|
+
* because a caller that REPORTS a count has to know the count is a floor —
|
|
13
|
+
* `publish` presents a full batch as "at least N" rather than as a total. */
|
|
14
|
+
export declare const MAX_RECORD_ISSUES = 25;
|
|
15
|
+
/** The `file` of the pseudo-issue reported when the backend could not be read
|
|
16
|
+
* at all.
|
|
17
|
+
*
|
|
18
|
+
* Exported because it is a DIFFERENT KIND of answer from "this record is
|
|
19
|
+
* invalid", and a caller that treats the two alike gets it wrong in the
|
|
20
|
+
* direction that matters: `publish` lets the user override invalid records,
|
|
21
|
+
* and overriding this one would mean publishing without ever having looked. */
|
|
22
|
+
export declare const STORE_UNREADABLE = "(store)";
|
|
11
23
|
export declare function validateCollectionRecords(collection: LoadedCollection, opts?: {
|
|
12
24
|
workspaceRoot?: string | undefined;
|
|
13
25
|
}): Promise<RecordIssue[]>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_root = require("../root-rPH6FGDT.cjs");
|
|
3
|
-
const
|
|
4
|
-
const require_promptSafety = require("../promptSafety-
|
|
3
|
+
const require_itemId = require("../itemId-Dzrr3vs5.cjs");
|
|
4
|
+
const require_promptSafety = require("../promptSafety-CWqYq-NS.cjs");
|
|
5
5
|
require("../collection/index.cjs");
|
|
6
|
-
const require_discovery = require("../discovery-
|
|
7
|
-
const require_server = require("../server-
|
|
6
|
+
const require_discovery = require("../discovery-BsXiDZBR.cjs");
|
|
7
|
+
const require_server = require("../server-CgkkWWBd.cjs");
|
|
8
8
|
const require_notifier = require("../notifier-DZJj0sOh.cjs");
|
|
9
9
|
let node_fs_promises = require("node:fs/promises");
|
|
10
10
|
//#region src/collection-watchers/config.ts
|
|
@@ -86,8 +86,8 @@ var SHARED_MARK = "#";
|
|
|
86
86
|
* Deliberately NOT on the package barrel: the id is a disk format, not an API. */
|
|
87
87
|
function completionLegacyId(slug, itemId, root, aid) {
|
|
88
88
|
if (root !== void 0 && aid !== void 0) throw new Error(`completionLegacyId: "${slug}" carries both a root (${root}) and an app (${aid})`);
|
|
89
|
-
if (!
|
|
90
|
-
if (aid !== void 0 && !
|
|
89
|
+
if (!require_itemId.isValidCollectionName(slug)) throw new Error(`completionLegacyId: collection name "${slug}" is not a valid collection name`);
|
|
90
|
+
if (aid !== void 0 && !require_itemId.isValidCollectionName(aid)) throw new Error(`completionLegacyId: app id "${aid}" is not a valid collection name`);
|
|
91
91
|
return `${LEGACY_ID_PREFIX}${aid !== void 0 ? `${SHARED_MARK}${aid}${ROOT_SEP}` : root === void 0 ? "" : `${ROOT_MARK}${require_root.canonicalRoot(root)}${ROOT_SEP}`}${slug}:${itemId}`;
|
|
92
92
|
}
|
|
93
93
|
/** Decode a key back into its (root, slug, itemId), or null if the string
|
|
@@ -130,7 +130,7 @@ function parseCompletionLegacyId(legacyId) {
|
|
|
130
130
|
function resolveDisplayLabel(schema, item, itemId) {
|
|
131
131
|
const { displayField } = schema;
|
|
132
132
|
if (!displayField) return itemId;
|
|
133
|
-
const label =
|
|
133
|
+
const label = require_itemId.fieldText(item[displayField]).trim();
|
|
134
134
|
return label.length > 0 ? label : itemId;
|
|
135
135
|
}
|
|
136
136
|
/** The scope a collection's bells belong to.
|
|
@@ -714,10 +714,22 @@ function stopVanishedWatchers(gen, liveSlugs) {
|
|
|
714
714
|
}
|
|
715
715
|
return mutated;
|
|
716
716
|
}
|
|
717
|
+
/** True when the collection's records moved — a different `dataSource.path`, a
|
|
718
|
+
* different `dataPath`, a flip between the two modes, or a different APP. The
|
|
719
|
+
* mount (an fs.watch, or a Firestore listener) is bound to the OLD location,
|
|
720
|
+
* so it must be remounted, not just re-reconciled.
|
|
721
|
+
*
|
|
722
|
+
* `appId` belongs here even though it is not in the schema: it is resolved
|
|
723
|
+
* separately, from the repository's `app.json`. Point that file at another app
|
|
724
|
+
* and every read and write follows immediately, while a listener armed on the
|
|
725
|
+
* old one keeps running — so the collection would serve the NEW app's records
|
|
726
|
+
* and be woken by the OLD app's changes, forever. */
|
|
727
|
+
function backendMoved(previous, next) {
|
|
728
|
+
return previous.appId !== next.appId || storagePathChanged(previous.schema, next.schema);
|
|
729
|
+
}
|
|
717
730
|
/** True when a schema edit moved the collection's storage — a different
|
|
718
731
|
* `dataSource.path`, a different `dataPath`, or a flip between the two
|
|
719
|
-
* modes.
|
|
720
|
-
* be remounted, not just re-reconciled. */
|
|
732
|
+
* modes. */
|
|
721
733
|
function storagePathChanged(previous, next) {
|
|
722
734
|
return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath || previous.storage?.type !== next.storage?.type || storageFilePath(previous.storage) !== storageFilePath(next.storage);
|
|
723
735
|
}
|
|
@@ -729,23 +741,30 @@ function storageFilePath(storage) {
|
|
|
729
741
|
}
|
|
730
742
|
/** Re-reconcile already-watched collections whose schema changed since
|
|
731
743
|
* the last tick. New collections fall through to `startNewWatchers`. */
|
|
744
|
+
/** Unmount a collection whose records moved. `startNewWatchers` (right after
|
|
745
|
+
* this pass in syncWatchers) remounts it on the new location. A dataSource
|
|
746
|
+
* collection also gets a change ping so open views refetch immediately. */
|
|
747
|
+
function dropStaleMount(gen, existing, collection) {
|
|
748
|
+
log().info("watcher backend moved, remounting", { slug: collection.slug });
|
|
749
|
+
try {
|
|
750
|
+
existing.unsubscribe();
|
|
751
|
+
} catch {}
|
|
752
|
+
gen.watchers.delete(collection.slug);
|
|
753
|
+
if (collection.schema.dataSource !== void 0) safePublish(gen, collection, {
|
|
754
|
+
slug: collection.slug,
|
|
755
|
+
op: "upsert"
|
|
756
|
+
});
|
|
757
|
+
}
|
|
732
758
|
async function reconcileChangedSchemas(gen, collections) {
|
|
733
759
|
let mutated = false;
|
|
734
760
|
for (const collection of collections) {
|
|
735
761
|
const existing = gen.watchers.get(collection.slug);
|
|
736
762
|
if (!existing) continue;
|
|
737
763
|
const nextJson = JSON.stringify(collection.schema);
|
|
738
|
-
|
|
739
|
-
if (
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
existing.unsubscribe();
|
|
743
|
-
} catch {}
|
|
744
|
-
gen.watchers.delete(collection.slug);
|
|
745
|
-
if (collection.schema.dataSource !== void 0) safePublish(gen, {
|
|
746
|
-
slug: collection.slug,
|
|
747
|
-
op: "upsert"
|
|
748
|
-
});
|
|
764
|
+
const moved = backendMoved(existing.collection, collection);
|
|
765
|
+
if (existing.schemaJson === nextJson && !moved) continue;
|
|
766
|
+
if (moved) {
|
|
767
|
+
dropStaleMount(gen, existing, collection);
|
|
749
768
|
mutated = true;
|
|
750
769
|
continue;
|
|
751
770
|
}
|
|
@@ -753,7 +772,7 @@ async function reconcileChangedSchemas(gen, collections) {
|
|
|
753
772
|
existing.collection = collection;
|
|
754
773
|
log().info("watcher schema changed, re-reconciling", { slug: collection.slug });
|
|
755
774
|
await reconcileAllItems(collection, gen.discoveryOpts);
|
|
756
|
-
if (collection.schema.dataSource !== void 0) safePublish(gen, {
|
|
775
|
+
if (collection.schema.dataSource !== void 0) safePublish(gen, collection, {
|
|
757
776
|
slug: collection.slug,
|
|
758
777
|
op: "upsert"
|
|
759
778
|
});
|
|
@@ -841,13 +860,15 @@ async function handleStoreChange(gen, slug, change) {
|
|
|
841
860
|
* per slug so a burst of writes collapses into one pass plus one trailing
|
|
842
861
|
* re-run. */
|
|
843
862
|
function scheduleCollectionReconcile(gen, slug) {
|
|
863
|
+
let target = gen.watchers.get(slug)?.collection ?? { appId: void 0 };
|
|
844
864
|
return runSingleFlight(gen.collectionSlots, slug, async () => {
|
|
845
865
|
const collection = await require_discovery.loadCollection(slug, gen.discoveryOpts);
|
|
846
866
|
if (!collection) return;
|
|
867
|
+
target = collection;
|
|
847
868
|
await reconcileAllItems(collection, gen.discoveryOpts);
|
|
848
869
|
await sweepStaleActiveEntries(gen.discoveryOpts);
|
|
849
870
|
}, () => {
|
|
850
|
-
safePublish(gen, {
|
|
871
|
+
safePublish(gen, target, {
|
|
851
872
|
slug,
|
|
852
873
|
op: "upsert"
|
|
853
874
|
});
|
|
@@ -911,13 +932,29 @@ function runSingleFlight(slots, key, pass, onSettled) {
|
|
|
911
932
|
* file-backed collections reach here; a `storage` (db) collection has no
|
|
912
933
|
* per-record file and publishes wholesale in `scheduleStorageReconcile`. */
|
|
913
934
|
async function publishItemChange(gen, collection, itemId) {
|
|
914
|
-
const changeOp = await
|
|
915
|
-
safePublish(gen, {
|
|
935
|
+
const changeOp = await recordStillExists(gen, collection, itemId) ? "upsert" : "delete";
|
|
936
|
+
safePublish(gen, collection, {
|
|
916
937
|
slug: collection.slug,
|
|
917
938
|
ids: [itemId],
|
|
918
939
|
op: changeOp
|
|
919
940
|
});
|
|
920
941
|
}
|
|
942
|
+
/** Upsert or delete? Answered by whatever actually holds the record.
|
|
943
|
+
*
|
|
944
|
+
* A shared collection's records are Firestore documents — there is no item
|
|
945
|
+
* file, so the file check answers "gone" for every one of them and each live
|
|
946
|
+
* change would be published as a DELETE. Asking the store is the question the
|
|
947
|
+
* file check was standing in for; the file path keeps it because it is
|
|
948
|
+
* cheaper and its semantics (an unreadable dir, an id that is not a safe
|
|
949
|
+
* filename) are already settled there. */
|
|
950
|
+
async function recordStillExists(gen, collection, itemId) {
|
|
951
|
+
if (collection.appId === void 0) return itemFileExists(collection.dataDir, itemId);
|
|
952
|
+
try {
|
|
953
|
+
return await require_discovery.storeFor(collection, gen.discoveryOpts).read(itemId) !== null;
|
|
954
|
+
} catch {
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
921
958
|
async function itemFileExists(dataDir, itemId) {
|
|
922
959
|
try {
|
|
923
960
|
await (0, node_fs_promises.access)(require_discovery.itemFilePath(dataDir, itemId));
|
|
@@ -934,8 +971,15 @@ async function itemFileExists(dataDir, itemId) {
|
|
|
934
971
|
* carries only absolute paths, and a root reconstructed by string surgery
|
|
935
972
|
* would be a guess. Undefined in a single-workspace host, which is exactly
|
|
936
973
|
* what the payload contract means by "the host's configured root". */
|
|
937
|
-
|
|
938
|
-
|
|
974
|
+
/** Publish a change on the channel the collection actually lives on.
|
|
975
|
+
*
|
|
976
|
+
* A SHARED collection is not keyed by this checkout: its identity is
|
|
977
|
+
* `(aid, cid)`, and the same collection is open in every clone. Publishing it
|
|
978
|
+
* as `(root, slug)` sends the refresh to a channel no shared subscriber is
|
|
979
|
+
* listening on — the live update simply never arrives, and nothing errors.
|
|
980
|
+
* That is why this takes the collection rather than a bare slug. */
|
|
981
|
+
function safePublish(gen, collection, base) {
|
|
982
|
+
const enriched = collection.appId === void 0 ? require_discovery.collectionChangePayload(base, gen.discoveryOpts.workspaceRoot) : require_discovery.sharedCollectionChangePayload(base, collection.appId);
|
|
939
983
|
try {
|
|
940
984
|
require_discovery.publishCollectionChange(enriched);
|
|
941
985
|
} catch (err) {
|