@frockbot/plugin-bot-template 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frockbot.json +43 -0
- package/package.json +47 -6
- package/src/agent.test.ts +194 -0
- package/src/agent.ts +221 -0
- package/src/backend.test.ts +326 -0
- package/src/backend.ts +262 -0
- package/src/client/BotTemplateImportSection.vue +325 -0
- package/src/client/BotTemplateSection.vue +338 -0
- package/src/client/index.ts +210 -0
- package/src/client/state.ts +43 -0
- package/src/env.d.ts +6 -0
- package/src/import-apply.test.ts +455 -0
- package/src/import.test.ts +266 -0
- package/src/import.ts +292 -0
- package/src/index.ts +9 -0
- package/src/manifest.ts +3 -0
- package/src/scrub.test.ts +528 -0
- package/src/scrub.ts +523 -0
- package/src/shared.ts +620 -0
- package/src/user.test.ts +361 -0
- package/src/user.ts +913 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +31 -0
- package/README.md +0 -3
package/src/shared.ts
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
// The Bot Template Package's wire shapes: everything that crosses a seam.
|
|
2
|
+
//
|
|
3
|
+
// "Cross-runtime communication uses narrow, versioned DTOs, and every inbound
|
|
4
|
+
// value is decoded at its seam." The template *document* lives in
|
|
5
|
+
// `@frockbot/template-core` beside `catalog-core`, because it is a product
|
|
6
|
+
// artifact several runtimes read without this Package mounted. What lives here
|
|
7
|
+
// is the traffic around it: the commands a User issues, the receipts they get
|
|
8
|
+
// back, and the summary of what an export packed and what it scrubbed.
|
|
9
|
+
//
|
|
10
|
+
// Nothing here carries a credential, a `connectionId`, an Assignment, or a
|
|
11
|
+
// webhook key, and the decoders refuse an unknown key, so a shape that grew one
|
|
12
|
+
// would fail at the seam rather than travel.
|
|
13
|
+
import {
|
|
14
|
+
decodeTemplateShareRecordV1,
|
|
15
|
+
decodeTemplateVisibilityV1,
|
|
16
|
+
parseTemplateShareIdV1,
|
|
17
|
+
TemplateDecodeError,
|
|
18
|
+
type TemplateShareRecordV1,
|
|
19
|
+
type TemplateVisibilityV1,
|
|
20
|
+
} from "@frockbot/template-core";
|
|
21
|
+
|
|
22
|
+
export { TemplateDecodeError };
|
|
23
|
+
|
|
24
|
+
/** Why something on the Bot did not reach the template. */
|
|
25
|
+
export const TEMPLATE_OMISSION_REASONS_V1 = [
|
|
26
|
+
"managed-skill",
|
|
27
|
+
"plugin-skill",
|
|
28
|
+
"unattributed-skill",
|
|
29
|
+
"unreadable-skill",
|
|
30
|
+
"first-party-package",
|
|
31
|
+
"package-values",
|
|
32
|
+
"connection",
|
|
33
|
+
"assignment",
|
|
34
|
+
"model",
|
|
35
|
+
"memory",
|
|
36
|
+
"private-network-server",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
export type TemplateOmissionReasonV1 =
|
|
40
|
+
(typeof TEMPLATE_OMISSION_REASONS_V1)[number];
|
|
41
|
+
|
|
42
|
+
export interface TemplateOmissionV1 {
|
|
43
|
+
reason: TemplateOmissionReasonV1;
|
|
44
|
+
count: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What one export packed and what it left behind.
|
|
49
|
+
*
|
|
50
|
+
* The card a Bot returns and the dialog a User confirms in are both drawn from
|
|
51
|
+
* this, so "what was scrubbed" is durable state rather than prose a tool
|
|
52
|
+
* happened to write.
|
|
53
|
+
*/
|
|
54
|
+
export interface TemplateExportSummaryV1 {
|
|
55
|
+
schemaVersion: 1;
|
|
56
|
+
botId: string;
|
|
57
|
+
skills: number;
|
|
58
|
+
routines: number;
|
|
59
|
+
packages: number;
|
|
60
|
+
publicServers: number;
|
|
61
|
+
needsConnection: number;
|
|
62
|
+
omitted: TemplateOmissionV1[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface TemplateCommandMetaV1 {
|
|
66
|
+
schemaVersion: 1;
|
|
67
|
+
commandId: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type TemplateCommandV1 =
|
|
71
|
+
| (TemplateCommandMetaV1 & { type: "template/stage"; botId: string })
|
|
72
|
+
| (TemplateCommandMetaV1 & { type: "template/plan-import"; shareId: string })
|
|
73
|
+
| (TemplateCommandMetaV1 & {
|
|
74
|
+
type: "template/apply-import";
|
|
75
|
+
importId: string;
|
|
76
|
+
})
|
|
77
|
+
| (TemplateCommandMetaV1 & {
|
|
78
|
+
type: "template/set-visibility";
|
|
79
|
+
shareId: string;
|
|
80
|
+
visibility: TemplateVisibilityV1;
|
|
81
|
+
})
|
|
82
|
+
| (TemplateCommandMetaV1 & { type: "template/revoke"; shareId: string });
|
|
83
|
+
|
|
84
|
+
export const TEMPLATE_COMMAND_TYPES_V1: readonly TemplateCommandV1["type"][] = [
|
|
85
|
+
"template/stage",
|
|
86
|
+
"template/plan-import",
|
|
87
|
+
"template/apply-import",
|
|
88
|
+
"template/set-visibility",
|
|
89
|
+
"template/revoke",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
export interface TemplateShareReceiptV1 {
|
|
93
|
+
schemaVersion: 1;
|
|
94
|
+
commandId: string;
|
|
95
|
+
status: "applied";
|
|
96
|
+
share: TemplateShareRecordV1;
|
|
97
|
+
/** Present on a stage; a visibility change repacks nothing. */
|
|
98
|
+
summary?: TemplateExportSummaryV1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface TemplateShareListViewV1 {
|
|
102
|
+
schemaVersion: 1;
|
|
103
|
+
shares: TemplateShareRecordV1[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Most shares one User may hold, so a staging loop cannot fill the object. */
|
|
107
|
+
export const MAX_TEMPLATE_SHARES_V1 = 200;
|
|
108
|
+
|
|
109
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
110
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
111
|
+
throw new TemplateDecodeError(`${label} must be an object`);
|
|
112
|
+
}
|
|
113
|
+
return value as Record<string, unknown>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function exact(
|
|
117
|
+
value: Record<string, unknown>,
|
|
118
|
+
required: readonly string[],
|
|
119
|
+
label: string,
|
|
120
|
+
): void {
|
|
121
|
+
const allowed = new Set(required);
|
|
122
|
+
for (const key of Object.keys(value)) {
|
|
123
|
+
if (!allowed.has(key)) {
|
|
124
|
+
throw new TemplateDecodeError(`${label} has unknown field "${key}"`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const key of required) {
|
|
128
|
+
if (!Object.hasOwn(value, key)) {
|
|
129
|
+
throw new TemplateDecodeError(`${label} is missing "${key}"`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function identifier(value: unknown, label: string, maximum = 128): string {
|
|
135
|
+
if (
|
|
136
|
+
typeof value !== "string" ||
|
|
137
|
+
value.length === 0 ||
|
|
138
|
+
value.length > maximum ||
|
|
139
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/.test(value)
|
|
140
|
+
) {
|
|
141
|
+
throw new TemplateDecodeError(`${label} is invalid`);
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function count(value: unknown, label: string): number {
|
|
147
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
|
148
|
+
throw new TemplateDecodeError(`${label} is invalid`);
|
|
149
|
+
}
|
|
150
|
+
return value as number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function decodeTemplateCommandV1(input: unknown): TemplateCommandV1 {
|
|
154
|
+
const value = record(input, "template command");
|
|
155
|
+
if (value.schemaVersion !== 1) {
|
|
156
|
+
throw new TemplateDecodeError(
|
|
157
|
+
"template command schemaVersion is unsupported",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const type = TEMPLATE_COMMAND_TYPES_V1.find((known) => known === value.type);
|
|
161
|
+
if (!type) throw new TemplateDecodeError("template command type is unknown");
|
|
162
|
+
if (type === "template/stage") {
|
|
163
|
+
exact(value, ["schemaVersion", "type", "commandId", "botId"], type);
|
|
164
|
+
return {
|
|
165
|
+
schemaVersion: 1,
|
|
166
|
+
type,
|
|
167
|
+
commandId: identifier(value.commandId, "template commandId"),
|
|
168
|
+
botId: identifier(value.botId, "template command botId"),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (type === "template/plan-import") {
|
|
172
|
+
exact(value, ["schemaVersion", "type", "commandId", "shareId"], type);
|
|
173
|
+
parseTemplateShareIdV1(value.shareId);
|
|
174
|
+
return {
|
|
175
|
+
schemaVersion: 1,
|
|
176
|
+
type,
|
|
177
|
+
commandId: identifier(value.commandId, "template commandId"),
|
|
178
|
+
shareId: value.shareId as string,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (type === "template/apply-import") {
|
|
182
|
+
exact(value, ["schemaVersion", "type", "commandId", "importId"], type);
|
|
183
|
+
return {
|
|
184
|
+
schemaVersion: 1,
|
|
185
|
+
type,
|
|
186
|
+
commandId: identifier(value.commandId, "template commandId"),
|
|
187
|
+
importId: identifier(value.importId, "template importId"),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (type === "template/set-visibility") {
|
|
191
|
+
exact(
|
|
192
|
+
value,
|
|
193
|
+
["schemaVersion", "type", "commandId", "shareId", "visibility"],
|
|
194
|
+
type,
|
|
195
|
+
);
|
|
196
|
+
parseTemplateShareIdV1(value.shareId);
|
|
197
|
+
return {
|
|
198
|
+
schemaVersion: 1,
|
|
199
|
+
type,
|
|
200
|
+
commandId: identifier(value.commandId, "template commandId"),
|
|
201
|
+
shareId: value.shareId as string,
|
|
202
|
+
visibility: decodeTemplateVisibilityV1(value.visibility),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
exact(value, ["schemaVersion", "type", "commandId", "shareId"], type);
|
|
206
|
+
parseTemplateShareIdV1(value.shareId);
|
|
207
|
+
return {
|
|
208
|
+
schemaVersion: 1,
|
|
209
|
+
type,
|
|
210
|
+
commandId: identifier(value.commandId, "template commandId"),
|
|
211
|
+
shareId: value.shareId as string,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function decodeTemplateOmissionV1(input: unknown): TemplateOmissionV1 {
|
|
216
|
+
const value = record(input, "template omission");
|
|
217
|
+
exact(value, ["reason", "count"], "template omission");
|
|
218
|
+
const reason = TEMPLATE_OMISSION_REASONS_V1.find(
|
|
219
|
+
(known) => known === value.reason,
|
|
220
|
+
);
|
|
221
|
+
if (!reason) {
|
|
222
|
+
throw new TemplateDecodeError("template omission reason is unknown");
|
|
223
|
+
}
|
|
224
|
+
return { reason, count: count(value.count, "template omission count") };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function decodeTemplateExportSummaryV1(
|
|
228
|
+
input: unknown,
|
|
229
|
+
): TemplateExportSummaryV1 {
|
|
230
|
+
const value = record(input, "template summary");
|
|
231
|
+
exact(
|
|
232
|
+
value,
|
|
233
|
+
[
|
|
234
|
+
"schemaVersion",
|
|
235
|
+
"botId",
|
|
236
|
+
"skills",
|
|
237
|
+
"routines",
|
|
238
|
+
"packages",
|
|
239
|
+
"publicServers",
|
|
240
|
+
"needsConnection",
|
|
241
|
+
"omitted",
|
|
242
|
+
],
|
|
243
|
+
"template summary",
|
|
244
|
+
);
|
|
245
|
+
if (value.schemaVersion !== 1) {
|
|
246
|
+
throw new TemplateDecodeError("template summary schemaVersion is invalid");
|
|
247
|
+
}
|
|
248
|
+
if (
|
|
249
|
+
!Array.isArray(value.omitted) ||
|
|
250
|
+
value.omitted.length > TEMPLATE_OMISSION_REASONS_V1.length
|
|
251
|
+
) {
|
|
252
|
+
throw new TemplateDecodeError("template summary omissions are invalid");
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
schemaVersion: 1,
|
|
256
|
+
botId: identifier(value.botId, "template summary botId"),
|
|
257
|
+
skills: count(value.skills, "template summary skills"),
|
|
258
|
+
routines: count(value.routines, "template summary routines"),
|
|
259
|
+
packages: count(value.packages, "template summary packages"),
|
|
260
|
+
publicServers: count(value.publicServers, "template summary publicServers"),
|
|
261
|
+
needsConnection: count(
|
|
262
|
+
value.needsConnection,
|
|
263
|
+
"template summary needsConnection",
|
|
264
|
+
),
|
|
265
|
+
omitted: value.omitted.map(decodeTemplateOmissionV1),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function decodeTemplateShareReceiptV1(
|
|
270
|
+
input: unknown,
|
|
271
|
+
): TemplateShareReceiptV1 {
|
|
272
|
+
const value = record(input, "template receipt");
|
|
273
|
+
const keys = ["schemaVersion", "commandId", "status", "share"];
|
|
274
|
+
if (Object.hasOwn(value, "summary")) keys.push("summary");
|
|
275
|
+
exact(value, keys, "template receipt");
|
|
276
|
+
if (value.schemaVersion !== 1 || value.status !== "applied") {
|
|
277
|
+
throw new TemplateDecodeError("template receipt is invalid");
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
schemaVersion: 1,
|
|
281
|
+
commandId: identifier(value.commandId, "template receipt commandId"),
|
|
282
|
+
status: "applied",
|
|
283
|
+
share: decodeTemplateShareRecordV1(value.share),
|
|
284
|
+
...(value.summary === undefined
|
|
285
|
+
? {}
|
|
286
|
+
: { summary: decodeTemplateExportSummaryV1(value.summary) }),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function decodeTemplateShareListViewV1(
|
|
291
|
+
input: unknown,
|
|
292
|
+
): TemplateShareListViewV1 {
|
|
293
|
+
const value = record(input, "template share list");
|
|
294
|
+
exact(value, ["schemaVersion", "shares"], "template share list");
|
|
295
|
+
if (
|
|
296
|
+
value.schemaVersion !== 1 ||
|
|
297
|
+
!Array.isArray(value.shares) ||
|
|
298
|
+
value.shares.length > MAX_TEMPLATE_SHARES_V1
|
|
299
|
+
) {
|
|
300
|
+
throw new TemplateDecodeError("template share list is invalid");
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
schemaVersion: 1,
|
|
304
|
+
shares: value.shares.map(decodeTemplateShareRecordV1),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The idempotency fingerprint of one template command. */
|
|
309
|
+
export function templateCommandFingerprintV1(
|
|
310
|
+
command: TemplateCommandV1,
|
|
311
|
+
): string {
|
|
312
|
+
const { commandId: _commandId, ...semantic } = command;
|
|
313
|
+
return JSON.stringify([
|
|
314
|
+
"bot-template-command-v1",
|
|
315
|
+
semantic.type,
|
|
316
|
+
"botId" in semantic ? semantic.botId : null,
|
|
317
|
+
"shareId" in semantic ? semantic.shareId : null,
|
|
318
|
+
"importId" in semantic ? semantic.importId : null,
|
|
319
|
+
"visibility" in semantic ? semantic.visibility : null,
|
|
320
|
+
]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The path a `link` share is reachable at, relative to the deployment origin. */
|
|
324
|
+
export function templateSharePathV1(shareId: string): string {
|
|
325
|
+
parseTemplateShareIdV1(shareId);
|
|
326
|
+
return `/templates/v1/${encodeURIComponent(shareId)}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Import: the durable record and the review card.
|
|
331
|
+
//
|
|
332
|
+
// "Failures are observable through durable state rather than existing only in
|
|
333
|
+
// process logs or client memory." An import is a saga with one receipt per
|
|
334
|
+
// step, so a Bot that came out half-built says exactly which step failed and
|
|
335
|
+
// why, and re-applying resumes from there rather than starting again.
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
export const TEMPLATE_IMPORT_STEP_STATUSES_V1 = [
|
|
339
|
+
"pending",
|
|
340
|
+
"in-flight",
|
|
341
|
+
"done",
|
|
342
|
+
"skipped",
|
|
343
|
+
"failed",
|
|
344
|
+
] as const;
|
|
345
|
+
|
|
346
|
+
export type TemplateImportStepStatusV1 =
|
|
347
|
+
(typeof TEMPLATE_IMPORT_STEP_STATUSES_V1)[number];
|
|
348
|
+
|
|
349
|
+
export interface TemplateImportStepReceiptV1 {
|
|
350
|
+
key: string;
|
|
351
|
+
kind:
|
|
352
|
+
| "bot/create"
|
|
353
|
+
| "user/install-package"
|
|
354
|
+
| "skill/write"
|
|
355
|
+
| "routine/create"
|
|
356
|
+
| "routine/disable";
|
|
357
|
+
status: TemplateImportStepStatusV1;
|
|
358
|
+
subject?: string;
|
|
359
|
+
/** What the step produced: a generation id, a routine id, a receipt id. */
|
|
360
|
+
detail?: string;
|
|
361
|
+
failure?: string;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export const TEMPLATE_IMPORT_STATUSES_V1 = [
|
|
365
|
+
"planned",
|
|
366
|
+
"applying",
|
|
367
|
+
"applied",
|
|
368
|
+
"failed",
|
|
369
|
+
] as const;
|
|
370
|
+
|
|
371
|
+
export type TemplateImportStatusV1 =
|
|
372
|
+
(typeof TEMPLATE_IMPORT_STATUSES_V1)[number];
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* One import, as the User Durable Object holds it and the client renders it.
|
|
376
|
+
*
|
|
377
|
+
* `status: "planned"` is the state a review card is shown in, and nothing has
|
|
378
|
+
* been applied in it. Only an explicit `template/apply-import` moves it on,
|
|
379
|
+
* which is what "Nothing is applied before the User confirms" means in durable
|
|
380
|
+
* state rather than in a component's `v-if`.
|
|
381
|
+
*/
|
|
382
|
+
export interface TemplateImportRecordV1 {
|
|
383
|
+
schemaVersion: 1;
|
|
384
|
+
importId: string;
|
|
385
|
+
shareId: string;
|
|
386
|
+
hash: string;
|
|
387
|
+
botId: string;
|
|
388
|
+
status: TemplateImportStatusV1;
|
|
389
|
+
botName: string;
|
|
390
|
+
packages: {
|
|
391
|
+
catalogId: string;
|
|
392
|
+
packageId: string;
|
|
393
|
+
displayName: string;
|
|
394
|
+
version: string;
|
|
395
|
+
status: "will-install" | "already-installed" | "missing";
|
|
396
|
+
}[];
|
|
397
|
+
connections: {
|
|
398
|
+
name: string;
|
|
399
|
+
connectionTypeId?: string;
|
|
400
|
+
url?: string;
|
|
401
|
+
hint?: string;
|
|
402
|
+
}[];
|
|
403
|
+
skills: string[];
|
|
404
|
+
routines: { slug: string; disabled: boolean }[];
|
|
405
|
+
steps: TemplateImportStepReceiptV1[];
|
|
406
|
+
createdAt: string;
|
|
407
|
+
updatedAt: string;
|
|
408
|
+
catalogGeneration?: string;
|
|
409
|
+
failure?: string;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export interface TemplateImportListViewV1 {
|
|
413
|
+
schemaVersion: 1;
|
|
414
|
+
imports: TemplateImportRecordV1[];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface TemplateImportReceiptV1 {
|
|
418
|
+
schemaVersion: 1;
|
|
419
|
+
commandId: string;
|
|
420
|
+
status: "applied";
|
|
421
|
+
import: TemplateImportRecordV1;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Most imports one User may hold, so planning cannot fill the object. */
|
|
425
|
+
export const MAX_TEMPLATE_IMPORTS_V1 = 100;
|
|
426
|
+
|
|
427
|
+
function importStatus(value: unknown): TemplateImportStatusV1 {
|
|
428
|
+
const found = TEMPLATE_IMPORT_STATUSES_V1.find((known) => known === value);
|
|
429
|
+
if (!found)
|
|
430
|
+
throw new TemplateDecodeError("template import status is invalid");
|
|
431
|
+
return found;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function importStepStatus(value: unknown): TemplateImportStepStatusV1 {
|
|
435
|
+
const found = TEMPLATE_IMPORT_STEP_STATUSES_V1.find(
|
|
436
|
+
(known) => known === value,
|
|
437
|
+
);
|
|
438
|
+
if (!found) {
|
|
439
|
+
throw new TemplateDecodeError("template import step status is invalid");
|
|
440
|
+
}
|
|
441
|
+
return found;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const IMPORT_STEP_KINDS = [
|
|
445
|
+
"bot/create",
|
|
446
|
+
"user/install-package",
|
|
447
|
+
"skill/write",
|
|
448
|
+
"routine/create",
|
|
449
|
+
"routine/disable",
|
|
450
|
+
] as const;
|
|
451
|
+
|
|
452
|
+
function optional(value: unknown, label: string, maximum: number) {
|
|
453
|
+
if (value === undefined) return undefined;
|
|
454
|
+
if (typeof value !== "string" || value.length > maximum) {
|
|
455
|
+
throw new TemplateDecodeError(`${label} is invalid`);
|
|
456
|
+
}
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function required(value: unknown, label: string, maximum: number): string {
|
|
461
|
+
if (typeof value !== "string" || !value || value.length > maximum) {
|
|
462
|
+
throw new TemplateDecodeError(`${label} is invalid`);
|
|
463
|
+
}
|
|
464
|
+
return value;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function decodeTemplateImportStepReceiptV1(
|
|
468
|
+
input: unknown,
|
|
469
|
+
): TemplateImportStepReceiptV1 {
|
|
470
|
+
const value = record(input, "template import step");
|
|
471
|
+
const kind = IMPORT_STEP_KINDS.find((known) => known === value.kind);
|
|
472
|
+
if (!kind) {
|
|
473
|
+
throw new TemplateDecodeError("template import step kind is unknown");
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
key: required(value.key, "template import step key", 200),
|
|
477
|
+
kind,
|
|
478
|
+
status: importStepStatus(value.status),
|
|
479
|
+
...(value.subject === undefined
|
|
480
|
+
? {}
|
|
481
|
+
: { subject: required(value.subject, "step subject", 200) }),
|
|
482
|
+
...(value.detail === undefined
|
|
483
|
+
? {}
|
|
484
|
+
: { detail: required(value.detail, "step detail", 500) }),
|
|
485
|
+
...(value.failure === undefined
|
|
486
|
+
? {}
|
|
487
|
+
: { failure: required(value.failure, "step failure", 2_000) }),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function decodeTemplateImportRecordV1(
|
|
492
|
+
input: unknown,
|
|
493
|
+
): TemplateImportRecordV1 {
|
|
494
|
+
const value = record(input, "template import");
|
|
495
|
+
if (value.schemaVersion !== 1) {
|
|
496
|
+
throw new TemplateDecodeError("template import schemaVersion is invalid");
|
|
497
|
+
}
|
|
498
|
+
if (
|
|
499
|
+
!Array.isArray(value.steps) ||
|
|
500
|
+
!Array.isArray(value.packages) ||
|
|
501
|
+
!Array.isArray(value.connections) ||
|
|
502
|
+
!Array.isArray(value.skills) ||
|
|
503
|
+
!Array.isArray(value.routines)
|
|
504
|
+
) {
|
|
505
|
+
throw new TemplateDecodeError("template import sections are invalid");
|
|
506
|
+
}
|
|
507
|
+
parseTemplateShareIdV1(value.shareId);
|
|
508
|
+
return {
|
|
509
|
+
schemaVersion: 1,
|
|
510
|
+
importId: identifier(value.importId, "template importId"),
|
|
511
|
+
shareId: value.shareId as string,
|
|
512
|
+
hash: required(value.hash, "template import hash", 64),
|
|
513
|
+
botId: required(value.botId, "template import botId", 128),
|
|
514
|
+
status: importStatus(value.status),
|
|
515
|
+
botName: required(value.botName, "template import botName", 100),
|
|
516
|
+
packages: value.packages.map((entry) => {
|
|
517
|
+
const line = record(entry, "template import package");
|
|
518
|
+
const status = ["will-install", "already-installed", "missing"].find(
|
|
519
|
+
(known) => known === line.status,
|
|
520
|
+
);
|
|
521
|
+
if (!status) {
|
|
522
|
+
throw new TemplateDecodeError(
|
|
523
|
+
"template import package status is invalid",
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
catalogId: required(line.catalogId, "catalogId", 64),
|
|
528
|
+
packageId: required(line.packageId, "packageId", 64),
|
|
529
|
+
displayName: required(line.displayName, "displayName", 100),
|
|
530
|
+
version: required(line.version, "version", 100),
|
|
531
|
+
status: status as "will-install" | "already-installed" | "missing",
|
|
532
|
+
};
|
|
533
|
+
}),
|
|
534
|
+
connections: value.connections.map((entry) => {
|
|
535
|
+
const line = record(entry, "template import connection");
|
|
536
|
+
return {
|
|
537
|
+
name: required(line.name, "connection name", 100),
|
|
538
|
+
...(line.connectionTypeId === undefined
|
|
539
|
+
? {}
|
|
540
|
+
: {
|
|
541
|
+
connectionTypeId: required(
|
|
542
|
+
line.connectionTypeId,
|
|
543
|
+
"connectionTypeId",
|
|
544
|
+
64,
|
|
545
|
+
),
|
|
546
|
+
}),
|
|
547
|
+
...(line.url === undefined
|
|
548
|
+
? {}
|
|
549
|
+
: { url: required(line.url, "connection url", 2_048) }),
|
|
550
|
+
...(line.hint === undefined
|
|
551
|
+
? {}
|
|
552
|
+
: { hint: required(line.hint, "connection hint", 500) }),
|
|
553
|
+
};
|
|
554
|
+
}),
|
|
555
|
+
skills: value.skills.map((slug) => required(slug, "skill slug", 128)),
|
|
556
|
+
routines: value.routines.map((entry) => {
|
|
557
|
+
const line = record(entry, "template import routine");
|
|
558
|
+
if (typeof line.disabled !== "boolean") {
|
|
559
|
+
throw new TemplateDecodeError("template import routine is invalid");
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
slug: required(line.slug, "routine slug", 128),
|
|
563
|
+
disabled: line.disabled,
|
|
564
|
+
};
|
|
565
|
+
}),
|
|
566
|
+
steps: value.steps.map(decodeTemplateImportStepReceiptV1),
|
|
567
|
+
createdAt: required(value.createdAt, "template import createdAt", 64),
|
|
568
|
+
updatedAt: required(value.updatedAt, "template import updatedAt", 64),
|
|
569
|
+
...(value.catalogGeneration === undefined
|
|
570
|
+
? {}
|
|
571
|
+
: {
|
|
572
|
+
catalogGeneration: required(
|
|
573
|
+
value.catalogGeneration,
|
|
574
|
+
"catalogGeneration",
|
|
575
|
+
64,
|
|
576
|
+
),
|
|
577
|
+
}),
|
|
578
|
+
...(value.failure === undefined
|
|
579
|
+
? {}
|
|
580
|
+
: { failure: required(value.failure, "template import failure", 2_000) }),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function decodeTemplateImportReceiptV1(
|
|
585
|
+
input: unknown,
|
|
586
|
+
): TemplateImportReceiptV1 {
|
|
587
|
+
const value = record(input, "template import receipt");
|
|
588
|
+
exact(
|
|
589
|
+
value,
|
|
590
|
+
["schemaVersion", "commandId", "status", "import"],
|
|
591
|
+
"template import receipt",
|
|
592
|
+
);
|
|
593
|
+
if (value.schemaVersion !== 1 || value.status !== "applied") {
|
|
594
|
+
throw new TemplateDecodeError("template import receipt is invalid");
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
schemaVersion: 1,
|
|
598
|
+
commandId: identifier(value.commandId, "template receipt commandId"),
|
|
599
|
+
status: "applied",
|
|
600
|
+
import: decodeTemplateImportRecordV1(value.import),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export function decodeTemplateImportListViewV1(
|
|
605
|
+
input: unknown,
|
|
606
|
+
): TemplateImportListViewV1 {
|
|
607
|
+
const value = record(input, "template import list");
|
|
608
|
+
exact(value, ["schemaVersion", "imports"], "template import list");
|
|
609
|
+
if (
|
|
610
|
+
value.schemaVersion !== 1 ||
|
|
611
|
+
!Array.isArray(value.imports) ||
|
|
612
|
+
value.imports.length > MAX_TEMPLATE_IMPORTS_V1
|
|
613
|
+
) {
|
|
614
|
+
throw new TemplateDecodeError("template import list is invalid");
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
schemaVersion: 1,
|
|
618
|
+
imports: value.imports.map(decodeTemplateImportRecordV1),
|
|
619
|
+
};
|
|
620
|
+
}
|