@brydio/manifest 0.1.0-alpha.3 → 0.1.0-alpha.31
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/README.md +24 -0
- 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/define.d.ts +1 -1
- package/src/define.js +1 -1
- package/src/field-types.d.ts +24 -4
- package/src/field-types.js +42 -4
- package/src/grants.d.ts +1 -1
- package/src/grants.js +1 -1
- package/src/index.d.ts +6 -5
- package/src/index.js +6 -5
- package/src/requirements.d.ts +122 -0
- package/src/requirements.js +692 -0
- package/src/schema.d.ts +434 -4
- package/src/schema.js +233 -15
- package/src/secrets.d.ts +14 -0
- package/src/secrets.js +27 -5
- package/src/validate.d.ts +8 -2
- package/src/validate.js +7 -5
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @brydio/manifest
|
|
2
|
+
|
|
3
|
+
The schema and validation rules for `.brydio/app.json`. It covers app identity,
|
|
4
|
+
placements, screens, collections, generated and custom tools, grants, and
|
|
5
|
+
migrations.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { defineManifest } from '@brydio/manifest';
|
|
9
|
+
|
|
10
|
+
export const manifest = defineManifest({
|
|
11
|
+
name: 'checklist',
|
|
12
|
+
version: '0.1.0',
|
|
13
|
+
screens: { home: { entry: 'screens/home.js' } },
|
|
14
|
+
placements: [{ kind: 'project-widget', screen: 'home', label: 'Checklist', sizes: ['medium', 'large'] }],
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Read the [manifest reference](https://github.com/nakel-ola/brydio-sdk/blob/main/docs/manifest.md)
|
|
19
|
+
for every field, schema type, grant, migration operation, and limit. The
|
|
20
|
+
[publish checklist](https://github.com/nakel-ola/brydio-sdk/blob/main/docs/publish-checklist.md)
|
|
21
|
+
lists every validation code and its refusal message.
|
|
22
|
+
|
|
23
|
+
This package is MIT licensed. While the SDK is `0.x`, a minor version may
|
|
24
|
+
contain a breaking change. Read the repository changelog before upgrading.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brydio/manifest",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.31",
|
|
4
4
|
"description": "The shape of a Brydio app's manifest, its collections' schemas, and the checks that say what is wrong with one.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"url": "https://github.com/nakel-ola/brydio-sdk",
|
|
11
11
|
"directory": "packages/manifest"
|
|
12
12
|
},
|
|
13
|
-
"gitHead": "
|
|
13
|
+
"gitHead": "5b6cf4e4fdacc5c60684fbf25b6b3cc3c78bb4f8",
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
16
|
"types": "./src/index.d.ts",
|
package/src/base.d.ts
CHANGED
|
@@ -32,6 +32,15 @@ export declare const linksSchema: z.ZodObject<{
|
|
|
32
32
|
terms: z.ZodOptional<z.ZodString>;
|
|
33
33
|
support: z.ZodOptional<z.ZodString>;
|
|
34
34
|
}, z.core.$strip>;
|
|
35
|
+
/**
|
|
36
|
+
* One of an app's two images, in its two variants (ADR-A20): `color` for a
|
|
37
|
+
* tile and the app's own page, `mono` for the sidebar, where the theme tints
|
|
38
|
+
* it. Both are `./`-prefixed paths inside the app.
|
|
39
|
+
*/
|
|
40
|
+
export declare const brandImagesSchema: z.ZodObject<{
|
|
41
|
+
color: z.ZodString;
|
|
42
|
+
mono: z.ZodString;
|
|
43
|
+
}, z.core.$strip>;
|
|
35
44
|
export declare const requiresSchema: z.ZodObject<{
|
|
36
45
|
servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
37
46
|
integrations: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -57,7 +66,14 @@ export declare const baseManifestSchema: z.ZodObject<{
|
|
|
57
66
|
terms: z.ZodOptional<z.ZodString>;
|
|
58
67
|
support: z.ZodOptional<z.ZodString>;
|
|
59
68
|
}, z.core.$strip>>;
|
|
60
|
-
|
|
69
|
+
logo: z.ZodOptional<z.ZodObject<{
|
|
70
|
+
color: z.ZodString;
|
|
71
|
+
mono: z.ZodString;
|
|
72
|
+
}, z.core.$strip>>;
|
|
73
|
+
icon: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
74
|
+
color: z.ZodString;
|
|
75
|
+
mono: z.ZodString;
|
|
76
|
+
}, z.core.$strip>]>>;
|
|
61
77
|
brandColor: z.ZodOptional<z.ZodString>;
|
|
62
78
|
brandColorDark: z.ZodOptional<z.ZodString>;
|
|
63
79
|
defaultPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
package/src/base.js
CHANGED
|
@@ -32,6 +32,15 @@ export const linksSchema = z.object({
|
|
|
32
32
|
terms: z.string().optional(),
|
|
33
33
|
support: z.string().optional(),
|
|
34
34
|
});
|
|
35
|
+
/**
|
|
36
|
+
* One of an app's two images, in its two variants (ADR-A20): `color` for a
|
|
37
|
+
* tile and the app's own page, `mono` for the sidebar, where the theme tints
|
|
38
|
+
* it. Both are `./`-prefixed paths inside the app.
|
|
39
|
+
*/
|
|
40
|
+
export const brandImagesSchema = z.object({
|
|
41
|
+
color: z.string(),
|
|
42
|
+
mono: z.string(),
|
|
43
|
+
});
|
|
35
44
|
export const requiresSchema = z.object({
|
|
36
45
|
servers: z.array(z.string()).default([]),
|
|
37
46
|
integrations: z.array(z.string()).default([]),
|
|
@@ -49,7 +58,10 @@ export const baseManifestSchema = z.object({
|
|
|
49
58
|
license: z.string().max(MANIFEST_LIMITS.licenseChars).optional(),
|
|
50
59
|
keywords: z.array(z.string()).max(MANIFEST_LIMITS.keywords).optional(),
|
|
51
60
|
links: linksSchema.optional(),
|
|
52
|
-
|
|
61
|
+
/** Required to publish (ADR-A20); optional here so every stored manifest still reads. */
|
|
62
|
+
logo: brandImagesSchema.optional(),
|
|
63
|
+
/** The two-variant icon (ADR-A20), or the one path every app had before it, still read. */
|
|
64
|
+
icon: z.union([z.string(), brandImagesSchema]).optional(),
|
|
53
65
|
brandColor: z.string().optional(),
|
|
54
66
|
brandColorDark: z.string().optional(),
|
|
55
67
|
defaultPrompts: z.array(z.string()).max(MANIFEST_LIMITS.defaultPrompts).optional(),
|
package/src/bundle.d.ts
CHANGED
|
@@ -23,7 +23,14 @@ export type BundleFiles = ReadonlyMap<string, Uint8Array>;
|
|
|
23
23
|
export declare function isBundlePath(path: string): boolean;
|
|
24
24
|
/** Whether a path is a script a bundle can hold. */
|
|
25
25
|
export declare const isScriptPath: (path: string) => boolean;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The logo and icon files the bundle's manifest names (ADR-A20), by path: the
|
|
28
|
+
* one kind of file beside code a bundle may hold, and only at those paths.
|
|
29
|
+
* Brydio keeps them with the version, never as code, so they are not in the
|
|
30
|
+
* fingerprint.
|
|
31
|
+
*/
|
|
32
|
+
export declare function brandOf(files: BundleFiles): BundleFiles;
|
|
33
|
+
/** The files the store keeps and the fingerprint covers: every one but the root manifest and its brand images. */
|
|
27
34
|
export declare function codeOf(files: BundleFiles): BundleFiles;
|
|
28
35
|
/**
|
|
29
36
|
* The fingerprint: sha256, lowercase hex, over the sorted list of the code
|
package/src/bundle.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { brandPathsOf } from "./requirements.js";
|
|
2
3
|
/**
|
|
3
4
|
* What a bundle may hold, and the fingerprint its code is served under
|
|
4
5
|
* (contracts §11, A7-F02).
|
|
@@ -29,9 +30,32 @@ export function isBundlePath(path) {
|
|
|
29
30
|
}
|
|
30
31
|
/** Whether a path is a script a bundle can hold. */
|
|
31
32
|
export const isScriptPath = (path) => isBundlePath(path) && SCRIPT.test(path);
|
|
32
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The logo and icon files the bundle's manifest names (ADR-A20), by path: the
|
|
35
|
+
* one kind of file beside code a bundle may hold, and only at those paths.
|
|
36
|
+
* Brydio keeps them with the version, never as code, so they are not in the
|
|
37
|
+
* fingerprint.
|
|
38
|
+
*/
|
|
39
|
+
export function brandOf(files) {
|
|
40
|
+
const named = new Set(brandPathsOf(manifestIn(files)).values());
|
|
41
|
+
return new Map([...files].filter(([path]) => named.has(path)));
|
|
42
|
+
}
|
|
43
|
+
/** The bundle's manifest, read leniently: anything unreadable names no files. */
|
|
44
|
+
function manifestIn(files) {
|
|
45
|
+
const bytes = files.get(BUNDLE_MANIFEST);
|
|
46
|
+
if (!bytes)
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** The files the store keeps and the fingerprint covers: every one but the root manifest and its brand images. */
|
|
33
56
|
export function codeOf(files) {
|
|
34
|
-
|
|
57
|
+
const brand = brandOf(files);
|
|
58
|
+
return new Map([...files].filter(([path]) => path !== BUNDLE_MANIFEST && !brand.has(path)));
|
|
35
59
|
}
|
|
36
60
|
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
37
61
|
/**
|
|
@@ -54,11 +78,12 @@ export function bundleHash(files) {
|
|
|
54
78
|
* and words: every path first, then the manifest, then the code, then the size.
|
|
55
79
|
*/
|
|
56
80
|
export function bundleProblem(files) {
|
|
81
|
+
const brand = brandOf(files);
|
|
57
82
|
for (const path of files.keys()) {
|
|
58
83
|
if (!isBundlePath(path)) {
|
|
59
84
|
return { code: 'bundle_path_invalid', message: `"${JSON.stringify(path).slice(1, -1)}" is not a path a bundle can hold.`, file: path };
|
|
60
85
|
}
|
|
61
|
-
if (path !== BUNDLE_MANIFEST && !SCRIPT.test(path)) {
|
|
86
|
+
if (path !== BUNDLE_MANIFEST && !brand.has(path) && !SCRIPT.test(path)) {
|
|
62
87
|
return {
|
|
63
88
|
code: 'bundle_file_not_code',
|
|
64
89
|
message: `"${path}" is not a script. A bundle holds only .js files and ${BUNDLE_MANIFEST}.`,
|
package/src/define.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* name: 'issues',
|
|
12
12
|
* version: '0.1.0',
|
|
13
13
|
* screens: { board: { entry: 'screens/board.js' } },
|
|
14
|
-
* placements: [{ kind: 'project-
|
|
14
|
+
* placements: [{ kind: 'project-widget', screen: 'bord', sizes: ['large'] }], // error: "bord" is not a screen
|
|
15
15
|
* });
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
package/src/define.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* name: 'issues',
|
|
12
12
|
* version: '0.1.0',
|
|
13
13
|
* screens: { board: { entry: 'screens/board.js' } },
|
|
14
|
-
* placements: [{ kind: 'project-
|
|
14
|
+
* placements: [{ kind: 'project-widget', screen: 'bord', sizes: ['large'] }], // error: "bord" is not a screen
|
|
15
15
|
* });
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
package/src/field-types.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { type ZodType } from 'zod';
|
|
|
12
12
|
* and everything downstream — the store, the generated tools, the screen's
|
|
13
13
|
* form — reads that shape, never the manifest's spelling.
|
|
14
14
|
*/
|
|
15
|
-
export type FieldKind = 'string' | 'text' | 'enum' | 'member' | 'project' | 'date' | 'number' | 'boolean' | 'string[]' | 'token';
|
|
15
|
+
export type FieldKind = 'string' | 'text' | 'enum' | 'member' | 'project' | 'date' | 'number' | 'boolean' | 'string[]' | 'token' | 'canvas';
|
|
16
16
|
export interface FieldType {
|
|
17
17
|
kind: FieldKind;
|
|
18
18
|
/** Written with a trailing `?`: may be left out on create. */
|
|
@@ -27,6 +27,13 @@ export interface FieldType {
|
|
|
27
27
|
* changes, and a value with no label reads as itself.
|
|
28
28
|
*/
|
|
29
29
|
labels?: Readonly<Record<string, string>>;
|
|
30
|
+
/**
|
|
31
|
+
* Written by several people at once (`tasks/docs-wiki` DW01): a `text`
|
|
32
|
+
* field drawn with `bry-rich-text bind` is one fragment of a shared Yjs
|
|
33
|
+
* document the host keeps. Such a field leaves the record's `version`
|
|
34
|
+
* check, and a tool's change to it merges into the live text.
|
|
35
|
+
*/
|
|
36
|
+
coedit?: boolean;
|
|
30
37
|
}
|
|
31
38
|
/**
|
|
32
39
|
* The bounds a schema and a record are held to (A3-F01-S03).
|
|
@@ -74,8 +81,8 @@ export declare const FIELD_NAME: RegExp;
|
|
|
74
81
|
export declare const COLLECTION_NAME: RegExp;
|
|
75
82
|
/** A field type the manifest wrote that Brydio does not have, or a bad list. */
|
|
76
83
|
export declare class FieldTypeInvalid extends Error {
|
|
77
|
-
readonly code: 'data_field_type_unknown' | 'data_enum_empty' | 'data_enum_too_many' | 'data_enum_value_invalid' | 'data_enum_duplicate' | 'data_default_not_allowed' | 'data_default_on_required' | 'data_default_invalid' | 'data_labels_not_allowed' | 'data_label_invalid' | 'data_label_unknown_value' | 'data_field_key_unknown';
|
|
78
|
-
constructor(code: 'data_field_type_unknown' | 'data_enum_empty' | 'data_enum_too_many' | 'data_enum_value_invalid' | 'data_enum_duplicate' | 'data_default_not_allowed' | 'data_default_on_required' | 'data_default_invalid' | 'data_labels_not_allowed' | 'data_label_invalid' | 'data_label_unknown_value' | 'data_field_key_unknown', message: string);
|
|
84
|
+
readonly code: 'data_field_type_unknown' | 'data_enum_empty' | 'data_enum_too_many' | 'data_enum_value_invalid' | 'data_enum_duplicate' | 'data_default_not_allowed' | 'data_default_on_required' | 'data_default_invalid' | 'data_labels_not_allowed' | 'data_label_invalid' | 'data_label_unknown_value' | 'data_field_key_unknown' | 'data_coedit_not_text';
|
|
85
|
+
constructor(code: 'data_field_type_unknown' | 'data_enum_empty' | 'data_enum_too_many' | 'data_enum_value_invalid' | 'data_enum_duplicate' | 'data_default_not_allowed' | 'data_default_on_required' | 'data_default_invalid' | 'data_labels_not_allowed' | 'data_label_invalid' | 'data_label_unknown_value' | 'data_field_key_unknown' | 'data_coedit_not_text', message: string);
|
|
79
86
|
}
|
|
80
87
|
/**
|
|
81
88
|
* One field's manifest spelling, as a type.
|
|
@@ -87,6 +94,16 @@ export declare class FieldTypeInvalid extends Error {
|
|
|
87
94
|
export declare function parseFieldType(raw: unknown): FieldType;
|
|
88
95
|
/** How long a choice value's label may be. */
|
|
89
96
|
export declare const LABEL_CHARS = 60;
|
|
97
|
+
/** True for a field several people write at once, through the co-editing layer. */
|
|
98
|
+
export declare const isCoedited: (type: FieldType) => boolean;
|
|
99
|
+
/**
|
|
100
|
+
* What a drawing's field holds in the record (WB01): a summary the host writes
|
|
101
|
+
* from the live board, so a list, search and the assistant can say what is on
|
|
102
|
+
* it. The drawing itself lives only in the co-edited document.
|
|
103
|
+
*/
|
|
104
|
+
export declare const CANVAS_SUMMARY_CHARS = 10000;
|
|
105
|
+
/** A collection's co-edited fields, by name. */
|
|
106
|
+
export declare const coeditedFields: (fields: Readonly<Record<string, FieldType>>) => string[];
|
|
90
107
|
/** How a choice's value reads to a person: its label, or the value itself. */
|
|
91
108
|
export declare const labelOfValue: (type: FieldType, value: string) => string;
|
|
92
109
|
/** True for a type whose value lands in the plain `fields` column. */
|
|
@@ -127,7 +144,10 @@ type Optional<T> = T extends `${string}?` ? true : T extends {
|
|
|
127
144
|
/** The TypeScript type of one field's value, from its manifest spelling. */
|
|
128
145
|
export type FieldValue<T> = T extends {
|
|
129
146
|
type: infer U;
|
|
130
|
-
} ? FieldValue<U> : T extends readonly (infer V)[] ? V : T extends 'number' | 'number?' ? number : T extends 'boolean' | 'boolean?' ? boolean : T extends 'string[]' | 'string[]?' ? string[] : T extends 'token' | 'token?' ? (typeof COLOUR_TOKENS)[number] : T extends
|
|
147
|
+
} ? FieldValue<U> : T extends readonly (infer V)[] ? V : T extends 'number' | 'number?' ? number : T extends 'boolean' | 'boolean?' ? boolean : T extends 'string[]' | 'string[]?' ? string[] : T extends 'token' | 'token?' ? (typeof COLOUR_TOKENS)[number] : T extends 'canvas' | 'canvas?' ? {
|
|
148
|
+
elements: number;
|
|
149
|
+
text: string;
|
|
150
|
+
} : T extends string ? string : never;
|
|
131
151
|
type Simplify<T> = {
|
|
132
152
|
[K in keyof T]: T[K];
|
|
133
153
|
} & {};
|
package/src/field-types.js
CHANGED
|
@@ -68,7 +68,7 @@ const STRUCTURED = new Set([
|
|
|
68
68
|
]);
|
|
69
69
|
/** Only the words in these may one day be searched (A3-F01-S02). */
|
|
70
70
|
const SEARCHABLE = new Set(['string', 'text', 'string[]']);
|
|
71
|
-
const SCALARS = new Set(['string', 'text', 'member', 'project', 'date', 'number', 'boolean', 'token']);
|
|
71
|
+
const SCALARS = new Set(['string', 'text', 'member', 'project', 'date', 'number', 'boolean', 'token', 'canvas']);
|
|
72
72
|
/** A field type the manifest wrote that Brydio does not have, or a bad list. */
|
|
73
73
|
export class FieldTypeInvalid extends Error {
|
|
74
74
|
code;
|
|
@@ -99,18 +99,24 @@ export function parseFieldType(raw) {
|
|
|
99
99
|
return { kind: 'string[]', optional };
|
|
100
100
|
if (name === 'token')
|
|
101
101
|
return { kind: 'token', optional, values: COLOUR_TOKENS };
|
|
102
|
+
// A drawing is always co-edited and never required: it starts empty (WB01).
|
|
103
|
+
if (name === 'canvas')
|
|
104
|
+
return { kind: 'canvas', optional: true, coedit: true };
|
|
102
105
|
if (SCALARS.has(name))
|
|
103
106
|
return { kind: name, optional };
|
|
104
107
|
throw new FieldTypeInvalid('data_field_type_unknown', `"${raw}" is not a field type.`);
|
|
105
108
|
}
|
|
106
|
-
const FIELD_KEYS = new Set(['type', 'optional', 'default', 'labels']);
|
|
109
|
+
const FIELD_KEYS = new Set(['type', 'optional', 'default', 'labels', 'coedit']);
|
|
107
110
|
/** How long a choice value's label may be. */
|
|
108
111
|
export const LABEL_CHARS = 60;
|
|
109
112
|
/** The object form: the short form under `type`, and what it may add. */
|
|
110
113
|
function parseFieldObject(raw) {
|
|
111
114
|
const unknown = Object.keys(raw).find(key => !FIELD_KEYS.has(key));
|
|
112
115
|
if (unknown) {
|
|
113
|
-
throw new FieldTypeInvalid('data_field_key_unknown', `"${unknown}" is not something a field may say; use type, optional, default and
|
|
116
|
+
throw new FieldTypeInvalid('data_field_key_unknown', `"${unknown}" is not something a field may say; use type, optional, default, labels and coedit.`);
|
|
117
|
+
}
|
|
118
|
+
if (raw.coedit !== undefined && typeof raw.coedit !== 'boolean') {
|
|
119
|
+
throw new FieldTypeInvalid('data_field_key_unknown', '"coedit" is true or false.');
|
|
114
120
|
}
|
|
115
121
|
if (typeof raw.type !== 'string' && !Array.isArray(raw.type)) {
|
|
116
122
|
throw new FieldTypeInvalid('data_field_type_unknown', 'A field written as an object names its type under "type".');
|
|
@@ -119,7 +125,12 @@ function parseFieldObject(raw) {
|
|
|
119
125
|
throw new FieldTypeInvalid('data_field_key_unknown', '"optional" is true or false.');
|
|
120
126
|
}
|
|
121
127
|
const inner = parseFieldType(raw.type);
|
|
122
|
-
const type = {
|
|
128
|
+
const type = {
|
|
129
|
+
...inner,
|
|
130
|
+
optional: inner.optional || raw.optional === true,
|
|
131
|
+
...labelsOf(inner, raw),
|
|
132
|
+
...coeditOf(inner, raw),
|
|
133
|
+
};
|
|
123
134
|
if (!('default' in raw))
|
|
124
135
|
return type;
|
|
125
136
|
const value = raw.default;
|
|
@@ -157,6 +168,27 @@ function labelsOf(type, raw) {
|
|
|
157
168
|
}
|
|
158
169
|
return { labels: { ...labels } };
|
|
159
170
|
}
|
|
171
|
+
/** `coedit`, on long text only: nothing else is typed into by several people at once. */
|
|
172
|
+
function coeditOf(type, raw) {
|
|
173
|
+
if (raw.coedit !== true)
|
|
174
|
+
return {};
|
|
175
|
+
if (type.kind !== 'text' && type.kind !== 'canvas') {
|
|
176
|
+
throw new FieldTypeInvalid('data_coedit_not_text', `Only a text or canvas field may be co-edited, not a ${type.kind}.`);
|
|
177
|
+
}
|
|
178
|
+
return { coedit: true };
|
|
179
|
+
}
|
|
180
|
+
/** True for a field several people write at once, through the co-editing layer. */
|
|
181
|
+
export const isCoedited = (type) => type.kind === 'canvas' || (type.kind === 'text' && type.coedit === true);
|
|
182
|
+
/**
|
|
183
|
+
* What a drawing's field holds in the record (WB01): a summary the host writes
|
|
184
|
+
* from the live board, so a list, search and the assistant can say what is on
|
|
185
|
+
* it. The drawing itself lives only in the co-edited document.
|
|
186
|
+
*/
|
|
187
|
+
export const CANVAS_SUMMARY_CHARS = 10_000;
|
|
188
|
+
/** A collection's co-edited fields, by name. */
|
|
189
|
+
export const coeditedFields = (fields) => Object.entries(fields)
|
|
190
|
+
.filter(([, type]) => isCoedited(type))
|
|
191
|
+
.map(([field]) => field);
|
|
160
192
|
/** How a choice's value reads to a person: its label, or the value itself. */
|
|
161
193
|
export const labelOfValue = (type, value) => type.labels?.[value] ?? value;
|
|
162
194
|
function parseEnumeration(values) {
|
|
@@ -228,6 +260,8 @@ export function valueSchema(type) {
|
|
|
228
260
|
return z.boolean();
|
|
229
261
|
case 'string[]':
|
|
230
262
|
return z.array(z.string().max(FIELD_LIMITS.stringChars)).max(FIELD_LIMITS.listEntries);
|
|
263
|
+
case 'canvas':
|
|
264
|
+
return z.object({ elements: z.number().int().min(0), text: z.string().max(CANVAS_SUMMARY_CHARS) }).strict();
|
|
231
265
|
}
|
|
232
266
|
}
|
|
233
267
|
/**
|
|
@@ -263,6 +297,8 @@ export function valueProblem(field, type, value) {
|
|
|
263
297
|
return Array.isArray(value) && value.length > FIELD_LIMITS.listEntries
|
|
264
298
|
? `${field} may hold at most ${FIELD_LIMITS.listEntries} entries.`
|
|
265
299
|
: `${field} must be a list of short texts, each at most ${FIELD_LIMITS.stringChars.toLocaleString('en-GB')} characters.`;
|
|
300
|
+
case 'canvas':
|
|
301
|
+
return `${field} is a drawing: it is changed on its board, never written.`;
|
|
266
302
|
}
|
|
267
303
|
}
|
|
268
304
|
/** A type in a few words, for a tool's description: every allowed value named. */
|
|
@@ -288,6 +324,8 @@ export function describeType(type) {
|
|
|
288
324
|
return 'true or false';
|
|
289
325
|
case 'string[]':
|
|
290
326
|
return 'a list of short texts';
|
|
327
|
+
case 'canvas':
|
|
328
|
+
return 'a drawing, read only: how many shapes it has and the words on it';
|
|
291
329
|
}
|
|
292
330
|
}
|
|
293
331
|
/** ", read as \"todo\" is To do, \"done\" is Done", for a choice with labels. */
|
package/src/grants.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `apps/api/src/apps/publishing/app-publish.service.ts`.
|
|
8
8
|
*/
|
|
9
9
|
/** The host grants with a name of their own. A connection's is `connection:<name>`. */
|
|
10
|
-
export declare const HOST_CAPABILITIES: readonly ["navigate", "message", "members", "projects"];
|
|
10
|
+
export declare const HOST_CAPABILITIES: readonly ["navigate", "message", "members", "projects", "files", "chats", "model", "secrets", "notify"];
|
|
11
11
|
/** The older name for `HOST_CAPABILITIES`, kept for what already imports it. */
|
|
12
12
|
export declare const KNOWN_HOST_GRANTS: readonly string[];
|
|
13
13
|
/** True for a host grant Brydio knows how to honour: a named capability, or one connection. */
|
package/src/grants.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { BUNDLE_MANIFEST } from "./bundle.js";
|
|
10
10
|
/** The host grants with a name of their own. A connection's is `connection:<name>`. */
|
|
11
|
-
export const HOST_CAPABILITIES = ['navigate', 'message', 'members', 'projects'];
|
|
11
|
+
export const HOST_CAPABILITIES = ['navigate', 'message', 'members', 'projects', 'files', 'chats', 'model', 'secrets', 'notify'];
|
|
12
12
|
/** The older name for `HOST_CAPABILITIES`, kept for what already imports it. */
|
|
13
13
|
export const KNOWN_HOST_GRANTS = HOST_CAPABILITIES;
|
|
14
14
|
const CONNECTION = /^connection:[a-z0-9][a-z0-9_-]{0,59}$/;
|
package/src/index.d.ts
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* types, the tools Brydio generates from them, and the bundle rules. Each is
|
|
4
4
|
* a copy of the server's own, so an app that passes here passes there.
|
|
5
5
|
*/
|
|
6
|
-
export { APP_NAME_FORMAT, MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema } from './base.js';
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
6
|
+
export { APP_NAME_FORMAT, MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema, brandImagesSchema } from './base.js';
|
|
7
|
+
export { BRAND_FIELDS, BRAND_LIMITS, brandFilePath, brandPathsOf, brandProblems, brandShapeOf, isBrandPath, pngVerdict, svgColours, toolsProblem, type BrandField, type RequirementCode, type RequirementFiles, type RequirementProblem, } from './requirements.js';
|
|
8
|
+
export { BUNDLE_MANIFEST, BUNDLE_MAX_BYTES, BUNDLE_PATH_MAX_CHARS, bundleBytes, bundleHash, brandOf, bundleProblem, codeOf, isBundlePath, isScriptPath, sizeOf, type BundleFiles, type BundleProblem, type BundleProblemCode, } from './bundle.js';
|
|
9
|
+
export { COLOUR_TOKENS, coeditedFields, FIELD_LIMITS, RESERVED_FIELDS, describeType, isCoedited, isSearchable, isSortable, isStructured, parseFieldType, structuredFields, valueProblem, valueSchema, FieldTypeInvalid, type DocumentOf, type FieldKind, type FieldType, type FieldValue, type FieldsOf, } from './field-types.js';
|
|
10
|
+
export { appManifestSchema, collectionOf, collectionsOf, dataProblems, effectivePlacementKey, HOME_CARD_SIZES, labelOf, manifestExtensionsSchema, newAppManifestSchema, SIZED_PLACEMENT_KINDS, MAX_APP_SECRETS, MAX_SECRET_CHARS, PLACEMENT_KEY, SECRET_NAME, storedExtensionsSchema, type AppManifestWithData, type CollectionSpec, type DataProblem, type DataProblemCode, type DataProblemOptions, type HomeCardSize, type ManifestExtensions, type SecretSpec, } from './schema.js';
|
|
10
11
|
export { DOCUMENT_LIMITS } from './document-limits.js';
|
|
11
12
|
export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from './grants.js';
|
|
12
13
|
export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, type Migration, type MigrationCode, type MigrationProblem, type MigrationStep, type SchemaChange, } from './migrations.js';
|
|
13
|
-
export { SECRET_MESSAGE, findSecrets, secretsInJson, type SecretFound } from './secrets.js';
|
|
14
|
+
export { DEVELOPER_KEY_MESSAGE, SECRET_MESSAGE, developerKeyIn, findSecrets, holdsDeveloperKey, secretsInJson, type SecretFound, } from './secrets.js';
|
|
14
15
|
export { compareVersions, sdkRefusal, type SdkSupport } from './sdk.js';
|
|
15
16
|
export { TOOL_WRITES, generatedToolsOf, toolNames, type GeneratedTool, type ToolVerb } from './tools.js';
|
|
16
17
|
export { validateManifest, validateManifestText, type ManifestProblem, type ManifestProblemCode, type ManifestValidation, } from './validate.js';
|
package/src/index.js
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* types, the tools Brydio generates from them, and the bundle rules. Each is
|
|
4
4
|
* a copy of the server's own, so an app that passes here passes there.
|
|
5
5
|
*/
|
|
6
|
-
export { APP_NAME_FORMAT, MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema } from "./base.js";
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
6
|
+
export { APP_NAME_FORMAT, MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema, brandImagesSchema } from "./base.js";
|
|
7
|
+
export { BRAND_FIELDS, BRAND_LIMITS, brandFilePath, brandPathsOf, brandProblems, brandShapeOf, isBrandPath, pngVerdict, svgColours, toolsProblem, } from "./requirements.js";
|
|
8
|
+
export { BUNDLE_MANIFEST, BUNDLE_MAX_BYTES, BUNDLE_PATH_MAX_CHARS, bundleBytes, bundleHash, brandOf, bundleProblem, codeOf, isBundlePath, isScriptPath, sizeOf, } from "./bundle.js";
|
|
9
|
+
export { COLOUR_TOKENS, coeditedFields, FIELD_LIMITS, RESERVED_FIELDS, describeType, isCoedited, isSearchable, isSortable, isStructured, parseFieldType, structuredFields, valueProblem, valueSchema, FieldTypeInvalid, } from "./field-types.js";
|
|
10
|
+
export { appManifestSchema, collectionOf, collectionsOf, dataProblems, effectivePlacementKey, HOME_CARD_SIZES, labelOf, manifestExtensionsSchema, newAppManifestSchema, SIZED_PLACEMENT_KINDS, MAX_APP_SECRETS, MAX_SECRET_CHARS, PLACEMENT_KEY, SECRET_NAME, storedExtensionsSchema, } from "./schema.js";
|
|
10
11
|
export { DOCUMENT_LIMITS } from "./document-limits.js";
|
|
11
12
|
export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from "./grants.js";
|
|
12
13
|
export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, } from "./migrations.js";
|
|
13
|
-
export { SECRET_MESSAGE, findSecrets, secretsInJson } from "./secrets.js";
|
|
14
|
+
export { DEVELOPER_KEY_MESSAGE, SECRET_MESSAGE, developerKeyIn, findSecrets, holdsDeveloperKey, secretsInJson, } from "./secrets.js";
|
|
14
15
|
export { compareVersions, sdkRefusal } from "./sdk.js";
|
|
15
16
|
export { TOOL_WRITES, generatedToolsOf, toolNames } from "./tools.js";
|
|
16
17
|
export { validateManifest, validateManifestText, } from "./validate.js";
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two things every app must have before it is published (owner decisions of
|
|
3
|
+
* 22 September 2026), checked the same way by `brydio validate` and by
|
|
4
|
+
* `POST /apps/publish`.
|
|
5
|
+
*
|
|
6
|
+
* - **A logo and an icon, each in colour and in one colour** (ADR-A20). The
|
|
7
|
+
* manifest names four image files, and a package missing any of them is
|
|
8
|
+
* refused.
|
|
9
|
+
* - **Something for the assistant to call** (ADR-A19): generated tools from a
|
|
10
|
+
* collection, a custom tool, an MCP server or an integration.
|
|
11
|
+
*
|
|
12
|
+
* A copy of Brydio's `apps/api/src/apps/manifest/requirements.ts`, the
|
|
13
|
+
* checks `POST /apps/publish` runs. Nothing here imports anything, so the
|
|
14
|
+
* copy is the file: not even `node:zlib`, which a PNG is inflated with, since
|
|
15
|
+
* a screen may bundle `@brydio/manifest` and a worker has none.
|
|
16
|
+
* `test/requirements.test.ts` asks Brydio the same questions whenever a
|
|
17
|
+
* checkout is beside this one.
|
|
18
|
+
*
|
|
19
|
+
* ## What "one colour" means
|
|
20
|
+
*
|
|
21
|
+
* The mono variant is drawn tinted by the theme, so it must be a shape and
|
|
22
|
+
* nothing else: one colour on transparency.
|
|
23
|
+
*
|
|
24
|
+
* - An **SVG** is read for every colour it paints with: `fill`, `stroke`,
|
|
25
|
+
* `stop-color`, `flood-color` and `color`, as attributes and inside `style`.
|
|
26
|
+
* `none`, `transparent`, `inherit` and `currentColor` are not colours.
|
|
27
|
+
* A shape with no fill of its own paints in the default black, unless the
|
|
28
|
+
* root `<svg>` or a `<g>` sets one. More than one distinct colour is
|
|
29
|
+
* refused, and so is an embedded `<image>`, which could be anything.
|
|
30
|
+
* - A **PNG** is decoded, and every pixel at least 1/16 opaque must be within
|
|
31
|
+
* 32 of one colour on each channel, so anti-aliased edges of a black glyph
|
|
32
|
+
* (black at partial alpha) pass and a two-colour picture does not. A PNG
|
|
33
|
+
* with no transparent pixel at all is a solid block and is refused too.
|
|
34
|
+
* - A **JPEG** has no transparency, and a **WebP** is not decoded here, so
|
|
35
|
+
* neither can be the one-colour variant. Both are fine in colour.
|
|
36
|
+
*
|
|
37
|
+
* ## Sizes
|
|
38
|
+
*
|
|
39
|
+
* - An **icon** is square: from 48 to 1,024 pixels across as a raster, and a
|
|
40
|
+
* square view box as an SVG.
|
|
41
|
+
* - A **logo** may be wider than it is tall, never taller: from 48 to 1,024
|
|
42
|
+
* pixels tall, at most 2,048 wide, and at most four times as wide as it is
|
|
43
|
+
* tall. An SVG's view box is held to the same proportions.
|
|
44
|
+
* - Every file is at most 512 KB.
|
|
45
|
+
*/
|
|
46
|
+
export declare const BRAND_LIMITS: {
|
|
47
|
+
readonly bytes: number;
|
|
48
|
+
readonly iconMinSide: 48;
|
|
49
|
+
readonly iconMaxSide: 1024;
|
|
50
|
+
readonly logoMinHeight: 48;
|
|
51
|
+
readonly logoMaxHeight: 1024;
|
|
52
|
+
readonly logoMaxWidth: 2048;
|
|
53
|
+
/** Width over height, at most. */
|
|
54
|
+
readonly logoMaxRatio: 4;
|
|
55
|
+
};
|
|
56
|
+
/** The two images, and the two variants each one comes in. */
|
|
57
|
+
export declare const BRAND_FIELDS: readonly ["logo.color", "logo.mono", "icon.color", "icon.mono"];
|
|
58
|
+
export type BrandField = (typeof BRAND_FIELDS)[number];
|
|
59
|
+
export type RequirementCode = 'brand_logo_missing' | 'brand_icon_missing' | 'brand_path_invalid' | 'brand_file_missing' | 'brand_file_too_large' | 'brand_file_unreadable' | 'brand_icon_shape' | 'brand_logo_shape' | 'brand_mono_type' | 'brand_mono_colours' | 'app_has_no_tools';
|
|
60
|
+
export interface RequirementProblem {
|
|
61
|
+
code: RequirementCode;
|
|
62
|
+
message: string;
|
|
63
|
+
/** The manifest field, like `icon.mono`. */
|
|
64
|
+
path: string;
|
|
65
|
+
}
|
|
66
|
+
/** The files a check can see, by their path inside the package with no `./`. */
|
|
67
|
+
export type RequirementFiles = ReadonlyMap<string, Uint8Array>;
|
|
68
|
+
export type BrandImageType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/svg+xml';
|
|
69
|
+
/** A path the manifest wrote, as a bundle names the file: no `./`, forward slashes. */
|
|
70
|
+
export declare const brandFilePath: (declared: string) => string;
|
|
71
|
+
/**
|
|
72
|
+
* Whether a declared brand path is one Brydio will follow: `./`-prefixed,
|
|
73
|
+
* relative, inside the package, and a name a bundle can hold.
|
|
74
|
+
*/
|
|
75
|
+
export declare function isBrandPath(declared: unknown): declared is string;
|
|
76
|
+
/**
|
|
77
|
+
* The brand files a manifest names, by field, as bundle paths. Only fields
|
|
78
|
+
* written in the two-variant shape; a legacy `icon: "./icon.png"` names none.
|
|
79
|
+
*/
|
|
80
|
+
export declare function brandPathsOf(manifest: unknown): Map<BrandField, string>;
|
|
81
|
+
/**
|
|
82
|
+
* Everything wrong with an app's logo and icon.
|
|
83
|
+
*
|
|
84
|
+
* `required` is off only where a package is read that is not being published
|
|
85
|
+
* (an import of somebody's plugin with no screens): the images it does name
|
|
86
|
+
* are still checked, and the ones it leaves out are not asked for.
|
|
87
|
+
*/
|
|
88
|
+
export declare function brandProblems(manifest: Record<string, unknown>, files: RequirementFiles, options?: {
|
|
89
|
+
required?: boolean;
|
|
90
|
+
}): RequirementProblem[];
|
|
91
|
+
/**
|
|
92
|
+
* Whether the assistant has anything to call in this app (ADR-A19).
|
|
93
|
+
*
|
|
94
|
+
* `parts` counts what the package bundles beside the manifest: the servers in
|
|
95
|
+
* its `servers.json` and the files in its `integrations/`. Where they can't
|
|
96
|
+
* be seen (a published bundle holds only code), `requires` still counts.
|
|
97
|
+
*/
|
|
98
|
+
export declare function toolsProblem(manifest: Record<string, unknown>, parts?: {
|
|
99
|
+
servers?: number;
|
|
100
|
+
integrations?: number;
|
|
101
|
+
}): RequirementProblem | null;
|
|
102
|
+
export interface BrandShape {
|
|
103
|
+
type: BrandImageType;
|
|
104
|
+
width: number;
|
|
105
|
+
height: number;
|
|
106
|
+
}
|
|
107
|
+
/** What an image is and how big, from its bytes, or null for anything else. */
|
|
108
|
+
export declare function brandShapeOf(bytes: Uint8Array): BrandShape | null;
|
|
109
|
+
/** No script, no event handler, no `javascript:` and no reference that leaves the file. */
|
|
110
|
+
export declare function svgIsInert(svg: string): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Every distinct colour an SVG paints with, normalised, or null when it
|
|
113
|
+
* embeds a raster whose colours can't be read from the markup.
|
|
114
|
+
*/
|
|
115
|
+
export declare function svgColours(svg: string): string[] | null;
|
|
116
|
+
/**
|
|
117
|
+
* What a PNG's pixels say about being one colour.
|
|
118
|
+
*
|
|
119
|
+
* Decodes the image (non-interlaced, every colour type and bit depth) and
|
|
120
|
+
* compares every pixel at least 1/16 opaque with the most opaque one.
|
|
121
|
+
*/
|
|
122
|
+
export declare function pngVerdict(bytes: Uint8Array): 'one' | 'colours' | 'opaque' | 'unreadable';
|