@brydio/manifest 0.1.0-alpha.0 → 0.1.0-alpha.11

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/src/schema.d.ts CHANGED
@@ -1,6 +1,45 @@
1
1
  import { z } from 'zod';
2
2
  import { FieldTypeInvalid, type FieldType } from './field-types.js';
3
3
  export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, type FieldKind, type FieldType, } from './field-types.js';
4
+ /**
5
+ * What an app adds to `.brydio/app.json` to be more than a bundle of servers
6
+ * and skills: where it is shown, what it keeps, the tools that come from what
7
+ * it keeps, its screens, and what it asks the workspace for (contracts §4).
8
+ *
9
+ * Every key is optional, so every manifest E5 already imports still parses.
10
+ * A copy of Brydio's `apps/api/src/apps/manifest/manifest-ext.schema.ts`, the
11
+ * schema the server reads an installed app's manifest with. The two change
12
+ * together; `test/manifest.test.ts` parses the same manifests with both.
13
+ *
14
+ * Zod says what the additions *are*. The checks that are claims about one
15
+ * field against another — a search list naming a date, a placement naming a
16
+ * screen nobody declared — are `dataProblems` below, with a code each, so an
17
+ * import report can say which collection and which field, not just "invalid".
18
+ */
19
+ declare const placementSettingSchema: z.ZodObject<{
20
+ type: z.ZodEnum<{
21
+ string: "string";
22
+ url: "url";
23
+ }>;
24
+ label: z.ZodString;
25
+ required: z.ZodOptional<z.ZodBoolean>;
26
+ placeholder: z.ZodOptional<z.ZodString>;
27
+ }, z.core.$strip>;
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;
4
43
  /** Most custom tools one app may declare (A3-F08). */
5
44
  export declare const MAX_CUSTOM_TOOLS = 20;
6
45
  /** A tool name the model and a policy row can both use as it is. */
@@ -18,16 +57,74 @@ declare const customToolSchema: z.ZodObject<{
18
57
  collection: z.ZodOptional<z.ZodString>;
19
58
  }, z.core.$strip>;
20
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>;
21
86
  declare const extensionShape: {
22
87
  placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
88
+ key: z.ZodOptional<z.ZodString>;
23
89
  kind: z.ZodEnum<{
24
90
  "project-tab": "project-tab";
25
91
  "project-sidebar": "project-sidebar";
26
92
  "workspace-sidebar": "workspace-sidebar";
93
+ home: "home";
27
94
  }>;
28
95
  screen: z.ZodString;
29
96
  label: z.ZodOptional<z.ZodString>;
30
97
  icon: z.ZodOptional<z.ZodString>;
98
+ settings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
99
+ type: z.ZodEnum<{
100
+ string: "string";
101
+ url: "url";
102
+ }>;
103
+ label: z.ZodString;
104
+ required: z.ZodOptional<z.ZodBoolean>;
105
+ placeholder: z.ZodOptional<z.ZodString>;
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>>;
31
128
  }, z.core.$strip>>>;
32
129
  data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
33
130
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -53,6 +150,17 @@ declare const extensionShape: {
53
150
  collections: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
151
  host: z.ZodOptional<z.ZodArray<z.ZodString>>;
55
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>>>;
56
164
  /**
57
165
  * How records move when the schema changes between versions (A3-F07).
58
166
  * Checked against the previous version when a version is published, and
@@ -99,7 +207,7 @@ export interface DataProblem {
99
207
  field?: string;
100
208
  message: string;
101
209
  }
102
- 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';
103
211
  type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
104
212
  /**
105
213
  * Everything wrong with an app's additions that a type cannot say.
@@ -114,14 +222,46 @@ export declare function dataProblems(additions: Additions, options?: {
114
222
  /** The additions alone, for code that has the rest of the manifest already. */
115
223
  export declare const manifestExtensionsSchema: z.ZodObject<{
116
224
  placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
225
+ key: z.ZodOptional<z.ZodString>;
117
226
  kind: z.ZodEnum<{
118
227
  "project-tab": "project-tab";
119
228
  "project-sidebar": "project-sidebar";
120
229
  "workspace-sidebar": "workspace-sidebar";
230
+ home: "home";
121
231
  }>;
122
232
  screen: z.ZodString;
123
233
  label: z.ZodOptional<z.ZodString>;
124
234
  icon: z.ZodOptional<z.ZodString>;
235
+ settings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
236
+ type: z.ZodEnum<{
237
+ string: "string";
238
+ url: "url";
239
+ }>;
240
+ label: z.ZodString;
241
+ required: z.ZodOptional<z.ZodBoolean>;
242
+ placeholder: z.ZodOptional<z.ZodString>;
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>>;
125
265
  }, z.core.$strip>>>;
126
266
  data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
127
267
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -147,6 +287,16 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
147
287
  collections: z.ZodOptional<z.ZodArray<z.ZodString>>;
148
288
  host: z.ZodOptional<z.ZodArray<z.ZodString>>;
149
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>>>;
150
300
  migrations: z.ZodOptional<z.ZodArray<z.ZodObject<{
151
301
  version: z.ZodString;
152
302
  steps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -183,14 +333,46 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
183
333
  */
184
334
  export declare const storedExtensionsSchema: z.ZodObject<{
185
335
  placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
336
+ key: z.ZodOptional<z.ZodString>;
186
337
  kind: z.ZodEnum<{
187
338
  "project-tab": "project-tab";
188
339
  "project-sidebar": "project-sidebar";
189
340
  "workspace-sidebar": "workspace-sidebar";
341
+ home: "home";
190
342
  }>;
191
343
  screen: z.ZodString;
192
344
  label: z.ZodOptional<z.ZodString>;
193
345
  icon: z.ZodOptional<z.ZodString>;
346
+ settings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
347
+ type: z.ZodEnum<{
348
+ string: "string";
349
+ url: "url";
350
+ }>;
351
+ label: z.ZodString;
352
+ required: z.ZodOptional<z.ZodBoolean>;
353
+ placeholder: z.ZodOptional<z.ZodString>;
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>>;
194
376
  }, z.core.$strip>>>;
195
377
  data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
196
378
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -216,6 +398,16 @@ export declare const storedExtensionsSchema: z.ZodObject<{
216
398
  collections: z.ZodOptional<z.ZodArray<z.ZodString>>;
217
399
  host: z.ZodOptional<z.ZodArray<z.ZodString>>;
218
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>>>;
219
411
  migrations: z.ZodOptional<z.ZodArray<z.ZodObject<{
220
412
  version: z.ZodString;
221
413
  steps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -266,7 +458,14 @@ export declare const appManifestSchema: z.ZodObject<{
266
458
  terms: z.ZodOptional<z.ZodString>;
267
459
  support: z.ZodOptional<z.ZodString>;
268
460
  }, z.core.$strip>>;
269
- icon: z.ZodOptional<z.ZodString>;
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>]>>;
270
469
  brandColor: z.ZodOptional<z.ZodString>;
271
470
  brandColorDark: z.ZodOptional<z.ZodString>;
272
471
  defaultPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -280,14 +479,46 @@ export declare const appManifestSchema: z.ZodObject<{
280
479
  }, z.core.$strip>>;
281
480
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
282
481
  placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
482
+ key: z.ZodOptional<z.ZodString>;
283
483
  kind: z.ZodEnum<{
284
484
  "project-tab": "project-tab";
285
485
  "project-sidebar": "project-sidebar";
286
486
  "workspace-sidebar": "workspace-sidebar";
487
+ home: "home";
287
488
  }>;
288
489
  screen: z.ZodString;
289
490
  label: z.ZodOptional<z.ZodString>;
290
491
  icon: z.ZodOptional<z.ZodString>;
492
+ settings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
493
+ type: z.ZodEnum<{
494
+ string: "string";
495
+ url: "url";
496
+ }>;
497
+ label: z.ZodString;
498
+ required: z.ZodOptional<z.ZodBoolean>;
499
+ placeholder: z.ZodOptional<z.ZodString>;
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>>;
291
522
  }, z.core.$strip>>>;
292
523
  data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
293
524
  schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -313,6 +544,16 @@ export declare const appManifestSchema: z.ZodObject<{
313
544
  collections: z.ZodOptional<z.ZodArray<z.ZodString>>;
314
545
  host: z.ZodOptional<z.ZodArray<z.ZodString>>;
315
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>>>;
316
557
  migrations: z.ZodOptional<z.ZodArray<z.ZodObject<{
317
558
  version: z.ZodString;
318
559
  steps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
package/src/schema.js CHANGED
@@ -18,11 +18,69 @@ export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structure
18
18
  * screen nobody declared — are `dataProblems` below, with a code each, so an
19
19
  * import report can say which collection and which field, not just "invalid".
20
20
  */
21
+ const placementSettingSchema = z.object({
22
+ type: z.enum(['string', 'url']),
23
+ label: z.string().min(1).max(60),
24
+ required: z.boolean().optional(),
25
+ placeholder: z.string().max(200).optional(),
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
+ }
21
56
  const placementSchema = z.object({
22
- kind: z.enum(['project-tab', 'project-sidebar', 'workspace-sidebar']),
57
+ key: z.string().regex(PLACEMENT_KEY).optional(),
58
+ kind: z.enum(['project-tab', 'project-sidebar', 'workspace-sidebar', 'home']),
23
59
  screen: z.string().min(1).max(FIELD_LIMITS.nameChars),
24
60
  label: z.string().min(1).max(60).optional(),
25
61
  icon: z.string().max(60).optional(),
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(),
26
84
  });
27
85
  const collectionSchema = z.object({
28
86
  /** Field name to type, in the manifest's spelling (`"member?"`, `["todo","done"]`). */
@@ -73,6 +131,28 @@ const toolsSchema = z.object({
73
131
  generated: z.boolean().optional(),
74
132
  custom: z.array(customToolSchema).max(MAX_CUSTOM_TOOLS).optional(),
75
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
+ });
76
156
  const grantsSchema = z.object({
77
157
  tools: z.array(z.string().max(100)).max(200).optional(),
78
158
  collections: z.array(z.string().max(100)).max(FIELD_LIMITS.collections + 1).optional(),
@@ -84,6 +164,8 @@ const extensionShape = {
84
164
  tools: toolsSchema.optional(),
85
165
  screens: z.record(z.string(), screenSchema).optional(),
86
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(),
87
169
  /**
88
170
  * How records move when the schema changes between versions (A3-F07).
89
171
  * Checked against the previous version when a version is published, and
@@ -234,8 +316,20 @@ export function dataProblems(additions, options = {}) {
234
316
  }
235
317
  }
236
318
  problems.push(...customToolProblems(additions, options));
319
+ problems.push(...secretProblems(additions, options));
237
320
  const screens = new Set(Object.keys(additions.screens ?? {}));
321
+ const placementKeys = new Set();
238
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
+ }
239
333
  if (!screens.has(placement.screen)) {
240
334
  problems.push({
241
335
  code: 'placement_screen_unknown',
@@ -243,6 +337,61 @@ export function dataProblems(additions, options = {}) {
243
337
  });
244
338
  }
245
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
+ }
246
395
  return problems;
247
396
  }
248
397
  /**
@@ -309,6 +458,33 @@ function customToolProblems(additions, options) {
309
458
  }
310
459
  return problems;
311
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
+ }
312
488
  const refuse = (additions, ctx, options = {}) => {
313
489
  for (const problem of dataProblems(additions, options)) {
314
490
  ctx.addIssue({
@@ -316,13 +492,17 @@ const refuse = (additions, ctx, options = {}) => {
316
492
  message: problem.message,
317
493
  path: problem.code === 'grant_tool_missing'
318
494
  ? ['grants', 'tools']
319
- : problem.code.startsWith('grant_')
320
- ? ['grants', 'collections']
321
- : problem.code.startsWith('custom_')
322
- ? ['tools', 'custom']
323
- : problem.collection
324
- ? ['data', problem.collection, ...(problem.field ? ['schema', problem.field] : [])]
325
- : [],
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
+ : [],
326
506
  params: { code: problem.code },
327
507
  });
328
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;