@brydio/manifest 0.1.0-alpha.0
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/LICENSE +21 -0
- package/package.json +27 -0
- package/src/base.d.ts +73 -0
- package/src/base.js +61 -0
- package/src/bundle.d.ts +52 -0
- package/src/bundle.js +95 -0
- package/src/define.d.ts +49 -0
- package/src/define.js +21 -0
- package/src/document-limits.d.ts +21 -0
- package/src/document-limits.js +21 -0
- package/src/field-types.d.ts +157 -0
- package/src/field-types.js +298 -0
- package/src/grants.d.ts +20 -0
- package/src/grants.js +30 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +17 -0
- package/src/migrations.d.ts +142 -0
- package/src/migrations.js +322 -0
- package/src/schema.d.ts +381 -0
- package/src/schema.js +375 -0
- package/src/sdk.d.ts +30 -0
- package/src/sdk.js +77 -0
- package/src/secrets.d.ts +32 -0
- package/src/secrets.js +81 -0
- package/src/tools.d.ts +21 -0
- package/src/tools.js +31 -0
- package/src/validate.d.ts +27 -0
- package/src/validate.js +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brydio Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brydio/manifest",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "The shape of a Brydio app's manifest, its collections' schemas, and the checks that say what is wrong with one.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Brydio Inc.",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/nakel-ola/brydio-sdk",
|
|
11
|
+
"directory": "packages/manifest"
|
|
12
|
+
},
|
|
13
|
+
"gitHead": "da4f607b1e4bc8e5221b7556d03c67de30503f5d",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./src/index.d.ts",
|
|
17
|
+
"default": "./src/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"zod": "4.6.2"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/base.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The manifest every Brydio app has had since before apps drew screens.
|
|
4
|
+
*
|
|
5
|
+
* A copy of Brydio's `apps/api/src/extensions/apps/manifest.schema.ts` (E5's
|
|
6
|
+
* canonical `.brydio/app.json`) and the three numbers and formats it takes
|
|
7
|
+
* from `manifest-codes.ts`. Copied rather than imported because the SDK
|
|
8
|
+
* imports nothing from Brydio's tree (A5-F01-S01); `test/manifest.test.ts`
|
|
9
|
+
* holds the two side by side whenever a Brydio checkout is beside this one.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MANIFEST_LIMITS: {
|
|
12
|
+
readonly nameChars: 64;
|
|
13
|
+
readonly versionChars: 64;
|
|
14
|
+
readonly displayNameChars: 80;
|
|
15
|
+
readonly summaryChars: 240;
|
|
16
|
+
readonly descriptionChars: 4000;
|
|
17
|
+
readonly licenseChars: 64;
|
|
18
|
+
readonly keywords: 20;
|
|
19
|
+
readonly defaultPrompts: 3;
|
|
20
|
+
};
|
|
21
|
+
/** Kebab-case: "acme-projects", not "Acme Projects". */
|
|
22
|
+
export declare const APP_NAME_FORMAT: RegExp;
|
|
23
|
+
/** Semver, the permissive reading: `1.0.0-beta.2` is a version. */
|
|
24
|
+
export declare const SEMVER_FORMAT: RegExp;
|
|
25
|
+
export declare const authorSchema: z.ZodObject<{
|
|
26
|
+
name: z.ZodString;
|
|
27
|
+
email: z.ZodOptional<z.ZodString>;
|
|
28
|
+
url: z.ZodOptional<z.ZodString>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
export declare const linksSchema: z.ZodObject<{
|
|
31
|
+
privacy: z.ZodOptional<z.ZodString>;
|
|
32
|
+
terms: z.ZodOptional<z.ZodString>;
|
|
33
|
+
support: z.ZodOptional<z.ZodString>;
|
|
34
|
+
}, z.core.$strip>;
|
|
35
|
+
export declare const requiresSchema: z.ZodObject<{
|
|
36
|
+
servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
37
|
+
integrations: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
38
|
+
builtin: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
39
|
+
}, z.core.$strip>;
|
|
40
|
+
export declare const baseManifestSchema: z.ZodObject<{
|
|
41
|
+
name: z.ZodString;
|
|
42
|
+
version: z.ZodString;
|
|
43
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
44
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
45
|
+
description: z.ZodOptional<z.ZodString>;
|
|
46
|
+
author: z.ZodOptional<z.ZodObject<{
|
|
47
|
+
name: z.ZodString;
|
|
48
|
+
email: z.ZodOptional<z.ZodString>;
|
|
49
|
+
url: z.ZodOptional<z.ZodString>;
|
|
50
|
+
}, z.core.$strip>>;
|
|
51
|
+
homepage: z.ZodOptional<z.ZodString>;
|
|
52
|
+
repository: z.ZodOptional<z.ZodString>;
|
|
53
|
+
license: z.ZodOptional<z.ZodString>;
|
|
54
|
+
keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
55
|
+
links: z.ZodOptional<z.ZodObject<{
|
|
56
|
+
privacy: z.ZodOptional<z.ZodString>;
|
|
57
|
+
terms: z.ZodOptional<z.ZodString>;
|
|
58
|
+
support: z.ZodOptional<z.ZodString>;
|
|
59
|
+
}, z.core.$strip>>;
|
|
60
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
61
|
+
brandColor: z.ZodOptional<z.ZodString>;
|
|
62
|
+
brandColorDark: z.ZodOptional<z.ZodString>;
|
|
63
|
+
defaultPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
64
|
+
skills: z.ZodOptional<z.ZodString>;
|
|
65
|
+
servers: z.ZodOptional<z.ZodString>;
|
|
66
|
+
integrations: z.ZodOptional<z.ZodString>;
|
|
67
|
+
requires: z.ZodOptional<z.ZodObject<{
|
|
68
|
+
servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
69
|
+
integrations: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
70
|
+
builtin: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
71
|
+
}, z.core.$strip>>;
|
|
72
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
73
|
+
}, z.core.$strip>;
|
package/src/base.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The manifest every Brydio app has had since before apps drew screens.
|
|
4
|
+
*
|
|
5
|
+
* A copy of Brydio's `apps/api/src/extensions/apps/manifest.schema.ts` (E5's
|
|
6
|
+
* canonical `.brydio/app.json`) and the three numbers and formats it takes
|
|
7
|
+
* from `manifest-codes.ts`. Copied rather than imported because the SDK
|
|
8
|
+
* imports nothing from Brydio's tree (A5-F01-S01); `test/manifest.test.ts`
|
|
9
|
+
* holds the two side by side whenever a Brydio checkout is beside this one.
|
|
10
|
+
*/
|
|
11
|
+
export const MANIFEST_LIMITS = {
|
|
12
|
+
nameChars: 64,
|
|
13
|
+
versionChars: 64,
|
|
14
|
+
displayNameChars: 80,
|
|
15
|
+
summaryChars: 240,
|
|
16
|
+
descriptionChars: 4_000,
|
|
17
|
+
licenseChars: 64,
|
|
18
|
+
keywords: 20,
|
|
19
|
+
defaultPrompts: 3,
|
|
20
|
+
};
|
|
21
|
+
/** Kebab-case: "acme-projects", not "Acme Projects". */
|
|
22
|
+
export const APP_NAME_FORMAT = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
23
|
+
/** Semver, the permissive reading: `1.0.0-beta.2` is a version. */
|
|
24
|
+
export const SEMVER_FORMAT = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
25
|
+
export const authorSchema = z.object({
|
|
26
|
+
name: z.string().min(1),
|
|
27
|
+
email: z.string().optional(),
|
|
28
|
+
url: z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
export const linksSchema = z.object({
|
|
31
|
+
privacy: z.string().optional(),
|
|
32
|
+
terms: z.string().optional(),
|
|
33
|
+
support: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
export const requiresSchema = z.object({
|
|
36
|
+
servers: z.array(z.string()).default([]),
|
|
37
|
+
integrations: z.array(z.string()).default([]),
|
|
38
|
+
builtin: z.array(z.string()).default([]),
|
|
39
|
+
});
|
|
40
|
+
export const baseManifestSchema = z.object({
|
|
41
|
+
name: z.string().min(1).max(MANIFEST_LIMITS.nameChars).regex(APP_NAME_FORMAT),
|
|
42
|
+
version: z.string().max(MANIFEST_LIMITS.versionChars).regex(SEMVER_FORMAT),
|
|
43
|
+
displayName: z.string().max(MANIFEST_LIMITS.displayNameChars).optional(),
|
|
44
|
+
summary: z.string().max(MANIFEST_LIMITS.summaryChars).optional(),
|
|
45
|
+
description: z.string().max(MANIFEST_LIMITS.descriptionChars).optional(),
|
|
46
|
+
author: authorSchema.optional(),
|
|
47
|
+
homepage: z.string().optional(),
|
|
48
|
+
repository: z.string().optional(),
|
|
49
|
+
license: z.string().max(MANIFEST_LIMITS.licenseChars).optional(),
|
|
50
|
+
keywords: z.array(z.string()).max(MANIFEST_LIMITS.keywords).optional(),
|
|
51
|
+
links: linksSchema.optional(),
|
|
52
|
+
icon: z.string().optional(),
|
|
53
|
+
brandColor: z.string().optional(),
|
|
54
|
+
brandColorDark: z.string().optional(),
|
|
55
|
+
defaultPrompts: z.array(z.string()).max(MANIFEST_LIMITS.defaultPrompts).optional(),
|
|
56
|
+
skills: z.string().optional(),
|
|
57
|
+
servers: z.string().optional(),
|
|
58
|
+
integrations: z.string().optional(),
|
|
59
|
+
requires: requiresSchema.optional(),
|
|
60
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
61
|
+
});
|
package/src/bundle.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a bundle may hold, and the fingerprint its code is served under
|
|
3
|
+
* (contracts §11, A7-F02).
|
|
4
|
+
*
|
|
5
|
+
* The rules and the recipe are Brydio's, from
|
|
6
|
+
* `apps/api/src/apps/bundles/bundle-files.ts`, which refuses a bundle at the
|
|
7
|
+
* store. The SDK applies them first so a refusal is something the builder sees
|
|
8
|
+
* on their own machine, with the same numbers and the same words, and so
|
|
9
|
+
* `brydio build` can print the fingerprint the server will serve the code
|
|
10
|
+
* under before anything is uploaded.
|
|
11
|
+
*/
|
|
12
|
+
/** 1 MB per version, every file counted, the manifest included. */
|
|
13
|
+
export declare const BUNDLE_MAX_BYTES: number;
|
|
14
|
+
/** The manifest, at the root of every published bundle. Never stored, never hashed. */
|
|
15
|
+
export declare const BUNDLE_MANIFEST = "app.json";
|
|
16
|
+
export declare const BUNDLE_PATH_MAX_CHARS = 200;
|
|
17
|
+
/** The files of one bundle, by path inside it. */
|
|
18
|
+
export type BundleFiles = ReadonlyMap<string, Uint8Array>;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a path can name a file inside a bundle: relative, forward slashes,
|
|
21
|
+
* no empty, `.` or `..` segment, no hidden segment, plain printable ASCII.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isBundlePath(path: string): boolean;
|
|
24
|
+
/** Whether a path is a script a bundle can hold. */
|
|
25
|
+
export declare const isScriptPath: (path: string) => boolean;
|
|
26
|
+
/** The files the store keeps and the fingerprint covers: every one but the root manifest. */
|
|
27
|
+
export declare function codeOf(files: BundleFiles): BundleFiles;
|
|
28
|
+
/**
|
|
29
|
+
* The fingerprint: sha256, lowercase hex, over the sorted list of the code
|
|
30
|
+
* files, each written as its path, one NUL byte, the sha256 hex of its bytes
|
|
31
|
+
* and one newline.
|
|
32
|
+
*
|
|
33
|
+
* Over the files rather than an archive, because two zips of the same files
|
|
34
|
+
* differ by their timestamps. Without `app.json`, because the manifest carries
|
|
35
|
+
* the version number, and the same code under two numbers has to be one
|
|
36
|
+
* bundle. Sorted by JavaScript's default string order, as the server sorts.
|
|
37
|
+
*/
|
|
38
|
+
export declare function bundleHash(files: BundleFiles): string;
|
|
39
|
+
export type BundleProblemCode = 'bundle_too_large' | 'bundle_file_not_code' | 'bundle_path_invalid' | 'bundle_manifest_missing' | 'bundle_empty';
|
|
40
|
+
export interface BundleProblem {
|
|
41
|
+
code: BundleProblemCode;
|
|
42
|
+
message: string;
|
|
43
|
+
file?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* What the store would refuse about these files, or null. The server's order
|
|
47
|
+
* and words: every path first, then the manifest, then the code, then the size.
|
|
48
|
+
*/
|
|
49
|
+
export declare function bundleProblem(files: BundleFiles): BundleProblem | null;
|
|
50
|
+
/** Every file's bytes, the manifest included: what the cap is measured on. */
|
|
51
|
+
export declare const bundleBytes: (files: BundleFiles) => number;
|
|
52
|
+
export declare function sizeOf(bytes: number): string;
|
package/src/bundle.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* What a bundle may hold, and the fingerprint its code is served under
|
|
4
|
+
* (contracts §11, A7-F02).
|
|
5
|
+
*
|
|
6
|
+
* The rules and the recipe are Brydio's, from
|
|
7
|
+
* `apps/api/src/apps/bundles/bundle-files.ts`, which refuses a bundle at the
|
|
8
|
+
* store. The SDK applies them first so a refusal is something the builder sees
|
|
9
|
+
* on their own machine, with the same numbers and the same words, and so
|
|
10
|
+
* `brydio build` can print the fingerprint the server will serve the code
|
|
11
|
+
* under before anything is uploaded.
|
|
12
|
+
*/
|
|
13
|
+
/** 1 MB per version, every file counted, the manifest included. */
|
|
14
|
+
export const BUNDLE_MAX_BYTES = 1024 * 1024;
|
|
15
|
+
/** The manifest, at the root of every published bundle. Never stored, never hashed. */
|
|
16
|
+
export const BUNDLE_MANIFEST = 'app.json';
|
|
17
|
+
export const BUNDLE_PATH_MAX_CHARS = 200;
|
|
18
|
+
const SCRIPT = /\.m?js$/;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a path can name a file inside a bundle: relative, forward slashes,
|
|
21
|
+
* no empty, `.` or `..` segment, no hidden segment, plain printable ASCII.
|
|
22
|
+
*/
|
|
23
|
+
export function isBundlePath(path) {
|
|
24
|
+
if (path.length === 0 || path.length > BUNDLE_PATH_MAX_CHARS)
|
|
25
|
+
return false;
|
|
26
|
+
if (!/^[A-Za-z0-9._\-/]+$/.test(path))
|
|
27
|
+
return false;
|
|
28
|
+
return path.split('/').every(segment => segment.length > 0 && !segment.startsWith('.'));
|
|
29
|
+
}
|
|
30
|
+
/** Whether a path is a script a bundle can hold. */
|
|
31
|
+
export const isScriptPath = (path) => isBundlePath(path) && SCRIPT.test(path);
|
|
32
|
+
/** The files the store keeps and the fingerprint covers: every one but the root manifest. */
|
|
33
|
+
export function codeOf(files) {
|
|
34
|
+
return new Map([...files].filter(([path]) => path !== BUNDLE_MANIFEST));
|
|
35
|
+
}
|
|
36
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
37
|
+
/**
|
|
38
|
+
* The fingerprint: sha256, lowercase hex, over the sorted list of the code
|
|
39
|
+
* files, each written as its path, one NUL byte, the sha256 hex of its bytes
|
|
40
|
+
* and one newline.
|
|
41
|
+
*
|
|
42
|
+
* Over the files rather than an archive, because two zips of the same files
|
|
43
|
+
* differ by their timestamps. Without `app.json`, because the manifest carries
|
|
44
|
+
* the version number, and the same code under two numbers has to be one
|
|
45
|
+
* bundle. Sorted by JavaScript's default string order, as the server sorts.
|
|
46
|
+
*/
|
|
47
|
+
export function bundleHash(files) {
|
|
48
|
+
const code = codeOf(files);
|
|
49
|
+
const lines = [...code.keys()].sort().map(path => `${path}\0${sha256(code.get(path))}\n`);
|
|
50
|
+
return sha256(lines.join(''));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What the store would refuse about these files, or null. The server's order
|
|
54
|
+
* and words: every path first, then the manifest, then the code, then the size.
|
|
55
|
+
*/
|
|
56
|
+
export function bundleProblem(files) {
|
|
57
|
+
for (const path of files.keys()) {
|
|
58
|
+
if (!isBundlePath(path)) {
|
|
59
|
+
return { code: 'bundle_path_invalid', message: `"${JSON.stringify(path).slice(1, -1)}" is not a path a bundle can hold.`, file: path };
|
|
60
|
+
}
|
|
61
|
+
if (path !== BUNDLE_MANIFEST && !SCRIPT.test(path)) {
|
|
62
|
+
return {
|
|
63
|
+
code: 'bundle_file_not_code',
|
|
64
|
+
message: `"${path}" is not a script. A bundle holds only .js files and ${BUNDLE_MANIFEST}.`,
|
|
65
|
+
file: path,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!files.has(BUNDLE_MANIFEST)) {
|
|
70
|
+
return { code: 'bundle_manifest_missing', message: `That bundle has no ${BUNDLE_MANIFEST} at its root.` };
|
|
71
|
+
}
|
|
72
|
+
if (codeOf(files).size === 0) {
|
|
73
|
+
return { code: 'bundle_empty', message: 'That bundle has no screens in it, only a manifest.' };
|
|
74
|
+
}
|
|
75
|
+
const total = bundleBytes(files);
|
|
76
|
+
if (total > BUNDLE_MAX_BYTES) {
|
|
77
|
+
const [largest] = [...files].sort(([, a], [, b]) => b.length - a.length);
|
|
78
|
+
return {
|
|
79
|
+
code: 'bundle_too_large',
|
|
80
|
+
message: `That bundle is ${sizeOf(total)}, over the ${sizeOf(BUNDLE_MAX_BYTES)} cap. ` +
|
|
81
|
+
`The largest file is "${largest[0]}" at ${sizeOf(largest[1].length)}.`,
|
|
82
|
+
file: largest[0],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/** Every file's bytes, the manifest included: what the cap is measured on. */
|
|
88
|
+
export const bundleBytes = (files) => [...files.values()].reduce((sum, bytes) => sum + bytes.length, 0);
|
|
89
|
+
export function sizeOf(bytes) {
|
|
90
|
+
if (bytes < 1024)
|
|
91
|
+
return `${bytes} bytes`;
|
|
92
|
+
if (bytes < 1024 * 1024)
|
|
93
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
94
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
95
|
+
}
|
package/src/define.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A manifest written in TypeScript, checked as it is written (A5-F01-S03).
|
|
3
|
+
*
|
|
4
|
+
* `.brydio/app.json` is what Brydio reads, and `validate` checks it. An app
|
|
5
|
+
* that also keeps its manifest in code (to share its collections' schemas
|
|
6
|
+
* with its screens, say) gets one more check at type level: a placement that
|
|
7
|
+
* names a screen the manifest doesn't declare doesn't compile.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* export const manifest = defineManifest({
|
|
11
|
+
* name: 'issues',
|
|
12
|
+
* version: '0.1.0',
|
|
13
|
+
* screens: { board: { entry: 'screens/board.js' } },
|
|
14
|
+
* placements: [{ kind: 'project-tab', screen: 'bord' }], // error: "bord" is not a screen
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
/** The parts of a manifest this check reads. Everything else passes through as written. */
|
|
19
|
+
export interface ManifestShape {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
screens?: Readonly<Record<string, {
|
|
23
|
+
readonly entry: string;
|
|
24
|
+
}>>;
|
|
25
|
+
placements?: readonly {
|
|
26
|
+
readonly kind: string;
|
|
27
|
+
readonly screen: string;
|
|
28
|
+
readonly label?: string;
|
|
29
|
+
readonly icon?: string;
|
|
30
|
+
}[];
|
|
31
|
+
[field: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
/** The screen names a manifest declares. */
|
|
34
|
+
export type ScreenNameOf<M> = M extends {
|
|
35
|
+
readonly screens: infer S;
|
|
36
|
+
} ? Extract<keyof S, string> : never;
|
|
37
|
+
/** A manifest whose every placement names one of its own screens. */
|
|
38
|
+
export type WithDeclaredScreens<M> = M extends {
|
|
39
|
+
readonly placements: readonly unknown[];
|
|
40
|
+
} ? Omit<M, 'placements'> & {
|
|
41
|
+
readonly placements: readonly {
|
|
42
|
+
readonly kind: string;
|
|
43
|
+
readonly screen: ScreenNameOf<M>;
|
|
44
|
+
readonly label?: string;
|
|
45
|
+
readonly icon?: string;
|
|
46
|
+
}[];
|
|
47
|
+
} : M;
|
|
48
|
+
/** Returns the manifest as written, with its literal types, once its placements check. */
|
|
49
|
+
export declare function defineManifest<const M extends ManifestShape>(manifest: M & WithDeclaredScreens<M>): M;
|
package/src/define.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A manifest written in TypeScript, checked as it is written (A5-F01-S03).
|
|
3
|
+
*
|
|
4
|
+
* `.brydio/app.json` is what Brydio reads, and `validate` checks it. An app
|
|
5
|
+
* that also keeps its manifest in code (to share its collections' schemas
|
|
6
|
+
* with its screens, say) gets one more check at type level: a placement that
|
|
7
|
+
* names a screen the manifest doesn't declare doesn't compile.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* export const manifest = defineManifest({
|
|
11
|
+
* name: 'issues',
|
|
12
|
+
* version: '0.1.0',
|
|
13
|
+
* screens: { board: { entry: 'screens/board.js' } },
|
|
14
|
+
* placements: [{ kind: 'project-tab', screen: 'bord' }], // error: "bord" is not a screen
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
/** Returns the manifest as written, with its literal types, once its placements check. */
|
|
19
|
+
export function defineManifest(manifest) {
|
|
20
|
+
return manifest;
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounds on one app's records, as Brydio's store holds them (A3-F02-S04).
|
|
3
|
+
*
|
|
4
|
+
* A copy of `DOCUMENT_LIMITS` in Brydio's `apps/api/src/apps/data/document-query.ts`,
|
|
5
|
+
* held to it by `limits-doc.test.ts`, and listed for app builders in
|
|
6
|
+
* `docs/manifest.md`. `FIELD_LIMITS` holds the bounds on a schema and its values.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DOCUMENT_LIMITS: {
|
|
9
|
+
/** Records a list or search returns when it doesn't say how many. */
|
|
10
|
+
readonly pageDefault: 50;
|
|
11
|
+
/** The most records one list or search page may return. */
|
|
12
|
+
readonly pageMax: 200;
|
|
13
|
+
/** One record's stored body, in bytes: 256 KB, so a `text` field's 100,000 characters fit. */
|
|
14
|
+
readonly bodyBytes: number;
|
|
15
|
+
/** A page stops adding records once it passes this many bytes, and hands back a cursor. */
|
|
16
|
+
readonly pageBytes: number;
|
|
17
|
+
/** Live records one instance's collection may hold. */
|
|
18
|
+
readonly recordsPerCollection: 100000;
|
|
19
|
+
/** Changes one `batch_<plural>` call may make. */
|
|
20
|
+
readonly batchChanges: 50;
|
|
21
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounds on one app's records, as Brydio's store holds them (A3-F02-S04).
|
|
3
|
+
*
|
|
4
|
+
* A copy of `DOCUMENT_LIMITS` in Brydio's `apps/api/src/apps/data/document-query.ts`,
|
|
5
|
+
* held to it by `limits-doc.test.ts`, and listed for app builders in
|
|
6
|
+
* `docs/manifest.md`. `FIELD_LIMITS` holds the bounds on a schema and its values.
|
|
7
|
+
*/
|
|
8
|
+
export const DOCUMENT_LIMITS = {
|
|
9
|
+
/** Records a list or search returns when it doesn't say how many. */
|
|
10
|
+
pageDefault: 50,
|
|
11
|
+
/** The most records one list or search page may return. */
|
|
12
|
+
pageMax: 200,
|
|
13
|
+
/** One record's stored body, in bytes: 256 KB, so a `text` field's 100,000 characters fit. */
|
|
14
|
+
bodyBytes: 256 * 1024,
|
|
15
|
+
/** A page stops adding records once it passes this many bytes, and hands back a cursor. */
|
|
16
|
+
pageBytes: 2 * 1024 * 1024,
|
|
17
|
+
/** Live records one instance's collection may hold. */
|
|
18
|
+
recordsPerCollection: 100_000,
|
|
19
|
+
/** Changes one `batch_<plural>` call may make. */
|
|
20
|
+
batchChanges: 50,
|
|
21
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type ZodType } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The field types an app's collection may declare (contracts §5, A3-F01).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately few. A tracker, a library and a folder can all be written in
|
|
6
|
+
* these, and every one of them is something Brydio knows how to store, how to
|
|
7
|
+
* validate, how to describe to the assistant and how to draw in a form. A
|
|
8
|
+
* type an app could invent would be a type none of those four knew about.
|
|
9
|
+
*
|
|
10
|
+
* The manifest writes a type as a string (`"member?"`) or, for a choice, as
|
|
11
|
+
* the list of allowed values. `parseFieldType` turns either into one shape,
|
|
12
|
+
* and everything downstream — the store, the generated tools, the screen's
|
|
13
|
+
* form — reads that shape, never the manifest's spelling.
|
|
14
|
+
*/
|
|
15
|
+
export type FieldKind = 'string' | 'text' | 'enum' | 'member' | 'project' | 'date' | 'number' | 'boolean' | 'string[]' | 'token';
|
|
16
|
+
export interface FieldType {
|
|
17
|
+
kind: FieldKind;
|
|
18
|
+
/** Written with a trailing `?`: may be left out on create. */
|
|
19
|
+
optional: boolean;
|
|
20
|
+
/** The allowed values, for an enumeration and for a token. */
|
|
21
|
+
values?: readonly string[];
|
|
22
|
+
/** What a create that leaves the field out stores: a choice's value, or a boolean. */
|
|
23
|
+
default?: string | boolean;
|
|
24
|
+
/**
|
|
25
|
+
* How a choice's values read to a person: `{ todo: "To do" }`. Shown on the
|
|
26
|
+
* approval card and in the tools' descriptions; the stored value never
|
|
27
|
+
* changes, and a value with no label reads as itself.
|
|
28
|
+
*/
|
|
29
|
+
labels?: Readonly<Record<string, string>>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The bounds a schema and a record are held to (A3-F01-S03).
|
|
33
|
+
*
|
|
34
|
+
* `stringChars` is 1,000 (E1, 16 Sep: the feature file wins over contracts'
|
|
35
|
+
* old 512), the same as `bry-input`'s `INPUT_MAX`, so nothing a person can
|
|
36
|
+
* type into a text field is refused by the store.
|
|
37
|
+
*/
|
|
38
|
+
export declare const FIELD_LIMITS: {
|
|
39
|
+
/** Collections one app may keep. */
|
|
40
|
+
readonly collections: 20;
|
|
41
|
+
/** Fields one collection may have. */
|
|
42
|
+
readonly fields: 40;
|
|
43
|
+
/** Values one choice may allow. */
|
|
44
|
+
readonly enumValues: 50;
|
|
45
|
+
/** Characters in one of a choice's values. */
|
|
46
|
+
readonly enumValueChars: 64;
|
|
47
|
+
/** Characters in a `string` value, the same as `bry-input` lets a person type. */
|
|
48
|
+
readonly stringChars: 1000;
|
|
49
|
+
/** Characters in a `text` value. */
|
|
50
|
+
readonly textChars: 100000;
|
|
51
|
+
/** Entries in a `string[]` value. */
|
|
52
|
+
readonly listEntries: 100;
|
|
53
|
+
/** Characters in a collection, label, field or screen name. */
|
|
54
|
+
readonly nameChars: 40;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The colours a `token` field may hold, by design-token name.
|
|
58
|
+
*
|
|
59
|
+
* The status roles `DESIGN_SYSTEM.md` already gives a solid, a text, a tint
|
|
60
|
+
* and an edge, plus the brand and a neutral. A label picks one by name and
|
|
61
|
+
* the host draws it in the current theme, so an app never holds a colour
|
|
62
|
+
* value it could get wrong in dark mode (ADR-A13). `@brydio/manifest` carries
|
|
63
|
+
* the same list.
|
|
64
|
+
*/
|
|
65
|
+
export declare const COLOUR_TOKENS: readonly ["neutral", "brand", "success", "warn", "danger"];
|
|
66
|
+
/**
|
|
67
|
+
* Names Brydio keeps on every record, or that the generated tools already
|
|
68
|
+
* use for their own arguments. A field called `version` would be ambiguous in
|
|
69
|
+
* `update_issue`, and one called `instance` would shadow ADR-A09's argument.
|
|
70
|
+
*/
|
|
71
|
+
export declare const RESERVED_FIELDS: ReadonlySet<string>;
|
|
72
|
+
/** A field, collection or label name: an identifier the tools can use as-is. */
|
|
73
|
+
export declare const FIELD_NAME: RegExp;
|
|
74
|
+
export declare const COLLECTION_NAME: RegExp;
|
|
75
|
+
/** A field type the manifest wrote that Brydio does not have, or a bad list. */
|
|
76
|
+
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);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* One field's manifest spelling, as a type.
|
|
82
|
+
*
|
|
83
|
+
* Throws `FieldTypeInvalid` with a code rather than returning null, because
|
|
84
|
+
* "unknown type" and "a choice with 51 values" are different mistakes and the
|
|
85
|
+
* import report has to say which.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parseFieldType(raw: unknown): FieldType;
|
|
88
|
+
/** How long a choice value's label may be. */
|
|
89
|
+
export declare const LABEL_CHARS = 60;
|
|
90
|
+
/** How a choice's value reads to a person: its label, or the value itself. */
|
|
91
|
+
export declare const labelOfValue: (type: FieldType, value: string) => string;
|
|
92
|
+
/** True for a type whose value lands in the plain `fields` column. */
|
|
93
|
+
export declare const isStructured: (type: FieldType) => boolean;
|
|
94
|
+
/** True for a type a list can be ordered by: structured, and one value. */
|
|
95
|
+
export declare const isSortable: (type: FieldType) => boolean;
|
|
96
|
+
export declare const isSearchable: (type: FieldType) => boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The names of a schema's structured fields: the ones kept in plain beside the
|
|
99
|
+
* encrypted body, and so the ones a list may filter on.
|
|
100
|
+
*
|
|
101
|
+
* One function, imported by the store and by the tools, so what is written
|
|
102
|
+
* plainly and what may be filtered cannot drift apart (A3-F01's note). Accepts
|
|
103
|
+
* the manifest's spelling or parsed types.
|
|
104
|
+
*/
|
|
105
|
+
export declare function structuredFields(schema: Record<string, unknown>): string[];
|
|
106
|
+
/**
|
|
107
|
+
* What one value of this type may be, as zod.
|
|
108
|
+
*
|
|
109
|
+
* The same schema checks a record in the store and describes the argument to
|
|
110
|
+
* the model, so the tool never accepts something the store then refuses.
|
|
111
|
+
* Always the required form: callers add `.optional()` where they mean it.
|
|
112
|
+
*/
|
|
113
|
+
export declare function valueSchema(type: FieldType): ZodType;
|
|
114
|
+
/**
|
|
115
|
+
* Why a value is not one of this type, in words the assistant can repeat to
|
|
116
|
+
* the person, naming the field. Null when it is fine.
|
|
117
|
+
*/
|
|
118
|
+
export declare function valueProblem(field: string, type: FieldType, value: unknown): string | null;
|
|
119
|
+
/** A type in a few words, for a tool's description: every allowed value named. */
|
|
120
|
+
export declare function describeType(type: FieldType): string;
|
|
121
|
+
export declare const quoted: (values: readonly string[]) => string;
|
|
122
|
+
type Optional<T> = T extends `${string}?` ? true : T extends {
|
|
123
|
+
optional: true;
|
|
124
|
+
} ? true : T extends {
|
|
125
|
+
type: infer U;
|
|
126
|
+
} ? Optional<U> : false;
|
|
127
|
+
/** The TypeScript type of one field's value, from its manifest spelling. */
|
|
128
|
+
export type FieldValue<T> = T extends {
|
|
129
|
+
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 string ? string : never;
|
|
131
|
+
type Simplify<T> = {
|
|
132
|
+
[K in keyof T]: T[K];
|
|
133
|
+
} & {};
|
|
134
|
+
/** A schema's fields as a type: optional fields may be absent or null. */
|
|
135
|
+
export type FieldsOf<S> = Simplify<{
|
|
136
|
+
-readonly [K in keyof S as Optional<S[K]> extends true ? never : K]: FieldValue<S[K]>;
|
|
137
|
+
} & {
|
|
138
|
+
-readonly [K in keyof S as Optional<S[K]> extends true ? K : never]?: FieldValue<S[K]> | null;
|
|
139
|
+
}>;
|
|
140
|
+
/**
|
|
141
|
+
* A record of a collection, as the generated tools hand it out: its id and
|
|
142
|
+
* version beside the schema's fields, flat, so what `get` returns is the
|
|
143
|
+
* shape `update` takes.
|
|
144
|
+
*
|
|
145
|
+
* ```ts
|
|
146
|
+
* const schema = { title: 'string', status: ['todo', 'doing', 'done'] } as const;
|
|
147
|
+
* type Issue = DocumentOf<typeof schema>; // { id; version; title: string; status: 'todo' | … }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export type DocumentOf<S> = Simplify<{
|
|
151
|
+
id: string;
|
|
152
|
+
version: number;
|
|
153
|
+
createdBy?: string;
|
|
154
|
+
createdAt?: string;
|
|
155
|
+
updatedAt?: string;
|
|
156
|
+
} & FieldsOf<S>>;
|
|
157
|
+
export {};
|