@brydio/manifest 0.1.0-alpha.6 → 0.1.0-alpha.9
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/package.json +2 -2
- package/src/base.d.ts +17 -1
- package/src/base.js +13 -1
- package/src/bundle.d.ts +8 -1
- package/src/bundle.js +28 -3
- package/src/grants.d.ts +1 -1
- package/src/grants.js +1 -1
- package/src/index.d.ts +5 -4
- package/src/index.js +5 -4
- package/src/requirements.d.ts +122 -0
- package/src/requirements.js +692 -0
- package/src/schema.d.ts +182 -2
- package/src/schema.js +181 -8
- package/src/secrets.d.ts +14 -0
- package/src/secrets.js +27 -5
package/src/schema.d.ts
CHANGED
|
@@ -26,6 +26,20 @@ declare const placementSettingSchema: z.ZodObject<{
|
|
|
26
26
|
placeholder: z.ZodOptional<z.ZodString>;
|
|
27
27
|
}, z.core.$strip>;
|
|
28
28
|
export type PlacementSettingSpec = z.infer<typeof placementSettingSchema>;
|
|
29
|
+
/**
|
|
30
|
+
* How deep a folder's rows may nest, counting its own rows as the first
|
|
31
|
+
* level (ADR-A21): Issues → sprints → issues is two. Each level is listed
|
|
32
|
+
* only when a person opens its parent, so depth costs nothing while closed;
|
|
33
|
+
* the limit is for the sidebar's width and a person's patience.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MAX_FOLDER_DEPTH = 4;
|
|
36
|
+
/** A stable identity for one independently addable placement offering. */
|
|
37
|
+
export declare const PLACEMENT_KEY: RegExp;
|
|
38
|
+
export declare function effectivePlacementKey(one: {
|
|
39
|
+
key?: string;
|
|
40
|
+
kind: string;
|
|
41
|
+
screen: string;
|
|
42
|
+
}): string;
|
|
29
43
|
/** Most custom tools one app may declare (A3-F08). */
|
|
30
44
|
export declare const MAX_CUSTOM_TOOLS = 20;
|
|
31
45
|
/** A tool name the model and a policy row can both use as it is. */
|
|
@@ -43,12 +57,40 @@ declare const customToolSchema: z.ZodObject<{
|
|
|
43
57
|
collection: z.ZodOptional<z.ZodString>;
|
|
44
58
|
}, z.core.$strip>;
|
|
45
59
|
export type CustomToolSpec = z.infer<typeof customToolSchema>;
|
|
60
|
+
/** Most secrets one app may declare (ADR-A24). */
|
|
61
|
+
export declare const MAX_APP_SECRETS = 20;
|
|
62
|
+
/** The longest value one secret may hold, in characters. */
|
|
63
|
+
export declare const MAX_SECRET_CHARS: number;
|
|
64
|
+
/** A secret's name, as a handler asks for it: `api_key`. */
|
|
65
|
+
export declare const SECRET_NAME: RegExp;
|
|
66
|
+
/**
|
|
67
|
+
* A secret the app needs (ADR-A24): named here, never valued here. The value
|
|
68
|
+
* is entered in the app's settings by an administrator, or stored by the
|
|
69
|
+
* app's own handler with `secrets.set`, and only that app's handlers can read
|
|
70
|
+
* it, with `secrets.get`. A screen never can.
|
|
71
|
+
*
|
|
72
|
+
* `install` (the default) is one value for the whole install; `instance` is
|
|
73
|
+
* one per instance.
|
|
74
|
+
*/
|
|
75
|
+
declare const secretSchema: z.ZodObject<{
|
|
76
|
+
name: z.ZodString;
|
|
77
|
+
label: z.ZodString;
|
|
78
|
+
description: z.ZodOptional<z.ZodString>;
|
|
79
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
80
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
81
|
+
instance: "instance";
|
|
82
|
+
install: "install";
|
|
83
|
+
}>>;
|
|
84
|
+
}, z.core.$strip>;
|
|
85
|
+
export type SecretSpec = z.infer<typeof secretSchema>;
|
|
46
86
|
declare const extensionShape: {
|
|
47
87
|
placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
88
|
+
key: z.ZodOptional<z.ZodString>;
|
|
48
89
|
kind: z.ZodEnum<{
|
|
49
90
|
"project-tab": "project-tab";
|
|
50
91
|
"project-sidebar": "project-sidebar";
|
|
51
92
|
"workspace-sidebar": "workspace-sidebar";
|
|
93
|
+
home: "home";
|
|
52
94
|
}>;
|
|
53
95
|
screen: z.ZodString;
|
|
54
96
|
label: z.ZodOptional<z.ZodString>;
|
|
@@ -62,6 +104,27 @@ declare const extensionShape: {
|
|
|
62
104
|
required: z.ZodOptional<z.ZodBoolean>;
|
|
63
105
|
placeholder: z.ZodOptional<z.ZodString>;
|
|
64
106
|
}, z.core.$strip>>>;
|
|
107
|
+
children: z.ZodOptional<z.ZodObject<{
|
|
108
|
+
tool: z.ZodString;
|
|
109
|
+
refreshSeconds: z.ZodOptional<z.ZodNumber>;
|
|
110
|
+
cap: z.ZodOptional<z.ZodNumber>;
|
|
111
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
112
|
+
create: z.ZodOptional<z.ZodObject<{
|
|
113
|
+
tool: z.ZodString;
|
|
114
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
115
|
+
titleField: z.ZodOptional<z.ZodString>;
|
|
116
|
+
parentField: z.ZodOptional<z.ZodString>;
|
|
117
|
+
}, z.core.$strip>>;
|
|
118
|
+
nested: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
119
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
120
|
+
create: z.ZodOptional<z.ZodObject<{
|
|
121
|
+
tool: z.ZodString;
|
|
122
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
123
|
+
titleField: z.ZodOptional<z.ZodString>;
|
|
124
|
+
parentField: z.ZodOptional<z.ZodString>;
|
|
125
|
+
}, z.core.$strip>>;
|
|
126
|
+
}, z.core.$strip>>>;
|
|
127
|
+
}, z.core.$strip>>;
|
|
65
128
|
}, z.core.$strip>>>;
|
|
66
129
|
data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
67
130
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
@@ -87,6 +150,17 @@ declare const extensionShape: {
|
|
|
87
150
|
collections: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
88
151
|
host: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
89
152
|
}, z.core.$strip>>;
|
|
153
|
+
/** The secrets its handlers read (ADR-A24): names only, never values. */
|
|
154
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
155
|
+
name: z.ZodString;
|
|
156
|
+
label: z.ZodString;
|
|
157
|
+
description: z.ZodOptional<z.ZodString>;
|
|
158
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
159
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
160
|
+
instance: "instance";
|
|
161
|
+
install: "install";
|
|
162
|
+
}>>;
|
|
163
|
+
}, z.core.$strip>>>;
|
|
90
164
|
/**
|
|
91
165
|
* How records move when the schema changes between versions (A3-F07).
|
|
92
166
|
* Checked against the previous version when a version is published, and
|
|
@@ -133,7 +207,7 @@ export interface DataProblem {
|
|
|
133
207
|
field?: string;
|
|
134
208
|
message: string;
|
|
135
209
|
}
|
|
136
|
-
export type DataProblemCode = FieldTypeInvalid['code'] | 'data_too_many_collections' | 'data_too_many_fields' | 'data_collection_name_format' | 'data_field_name_format' | 'data_field_reserved' | 'data_label_format' | 'data_label_taken' | 'data_project_field_twice' | 'data_search_unknown_field' | 'data_search_not_text' | 'grant_collection_missing' | 'grant_tool_missing' | 'custom_name_taken' | 'custom_collection_unknown' | 'custom_input_invalid' | 'placement_screen_unknown';
|
|
210
|
+
export type DataProblemCode = FieldTypeInvalid['code'] | 'data_too_many_collections' | 'data_too_many_fields' | 'data_collection_name_format' | 'data_field_name_format' | 'data_field_reserved' | 'data_label_format' | 'data_label_taken' | 'data_project_field_twice' | 'data_search_unknown_field' | 'data_search_not_text' | 'grant_collection_missing' | 'grant_tool_missing' | 'custom_name_taken' | 'custom_collection_unknown' | 'custom_input_invalid' | 'placement_key_taken' | 'placement_screen_unknown' | 'placement_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
|
|
137
211
|
type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
|
|
138
212
|
/**
|
|
139
213
|
* Everything wrong with an app's additions that a type cannot say.
|
|
@@ -148,10 +222,12 @@ export declare function dataProblems(additions: Additions, options?: {
|
|
|
148
222
|
/** The additions alone, for code that has the rest of the manifest already. */
|
|
149
223
|
export declare const manifestExtensionsSchema: z.ZodObject<{
|
|
150
224
|
placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
225
|
+
key: z.ZodOptional<z.ZodString>;
|
|
151
226
|
kind: z.ZodEnum<{
|
|
152
227
|
"project-tab": "project-tab";
|
|
153
228
|
"project-sidebar": "project-sidebar";
|
|
154
229
|
"workspace-sidebar": "workspace-sidebar";
|
|
230
|
+
home: "home";
|
|
155
231
|
}>;
|
|
156
232
|
screen: z.ZodString;
|
|
157
233
|
label: z.ZodOptional<z.ZodString>;
|
|
@@ -165,6 +241,27 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
|
|
|
165
241
|
required: z.ZodOptional<z.ZodBoolean>;
|
|
166
242
|
placeholder: z.ZodOptional<z.ZodString>;
|
|
167
243
|
}, z.core.$strip>>>;
|
|
244
|
+
children: z.ZodOptional<z.ZodObject<{
|
|
245
|
+
tool: z.ZodString;
|
|
246
|
+
refreshSeconds: z.ZodOptional<z.ZodNumber>;
|
|
247
|
+
cap: z.ZodOptional<z.ZodNumber>;
|
|
248
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
249
|
+
create: z.ZodOptional<z.ZodObject<{
|
|
250
|
+
tool: z.ZodString;
|
|
251
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
252
|
+
titleField: z.ZodOptional<z.ZodString>;
|
|
253
|
+
parentField: z.ZodOptional<z.ZodString>;
|
|
254
|
+
}, z.core.$strip>>;
|
|
255
|
+
nested: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
256
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
257
|
+
create: z.ZodOptional<z.ZodObject<{
|
|
258
|
+
tool: z.ZodString;
|
|
259
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
260
|
+
titleField: z.ZodOptional<z.ZodString>;
|
|
261
|
+
parentField: z.ZodOptional<z.ZodString>;
|
|
262
|
+
}, z.core.$strip>>;
|
|
263
|
+
}, z.core.$strip>>>;
|
|
264
|
+
}, z.core.$strip>>;
|
|
168
265
|
}, z.core.$strip>>>;
|
|
169
266
|
data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
170
267
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
@@ -190,6 +287,16 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
|
|
|
190
287
|
collections: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
191
288
|
host: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
192
289
|
}, z.core.$strip>>;
|
|
290
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
291
|
+
name: z.ZodString;
|
|
292
|
+
label: z.ZodString;
|
|
293
|
+
description: z.ZodOptional<z.ZodString>;
|
|
294
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
295
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
296
|
+
instance: "instance";
|
|
297
|
+
install: "install";
|
|
298
|
+
}>>;
|
|
299
|
+
}, z.core.$strip>>>;
|
|
193
300
|
migrations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
194
301
|
version: z.ZodString;
|
|
195
302
|
steps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -226,10 +333,12 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
|
|
|
226
333
|
*/
|
|
227
334
|
export declare const storedExtensionsSchema: z.ZodObject<{
|
|
228
335
|
placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
336
|
+
key: z.ZodOptional<z.ZodString>;
|
|
229
337
|
kind: z.ZodEnum<{
|
|
230
338
|
"project-tab": "project-tab";
|
|
231
339
|
"project-sidebar": "project-sidebar";
|
|
232
340
|
"workspace-sidebar": "workspace-sidebar";
|
|
341
|
+
home: "home";
|
|
233
342
|
}>;
|
|
234
343
|
screen: z.ZodString;
|
|
235
344
|
label: z.ZodOptional<z.ZodString>;
|
|
@@ -243,6 +352,27 @@ export declare const storedExtensionsSchema: z.ZodObject<{
|
|
|
243
352
|
required: z.ZodOptional<z.ZodBoolean>;
|
|
244
353
|
placeholder: z.ZodOptional<z.ZodString>;
|
|
245
354
|
}, z.core.$strip>>>;
|
|
355
|
+
children: z.ZodOptional<z.ZodObject<{
|
|
356
|
+
tool: z.ZodString;
|
|
357
|
+
refreshSeconds: z.ZodOptional<z.ZodNumber>;
|
|
358
|
+
cap: z.ZodOptional<z.ZodNumber>;
|
|
359
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
360
|
+
create: z.ZodOptional<z.ZodObject<{
|
|
361
|
+
tool: z.ZodString;
|
|
362
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
363
|
+
titleField: z.ZodOptional<z.ZodString>;
|
|
364
|
+
parentField: z.ZodOptional<z.ZodString>;
|
|
365
|
+
}, z.core.$strip>>;
|
|
366
|
+
nested: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
367
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
368
|
+
create: z.ZodOptional<z.ZodObject<{
|
|
369
|
+
tool: z.ZodString;
|
|
370
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
371
|
+
titleField: z.ZodOptional<z.ZodString>;
|
|
372
|
+
parentField: z.ZodOptional<z.ZodString>;
|
|
373
|
+
}, z.core.$strip>>;
|
|
374
|
+
}, z.core.$strip>>>;
|
|
375
|
+
}, z.core.$strip>>;
|
|
246
376
|
}, z.core.$strip>>>;
|
|
247
377
|
data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
248
378
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
@@ -268,6 +398,16 @@ export declare const storedExtensionsSchema: z.ZodObject<{
|
|
|
268
398
|
collections: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
269
399
|
host: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
270
400
|
}, z.core.$strip>>;
|
|
401
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
402
|
+
name: z.ZodString;
|
|
403
|
+
label: z.ZodString;
|
|
404
|
+
description: z.ZodOptional<z.ZodString>;
|
|
405
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
406
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
407
|
+
instance: "instance";
|
|
408
|
+
install: "install";
|
|
409
|
+
}>>;
|
|
410
|
+
}, z.core.$strip>>>;
|
|
271
411
|
migrations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
272
412
|
version: z.ZodString;
|
|
273
413
|
steps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -318,7 +458,14 @@ export declare const appManifestSchema: z.ZodObject<{
|
|
|
318
458
|
terms: z.ZodOptional<z.ZodString>;
|
|
319
459
|
support: z.ZodOptional<z.ZodString>;
|
|
320
460
|
}, z.core.$strip>>;
|
|
321
|
-
|
|
461
|
+
logo: z.ZodOptional<z.ZodObject<{
|
|
462
|
+
color: z.ZodString;
|
|
463
|
+
mono: z.ZodString;
|
|
464
|
+
}, z.core.$strip>>;
|
|
465
|
+
icon: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
466
|
+
color: z.ZodString;
|
|
467
|
+
mono: z.ZodString;
|
|
468
|
+
}, z.core.$strip>]>>;
|
|
322
469
|
brandColor: z.ZodOptional<z.ZodString>;
|
|
323
470
|
brandColorDark: z.ZodOptional<z.ZodString>;
|
|
324
471
|
defaultPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -332,10 +479,12 @@ export declare const appManifestSchema: z.ZodObject<{
|
|
|
332
479
|
}, z.core.$strip>>;
|
|
333
480
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
334
481
|
placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
482
|
+
key: z.ZodOptional<z.ZodString>;
|
|
335
483
|
kind: z.ZodEnum<{
|
|
336
484
|
"project-tab": "project-tab";
|
|
337
485
|
"project-sidebar": "project-sidebar";
|
|
338
486
|
"workspace-sidebar": "workspace-sidebar";
|
|
487
|
+
home: "home";
|
|
339
488
|
}>;
|
|
340
489
|
screen: z.ZodString;
|
|
341
490
|
label: z.ZodOptional<z.ZodString>;
|
|
@@ -349,6 +498,27 @@ export declare const appManifestSchema: z.ZodObject<{
|
|
|
349
498
|
required: z.ZodOptional<z.ZodBoolean>;
|
|
350
499
|
placeholder: z.ZodOptional<z.ZodString>;
|
|
351
500
|
}, z.core.$strip>>>;
|
|
501
|
+
children: z.ZodOptional<z.ZodObject<{
|
|
502
|
+
tool: z.ZodString;
|
|
503
|
+
refreshSeconds: z.ZodOptional<z.ZodNumber>;
|
|
504
|
+
cap: z.ZodOptional<z.ZodNumber>;
|
|
505
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
506
|
+
create: z.ZodOptional<z.ZodObject<{
|
|
507
|
+
tool: z.ZodString;
|
|
508
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
509
|
+
titleField: z.ZodOptional<z.ZodString>;
|
|
510
|
+
parentField: z.ZodOptional<z.ZodString>;
|
|
511
|
+
}, z.core.$strip>>;
|
|
512
|
+
nested: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
513
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
514
|
+
create: z.ZodOptional<z.ZodObject<{
|
|
515
|
+
tool: z.ZodString;
|
|
516
|
+
noun: z.ZodOptional<z.ZodString>;
|
|
517
|
+
titleField: z.ZodOptional<z.ZodString>;
|
|
518
|
+
parentField: z.ZodOptional<z.ZodString>;
|
|
519
|
+
}, z.core.$strip>>;
|
|
520
|
+
}, z.core.$strip>>>;
|
|
521
|
+
}, z.core.$strip>>;
|
|
352
522
|
}, z.core.$strip>>>;
|
|
353
523
|
data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
354
524
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
@@ -374,6 +544,16 @@ export declare const appManifestSchema: z.ZodObject<{
|
|
|
374
544
|
collections: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
375
545
|
host: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
376
546
|
}, z.core.$strip>>;
|
|
547
|
+
secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
548
|
+
name: z.ZodString;
|
|
549
|
+
label: z.ZodString;
|
|
550
|
+
description: z.ZodOptional<z.ZodString>;
|
|
551
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
552
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
553
|
+
instance: "instance";
|
|
554
|
+
install: "install";
|
|
555
|
+
}>>;
|
|
556
|
+
}, z.core.$strip>>>;
|
|
377
557
|
migrations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
378
558
|
version: z.ZodString;
|
|
379
559
|
steps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
package/src/schema.js
CHANGED
|
@@ -24,12 +24,63 @@ const placementSettingSchema = z.object({
|
|
|
24
24
|
required: z.boolean().optional(),
|
|
25
25
|
placeholder: z.string().max(200).optional(),
|
|
26
26
|
});
|
|
27
|
+
/**
|
|
28
|
+
* How deep a folder's rows may nest, counting its own rows as the first
|
|
29
|
+
* level (ADR-A21): Issues → sprints → issues is two. Each level is listed
|
|
30
|
+
* only when a person opens its parent, so depth costs nothing while closed;
|
|
31
|
+
* the limit is for the sidebar's width and a person's patience.
|
|
32
|
+
*/
|
|
33
|
+
export const MAX_FOLDER_DEPTH = 4;
|
|
34
|
+
/**
|
|
35
|
+
* "New sprint" on a folder's level (ADR-A21): one of the app's tools, run
|
|
36
|
+
* through the screen's door with the name the person typed in `titleField`
|
|
37
|
+
* and, below the first level, the row it is created under in `parentField`.
|
|
38
|
+
*/
|
|
39
|
+
const folderCreateSchema = z.object({
|
|
40
|
+
tool: z.string().min(1).max(64),
|
|
41
|
+
/** "sprint", in "New sprint". */
|
|
42
|
+
noun: z.string().min(1).max(40).optional(),
|
|
43
|
+
titleField: z.string().regex(FIELD_NAME, 'A field name is letters, digits and _, starting with a lower-case letter.').optional(),
|
|
44
|
+
parentField: z.string().regex(FIELD_NAME, 'A field name is letters, digits and _, starting with a lower-case letter.').optional(),
|
|
45
|
+
});
|
|
46
|
+
/** A level below a folder's own rows: what they are called, and how one is made. */
|
|
47
|
+
const folderLevelSchema = z.object({
|
|
48
|
+
noun: z.string().min(1).max(60).optional(),
|
|
49
|
+
create: folderCreateSchema.optional(),
|
|
50
|
+
});
|
|
51
|
+
/** A stable identity for one independently addable placement offering. */
|
|
52
|
+
export const PLACEMENT_KEY = /^[a-z][a-z0-9-]{0,39}$/;
|
|
53
|
+
export function effectivePlacementKey(one) {
|
|
54
|
+
return one.key ?? `${one.kind}:${one.screen}`;
|
|
55
|
+
}
|
|
27
56
|
const placementSchema = z.object({
|
|
28
|
-
|
|
57
|
+
key: z.string().regex(PLACEMENT_KEY).optional(),
|
|
58
|
+
kind: z.enum(['project-tab', 'project-sidebar', 'workspace-sidebar', 'home']),
|
|
29
59
|
screen: z.string().min(1).max(FIELD_LIMITS.nameChars),
|
|
30
60
|
label: z.string().min(1).max(60).optional(),
|
|
31
61
|
icon: z.string().max(60).optional(),
|
|
32
62
|
settings: z.record(z.string().regex(FIELD_NAME), placementSettingSchema).optional(),
|
|
63
|
+
/**
|
|
64
|
+
* A folder (A2-F02-S02): a sidebar item whose rows one of the app's read
|
|
65
|
+
* tools lists live, only while it is open. It can nest, and offer "New …"
|
|
66
|
+
* at each level, when the app opts in (ADR-A21).
|
|
67
|
+
*/
|
|
68
|
+
children: z
|
|
69
|
+
.object({
|
|
70
|
+
tool: z.string().min(1).max(64),
|
|
71
|
+
refreshSeconds: z.number().int().min(15).max(3600).optional(),
|
|
72
|
+
cap: z.number().int().min(1).max(50).optional(),
|
|
73
|
+
noun: z.string().min(1).max(60).optional(),
|
|
74
|
+
/** A "New …" row at the folder's first level (ADR-A21). */
|
|
75
|
+
create: folderCreateSchema.optional(),
|
|
76
|
+
/**
|
|
77
|
+
* The levels under the first, in order (ADR-A21). A row the tool marks
|
|
78
|
+
* `hasChildren` opens only when a level is declared for it; without
|
|
79
|
+
* `nested` the folder stays one flat list.
|
|
80
|
+
*/
|
|
81
|
+
nested: z.array(folderLevelSchema).max(8).optional(),
|
|
82
|
+
})
|
|
83
|
+
.optional(),
|
|
33
84
|
});
|
|
34
85
|
const collectionSchema = z.object({
|
|
35
86
|
/** Field name to type, in the manifest's spelling (`"member?"`, `["todo","done"]`). */
|
|
@@ -80,6 +131,28 @@ const toolsSchema = z.object({
|
|
|
80
131
|
generated: z.boolean().optional(),
|
|
81
132
|
custom: z.array(customToolSchema).max(MAX_CUSTOM_TOOLS).optional(),
|
|
82
133
|
});
|
|
134
|
+
/** Most secrets one app may declare (ADR-A24). */
|
|
135
|
+
export const MAX_APP_SECRETS = 20;
|
|
136
|
+
/** The longest value one secret may hold, in characters. */
|
|
137
|
+
export const MAX_SECRET_CHARS = 8 * 1024;
|
|
138
|
+
/** A secret's name, as a handler asks for it: `api_key`. */
|
|
139
|
+
export const SECRET_NAME = /^[a-z][a-z0-9_]{0,59}$/;
|
|
140
|
+
/**
|
|
141
|
+
* A secret the app needs (ADR-A24): named here, never valued here. The value
|
|
142
|
+
* is entered in the app's settings by an administrator, or stored by the
|
|
143
|
+
* app's own handler with `secrets.set`, and only that app's handlers can read
|
|
144
|
+
* it, with `secrets.get`. A screen never can.
|
|
145
|
+
*
|
|
146
|
+
* `install` (the default) is one value for the whole install; `instance` is
|
|
147
|
+
* one per instance.
|
|
148
|
+
*/
|
|
149
|
+
const secretSchema = z.object({
|
|
150
|
+
name: z.string().regex(SECRET_NAME, 'A secret name is lower case letters, digits and underscores, starting with a letter.'),
|
|
151
|
+
label: z.string().min(1).max(60),
|
|
152
|
+
description: z.string().max(300).optional(),
|
|
153
|
+
required: z.boolean().optional(),
|
|
154
|
+
scope: z.enum(['install', 'instance']).optional(),
|
|
155
|
+
});
|
|
83
156
|
const grantsSchema = z.object({
|
|
84
157
|
tools: z.array(z.string().max(100)).max(200).optional(),
|
|
85
158
|
collections: z.array(z.string().max(100)).max(FIELD_LIMITS.collections + 1).optional(),
|
|
@@ -91,6 +164,8 @@ const extensionShape = {
|
|
|
91
164
|
tools: toolsSchema.optional(),
|
|
92
165
|
screens: z.record(z.string(), screenSchema).optional(),
|
|
93
166
|
grants: grantsSchema.optional(),
|
|
167
|
+
/** The secrets its handlers read (ADR-A24): names only, never values. */
|
|
168
|
+
secrets: z.array(secretSchema).max(MAX_APP_SECRETS).optional(),
|
|
94
169
|
/**
|
|
95
170
|
* How records move when the schema changes between versions (A3-F07).
|
|
96
171
|
* Checked against the previous version when a version is published, and
|
|
@@ -241,8 +316,20 @@ export function dataProblems(additions, options = {}) {
|
|
|
241
316
|
}
|
|
242
317
|
}
|
|
243
318
|
problems.push(...customToolProblems(additions, options));
|
|
319
|
+
problems.push(...secretProblems(additions, options));
|
|
244
320
|
const screens = new Set(Object.keys(additions.screens ?? {}));
|
|
321
|
+
const placementKeys = new Set();
|
|
245
322
|
for (const placement of additions.placements ?? []) {
|
|
323
|
+
const key = effectivePlacementKey(placement);
|
|
324
|
+
if (placementKeys.has(key)) {
|
|
325
|
+
problems.push({
|
|
326
|
+
code: 'placement_key_taken',
|
|
327
|
+
message: `Two placements use the key "${key}"; each independently addable placement needs its own key.`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
placementKeys.add(key);
|
|
332
|
+
}
|
|
246
333
|
if (!screens.has(placement.screen)) {
|
|
247
334
|
problems.push({
|
|
248
335
|
code: 'placement_screen_unknown',
|
|
@@ -250,6 +337,61 @@ export function dataProblems(additions, options = {}) {
|
|
|
250
337
|
});
|
|
251
338
|
}
|
|
252
339
|
}
|
|
340
|
+
problems.push(...folderProblems(additions));
|
|
341
|
+
return problems;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* A folder that nests or creates (ADR-A21) must stay within
|
|
345
|
+
* `MAX_FOLDER_DEPTH`, and each "New …" must name a tool the app has that
|
|
346
|
+
* changes records. Only the new keys are checked: a published manifest's
|
|
347
|
+
* existing folder still reads as it did.
|
|
348
|
+
*/
|
|
349
|
+
function folderProblems(additions) {
|
|
350
|
+
const problems = [];
|
|
351
|
+
const custom = new Map((additions.tools?.custom ?? []).map(tool => [tool.name, tool]));
|
|
352
|
+
const generatedWrites = additions.tools?.generated === false
|
|
353
|
+
? new Set()
|
|
354
|
+
: new Set(Object.entries(additions.data ?? {}).flatMap(([name, declared]) => {
|
|
355
|
+
const label = labelOf(name, declared.label);
|
|
356
|
+
return [`create_${label}`, `update_${label}`, `delete_${label}`, `batch_${label}s`];
|
|
357
|
+
}));
|
|
358
|
+
const generatedReads = additions.tools?.generated === false
|
|
359
|
+
? new Set()
|
|
360
|
+
: new Set(Object.entries(additions.data ?? {}).flatMap(([name, declared]) => {
|
|
361
|
+
const label = labelOf(name, declared.label);
|
|
362
|
+
return [`get_${label}`, `list_${label}s`, `search_${label}s`];
|
|
363
|
+
}));
|
|
364
|
+
for (const placement of additions.placements ?? []) {
|
|
365
|
+
const children = placement.children;
|
|
366
|
+
if (!children)
|
|
367
|
+
continue;
|
|
368
|
+
const depth = 1 + (children.nested?.length ?? 0);
|
|
369
|
+
if (depth > MAX_FOLDER_DEPTH) {
|
|
370
|
+
problems.push({
|
|
371
|
+
code: 'placement_children_too_deep',
|
|
372
|
+
message: `The "${placement.screen}" folder nests ${depth} levels deep; a sidebar folder may nest at most ${MAX_FOLDER_DEPTH}.`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const creates = [children.create, ...(children.nested ?? []).map(level => level.create)];
|
|
376
|
+
for (const create of creates) {
|
|
377
|
+
if (!create)
|
|
378
|
+
continue;
|
|
379
|
+
const own = custom.get(create.tool);
|
|
380
|
+
const reads = own ? own.write !== true : generatedReads.has(create.tool);
|
|
381
|
+
if (!own && !reads && !generatedWrites.has(create.tool)) {
|
|
382
|
+
problems.push({
|
|
383
|
+
code: 'placement_create_tool_unknown',
|
|
384
|
+
message: `The "${placement.screen}" folder creates with ${create.tool}, which is not one of the app's tools.`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
else if (reads) {
|
|
388
|
+
problems.push({
|
|
389
|
+
code: 'placement_create_not_write',
|
|
390
|
+
message: `The "${placement.screen}" folder creates with ${create.tool}, which only reads: name a tool that makes records, or mark a custom one write: true.`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
253
395
|
return problems;
|
|
254
396
|
}
|
|
255
397
|
/**
|
|
@@ -316,6 +458,33 @@ function customToolProblems(additions, options) {
|
|
|
316
458
|
}
|
|
317
459
|
return problems;
|
|
318
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Each secret declared once, and asked for (ADR-A24): an app that declares
|
|
463
|
+
* secrets without the `secrets` host grant could never read one.
|
|
464
|
+
*/
|
|
465
|
+
function secretProblems(additions, options) {
|
|
466
|
+
const problems = [];
|
|
467
|
+
const secrets = additions.secrets ?? [];
|
|
468
|
+
const seen = new Set();
|
|
469
|
+
for (const secret of secrets) {
|
|
470
|
+
if (seen.has(secret.name)) {
|
|
471
|
+
problems.push({
|
|
472
|
+
code: 'secret_name_taken',
|
|
473
|
+
field: secret.name,
|
|
474
|
+
message: `The secret ${secret.name} is declared twice; keep one.`,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
seen.add(secret.name);
|
|
478
|
+
}
|
|
479
|
+
const host = additions.grants?.host ?? [];
|
|
480
|
+
if (options.grants !== false && secrets.length && !host.includes('secrets') && !host.includes('*')) {
|
|
481
|
+
problems.push({
|
|
482
|
+
code: 'grant_secrets_missing',
|
|
483
|
+
message: 'The app declares secrets but does not ask to read them: add "secrets" to grants.host.',
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return problems;
|
|
487
|
+
}
|
|
319
488
|
const refuse = (additions, ctx, options = {}) => {
|
|
320
489
|
for (const problem of dataProblems(additions, options)) {
|
|
321
490
|
ctx.addIssue({
|
|
@@ -323,13 +492,17 @@ const refuse = (additions, ctx, options = {}) => {
|
|
|
323
492
|
message: problem.message,
|
|
324
493
|
path: problem.code === 'grant_tool_missing'
|
|
325
494
|
? ['grants', 'tools']
|
|
326
|
-
: problem.code
|
|
327
|
-
? ['grants', '
|
|
328
|
-
: problem.code
|
|
329
|
-
? ['
|
|
330
|
-
: problem.
|
|
331
|
-
? ['
|
|
332
|
-
:
|
|
495
|
+
: problem.code === 'grant_secrets_missing'
|
|
496
|
+
? ['grants', 'host']
|
|
497
|
+
: problem.code === 'secret_name_taken'
|
|
498
|
+
? ['secrets']
|
|
499
|
+
: problem.code.startsWith('grant_')
|
|
500
|
+
? ['grants', 'collections']
|
|
501
|
+
: problem.code.startsWith('custom_')
|
|
502
|
+
? ['tools', 'custom']
|
|
503
|
+
: problem.collection
|
|
504
|
+
? ['data', problem.collection, ...(problem.field ? ['schema', problem.field] : [])]
|
|
505
|
+
: [],
|
|
333
506
|
params: { code: problem.code },
|
|
334
507
|
});
|
|
335
508
|
}
|
package/src/secrets.d.ts
CHANGED
|
@@ -30,3 +30,17 @@ export interface SecretFound {
|
|
|
30
30
|
export declare function secretsInJson(document: unknown, at: string): SecretFound[];
|
|
31
31
|
/** Every secret declared in a bundle's files, as the server finds them. */
|
|
32
32
|
export declare function findSecrets(files: ReadonlyMap<string, Uint8Array>, root?: string): SecretFound[];
|
|
33
|
+
/**
|
|
34
|
+
* The server's sentence for `developer_key_in_bundle` (ADR-A22), word for
|
|
35
|
+
* word: `DEVELOPER_KEY_IN_BUNDLE` in Brydio's `app-publish.service.ts`.
|
|
36
|
+
*/
|
|
37
|
+
export declare const DEVELOPER_KEY_MESSAGE = "That package contains a Brydio developer API key. A key belongs on your own server, never in an app: revoke it in Settings \u203A Developer, and call Brydio\u2019s model from a handler with the model grant instead.";
|
|
38
|
+
/** Whether a text holds a Brydio developer API key. */
|
|
39
|
+
export declare const holdsDeveloperKey: (text: string) => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The first file holding a developer API key, by path, as Brydio's publish
|
|
42
|
+
* route finds it (`developerKeyIn`). Unlike the declaration scan, every file
|
|
43
|
+
* is read, scripts included: the key's shape is exact enough that a match is
|
|
44
|
+
* a key, not a guess, and no part of an app may hold one (ADR-A24).
|
|
45
|
+
*/
|
|
46
|
+
export declare function developerKeyIn(files: ReadonlyMap<string, Uint8Array>): string | null;
|
package/src/secrets.js
CHANGED
|
@@ -35,21 +35,21 @@ const isRealValue = (value) => {
|
|
|
35
35
|
return false;
|
|
36
36
|
return true;
|
|
37
37
|
};
|
|
38
|
-
function walk(node, at, found) {
|
|
38
|
+
function walk(node, at, found, publicValue = () => false) {
|
|
39
39
|
if (Array.isArray(node)) {
|
|
40
|
-
node.forEach((one, index) => walk(one, `${at}[${index}]`, found));
|
|
40
|
+
node.forEach((one, index) => walk(one, `${at}[${index}]`, found, publicValue));
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
if (!node || typeof node !== 'object')
|
|
44
44
|
return;
|
|
45
45
|
for (const [key, value] of Object.entries(node)) {
|
|
46
46
|
const path = at ? `${at}.${key}` : key;
|
|
47
|
-
if (VALUE_KEYS.has(key) && isRealValue(value)) {
|
|
47
|
+
if (VALUE_KEYS.has(key) && isRealValue(value) && !publicValue(path)) {
|
|
48
48
|
// Never quoted back: a report that repeated the secret would copy it somewhere else.
|
|
49
49
|
found.push({ code: 'secret_in_bundle', message: SECRET_MESSAGE, path });
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
|
-
walk(value, path, found);
|
|
52
|
+
walk(value, path, found, publicValue);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
@@ -59,7 +59,7 @@ function walk(node, at, found) {
|
|
|
59
59
|
*/
|
|
60
60
|
export function secretsInJson(document, at) {
|
|
61
61
|
const found = [];
|
|
62
|
-
walk(document, at, found);
|
|
62
|
+
walk(document, at, found, path => /(?:^|\.)placements\[\d+\]\.key$/.test(path));
|
|
63
63
|
return found;
|
|
64
64
|
}
|
|
65
65
|
/** Every secret declared in a bundle's files, as the server finds them. */
|
|
@@ -79,3 +79,25 @@ export function findSecrets(files, root = '') {
|
|
|
79
79
|
}
|
|
80
80
|
return found;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The server's sentence for `developer_key_in_bundle` (ADR-A22), word for
|
|
84
|
+
* word: `DEVELOPER_KEY_IN_BUNDLE` in Brydio's `app-publish.service.ts`.
|
|
85
|
+
*/
|
|
86
|
+
export const DEVELOPER_KEY_MESSAGE = 'That package contains a Brydio developer API key. A key belongs on your own server, never in an app: revoke it in Settings › Developer, and call Brydio’s model from a handler with the model grant instead.';
|
|
87
|
+
/** A developer API key's exact shape, anywhere in a text. */
|
|
88
|
+
const DEVELOPER_KEY = /bry_live_[0-9a-f]{12}_[A-Za-z0-9_-]{43}/;
|
|
89
|
+
/** Whether a text holds a Brydio developer API key. */
|
|
90
|
+
export const holdsDeveloperKey = (text) => DEVELOPER_KEY.test(text);
|
|
91
|
+
/**
|
|
92
|
+
* The first file holding a developer API key, by path, as Brydio's publish
|
|
93
|
+
* route finds it (`developerKeyIn`). Unlike the declaration scan, every file
|
|
94
|
+
* is read, scripts included: the key's shape is exact enough that a match is
|
|
95
|
+
* a key, not a guess, and no part of an app may hold one (ADR-A24).
|
|
96
|
+
*/
|
|
97
|
+
export function developerKeyIn(files) {
|
|
98
|
+
for (const [path, bytes] of files) {
|
|
99
|
+
if (holdsDeveloperKey(new TextDecoder('latin1').decode(bytes)))
|
|
100
|
+
return path;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|