@brydio/manifest 0.1.0-alpha.6 → 0.1.0-alpha.61
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 +1 -1
- package/package.json +6 -2
- package/src/anonymous.d.ts +42 -0
- package/src/anonymous.js +86 -0
- package/src/base.d.ts +17 -1
- package/src/base.js +13 -1
- package/src/bundle.d.ts +22 -2
- package/src/bundle.js +93 -11
- package/src/confirm-email.d.ts +40 -0
- package/src/confirm-email.js +78 -0
- package/src/define.d.ts +1 -1
- package/src/define.js +1 -1
- package/src/field-types.d.ts +41 -5
- package/src/field-types.js +60 -10
- package/src/frame-checks.d.ts +13 -0
- package/src/frame-checks.js +35 -0
- package/src/grants.d.ts +14 -2
- package/src/grants.js +34 -2
- package/src/index.d.ts +13 -5
- package/src/index.js +15 -5
- package/src/look.d.ts +196 -0
- package/src/look.js +744 -0
- package/src/migrations.d.ts +1 -1
- package/src/migrations.js +29 -0
- package/src/open-schema.d.ts +128 -0
- package/src/open-schema.js +444 -0
- package/src/readers.d.ts +54 -0
- package/src/readers.js +96 -0
- package/src/requirements.d.ts +122 -0
- package/src/requirements.js +692 -0
- package/src/schema.d.ts +576 -9
- package/src/schema.js +484 -31
- package/src/screen-kinds.d.ts +18 -0
- package/src/screen-kinds.js +30 -0
- package/src/secrets.d.ts +14 -0
- package/src/secrets.js +38 -5
- package/src/validate.d.ts +12 -3
- package/src/validate.js +7 -5
package/src/readers.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { FIELD_LIMITS, parseFieldType } from "./field-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Record readers (P16): a record only the people it
|
|
5
|
+
* names may read.
|
|
6
|
+
*
|
|
7
|
+
* An app names one of the collection's own `string[]` fields, which holds
|
|
8
|
+
* user ids. When a record's list is empty, everyone who can read the
|
|
9
|
+
* instance reads it, as before; when it names anyone, nobody else finds it —
|
|
10
|
+
* not on a screen, not through a tool, not through the assistant, not on the
|
|
11
|
+
* co-edit socket. There is no admin past it: an app that wants its admins
|
|
12
|
+
* in puts them in the list. Brydio keeps the list in plain beside the record
|
|
13
|
+
* (`app_document.readers`), so every read can be narrowed before anything
|
|
14
|
+
* is decrypted. Mirrors Brydio's `apps/api/src/apps/manifest/readers.ts`;
|
|
15
|
+
* the two change together.
|
|
16
|
+
*/
|
|
17
|
+
export const readersSchema = z.string().min(1).max(FIELD_LIMITS.nameChars);
|
|
18
|
+
/** The most people one record's list may name. */
|
|
19
|
+
export const READERS_LIMIT = 1_000;
|
|
20
|
+
export function readersProblems(collection, declared) {
|
|
21
|
+
if (!declared.readers)
|
|
22
|
+
return [];
|
|
23
|
+
const problems = [];
|
|
24
|
+
const raw = declared.schema[declared.readers];
|
|
25
|
+
let listed = false;
|
|
26
|
+
try {
|
|
27
|
+
listed = raw !== undefined && parseFieldType(raw).kind === 'string[]';
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Refused as a field type already.
|
|
31
|
+
}
|
|
32
|
+
if (!listed) {
|
|
33
|
+
problems.push({
|
|
34
|
+
code: 'data_readers_field',
|
|
35
|
+
collection,
|
|
36
|
+
field: declared.readers,
|
|
37
|
+
message: `"${collection}" names "${declared.readers}" as its readers, which must be one of its own string[] fields holding user ids.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// An anonymous answer names nobody; a list of who may read it would.
|
|
41
|
+
if (declared.anonymous) {
|
|
42
|
+
problems.push({
|
|
43
|
+
code: 'data_readers_anonymous',
|
|
44
|
+
collection,
|
|
45
|
+
message: `"${collection}" can't be anonymous and name its readers.`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return problems;
|
|
49
|
+
}
|
|
50
|
+
/** The plain list a record is kept with: its readers, or null for everyone who can read the instance. */
|
|
51
|
+
export function readersOf(field, body) {
|
|
52
|
+
if (!field)
|
|
53
|
+
return null;
|
|
54
|
+
const value = body[field];
|
|
55
|
+
if (!Array.isArray(value))
|
|
56
|
+
return null;
|
|
57
|
+
const ids = [...new Set(value.filter((one) => typeof one === 'string' && one.length > 0))];
|
|
58
|
+
return ids.length ? ids.sort() : null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Record editors (DW06): a record only the people it names may change. The
|
|
62
|
+
* same shape as readers: one of the collection's own `string[]` fields of
|
|
63
|
+
* user ids, kept in plain beside the record (`app_document.editors`). Empty,
|
|
64
|
+
* and anyone who may write the instance may change it; naming anyone, and
|
|
65
|
+
* only they may — through a tool, a handler, the assistant or the co-edit
|
|
66
|
+
* socket, where anyone else's connection is read-only. Everyone who may read
|
|
67
|
+
* the record still reads it.
|
|
68
|
+
*/
|
|
69
|
+
export const editorsSchema = readersSchema;
|
|
70
|
+
export function editorsProblems(collection, declared) {
|
|
71
|
+
if (!declared.editors)
|
|
72
|
+
return [];
|
|
73
|
+
const problems = [];
|
|
74
|
+
const raw = declared.schema[declared.editors];
|
|
75
|
+
let listed = false;
|
|
76
|
+
try {
|
|
77
|
+
listed = raw !== undefined && parseFieldType(raw).kind === 'string[]';
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Refused as a field type already.
|
|
81
|
+
}
|
|
82
|
+
if (!listed) {
|
|
83
|
+
problems.push({
|
|
84
|
+
code: 'data_editors_field',
|
|
85
|
+
collection,
|
|
86
|
+
field: declared.editors,
|
|
87
|
+
message: `"${collection}" names "${declared.editors}" as its editors, which must be one of its own string[] fields holding user ids.`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (declared.anonymous) {
|
|
91
|
+
problems.push({ code: 'data_editors_anonymous', collection, message: `"${collection}" can't be anonymous and name its editors.` });
|
|
92
|
+
}
|
|
93
|
+
return problems;
|
|
94
|
+
}
|
|
95
|
+
/** The plain list a record is kept with: its editors, or null for anyone who may write the instance. */
|
|
96
|
+
export const editorsOf = readersOf;
|
|
@@ -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';
|