@brydio/manifest 0.1.0-alpha.35 → 0.1.0-alpha.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/anonymous.d.ts +42 -0
- package/src/anonymous.js +86 -0
- package/src/grants.d.ts +10 -2
- package/src/grants.js +24 -2
- package/src/index.d.ts +2 -1
- package/src/index.js +2 -1
- package/src/migrations.d.ts +1 -1
- package/src/migrations.js +29 -0
- package/src/schema.d.ts +40 -1
- package/src/schema.js +59 -2
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.37",
|
|
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": "c0364642e34a602b75477af366aa61582e5598f7",
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
16
|
"types": "./src/index.d.ts",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* An anonymous collection (P13): answers nobody can tie back to the person
|
|
4
|
+
* who gave them. Mirrors Brydio's `apps/api/src/apps/manifest/anonymous.ts`;
|
|
5
|
+
* the two change together.
|
|
6
|
+
*
|
|
7
|
+
* An app only declares it: which structured field groups the answers (a
|
|
8
|
+
* form, a survey round: a number, a choice, a date), and how many answers a
|
|
9
|
+
* group must hold before any of them is read. Brydio does the rest in the store, never the app: no writer kept
|
|
10
|
+
* on the record, times cut to the day, no live change told, no update, a
|
|
11
|
+
* create naming its own writer refused, and reads only of a whole group of
|
|
12
|
+
* at least `minimum` answers.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ANONYMOUS_LIMITS: {
|
|
15
|
+
/** The fewest answers a group, or a narrowed read of one, may be read at. */
|
|
16
|
+
readonly minimum: 5;
|
|
17
|
+
/** The most an app may ask for. */
|
|
18
|
+
readonly maximum: 1000;
|
|
19
|
+
};
|
|
20
|
+
/** What the store writes as `created_by` on every anonymous record. */
|
|
21
|
+
export declare const ANONYMOUS_WRITER = "anonymous";
|
|
22
|
+
export declare const anonymousSchema: z.ZodObject<{
|
|
23
|
+
group: z.ZodString;
|
|
24
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
export type AnonymousDeclared = z.infer<typeof anonymousSchema>;
|
|
27
|
+
/** As the host reads it, on `CollectionSpec.anonymous`. */
|
|
28
|
+
export interface AnonymousSpec {
|
|
29
|
+
group: string;
|
|
30
|
+
minimum: number;
|
|
31
|
+
}
|
|
32
|
+
export type AnonymousProblemCode = 'data_anonymous_group' | 'data_anonymous_member' | 'data_anonymous_minimum' | 'data_anonymous_coedit';
|
|
33
|
+
export declare function anonymousProblems(collection: string, declared: {
|
|
34
|
+
schema: Record<string, unknown>;
|
|
35
|
+
anonymous?: AnonymousDeclared;
|
|
36
|
+
}): {
|
|
37
|
+
code: AnonymousProblemCode;
|
|
38
|
+
collection: string;
|
|
39
|
+
field?: string;
|
|
40
|
+
message: string;
|
|
41
|
+
}[];
|
|
42
|
+
export declare function anonymousSpec(declared: AnonymousDeclared): AnonymousSpec;
|
package/src/anonymous.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { FIELD_LIMITS, isStructured, parseFieldType, } from "./field-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* An anonymous collection (P13): answers nobody can tie back to the person
|
|
5
|
+
* who gave them. Mirrors Brydio's `apps/api/src/apps/manifest/anonymous.ts`;
|
|
6
|
+
* the two change together.
|
|
7
|
+
*
|
|
8
|
+
* An app only declares it: which structured field groups the answers (a
|
|
9
|
+
* form, a survey round: a number, a choice, a date), and how many answers a
|
|
10
|
+
* group must hold before any of them is read. Brydio does the rest in the store, never the app: no writer kept
|
|
11
|
+
* on the record, times cut to the day, no live change told, no update, a
|
|
12
|
+
* create naming its own writer refused, and reads only of a whole group of
|
|
13
|
+
* at least `minimum` answers.
|
|
14
|
+
*/
|
|
15
|
+
export const ANONYMOUS_LIMITS = {
|
|
16
|
+
/** The fewest answers a group, or a narrowed read of one, may be read at. */
|
|
17
|
+
minimum: 5,
|
|
18
|
+
/** The most an app may ask for. */
|
|
19
|
+
maximum: 1000,
|
|
20
|
+
};
|
|
21
|
+
/** What the store writes as `created_by` on every anonymous record. */
|
|
22
|
+
export const ANONYMOUS_WRITER = 'anonymous';
|
|
23
|
+
export const anonymousSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
/** A structured field of the same collection, never a member: which group an answer belongs to. */
|
|
26
|
+
group: z.string().min(1).max(FIELD_LIMITS.nameChars),
|
|
27
|
+
/** At least 5, the default. */
|
|
28
|
+
minimum: z.number().int().max(ANONYMOUS_LIMITS.maximum).optional(),
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
31
|
+
export function anonymousProblems(collection, declared) {
|
|
32
|
+
const anonymous = declared.anonymous;
|
|
33
|
+
if (!anonymous)
|
|
34
|
+
return [];
|
|
35
|
+
const problems = [];
|
|
36
|
+
const types = new Map();
|
|
37
|
+
for (const [field, raw] of Object.entries(declared.schema)) {
|
|
38
|
+
try {
|
|
39
|
+
types.set(field, parseFieldType(raw));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Refused as a field type already.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Kept in plain, so a read can name its group; words never are.
|
|
46
|
+
const group = types.get(anonymous.group);
|
|
47
|
+
if (!group || !isStructured(group) || group.kind === 'member') {
|
|
48
|
+
problems.push({
|
|
49
|
+
code: 'data_anonymous_group',
|
|
50
|
+
collection,
|
|
51
|
+
field: anonymous.group,
|
|
52
|
+
message: `${collection}.anonymous.group must name one of its structured fields (a number, choice, date, boolean, token or project); ${anonymous.group} is not.`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (anonymous.minimum !== undefined && anonymous.minimum < ANONYMOUS_LIMITS.minimum) {
|
|
56
|
+
problems.push({
|
|
57
|
+
code: 'data_anonymous_minimum',
|
|
58
|
+
collection,
|
|
59
|
+
message: `${collection}.anonymous.minimum is at least ${ANONYMOUS_LIMITS.minimum}: fewer answers than that can be told apart.`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
for (const [field, type] of types) {
|
|
63
|
+
// A member field names a person on the record, which is what this is for not doing.
|
|
64
|
+
if (type.kind === 'member') {
|
|
65
|
+
problems.push({
|
|
66
|
+
code: 'data_anonymous_member',
|
|
67
|
+
collection,
|
|
68
|
+
field,
|
|
69
|
+
message: `${collection} is anonymous, so it can't name a member: ${field} would.`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Writing together shows who is typing, and changes a record after it is given.
|
|
73
|
+
if (type.coedit) {
|
|
74
|
+
problems.push({
|
|
75
|
+
code: 'data_anonymous_coedit',
|
|
76
|
+
collection,
|
|
77
|
+
field,
|
|
78
|
+
message: `${collection} is anonymous, so its answers can't be written together: ${field} would be.`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return problems;
|
|
83
|
+
}
|
|
84
|
+
export function anonymousSpec(declared) {
|
|
85
|
+
return { group: declared.group, minimum: declared.minimum ?? ANONYMOUS_LIMITS.minimum };
|
|
86
|
+
}
|
package/src/grants.d.ts
CHANGED
|
@@ -6,8 +6,16 @@
|
|
|
6
6
|
* `apps/api/src/apps/manifest/grants.ts`, and the `grant_unknown` sentence in
|
|
7
7
|
* `apps/api/src/apps/publishing/app-publish.service.ts`.
|
|
8
8
|
*/
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The host grants with a name of their own. A connection's is `connection:<name>`.
|
|
11
|
+
*
|
|
12
|
+
* `chats` and `chat` differ: `chats` is the typed Brydio API's reach into the
|
|
13
|
+
* person's assistant conversations; `chat` is a handler's `chat.post`, the
|
|
14
|
+
* app posting one of its cards into a workspace room (FO03). `directory` is
|
|
15
|
+
* a handler's `directory.*` and a screen's `useProfile` (FO02); `webhooks`
|
|
16
|
+
* is a handler's `webhooks.send` (FO07).
|
|
17
|
+
*/
|
|
18
|
+
export declare const HOST_CAPABILITIES: readonly ["navigate", "message", "members", "projects", "files", "chats", "model", "secrets", "notify", "approvals", "chat", "directory", "webhooks"];
|
|
11
19
|
/** The older name for `HOST_CAPABILITIES`, kept for what already imports it. */
|
|
12
20
|
export declare const KNOWN_HOST_GRANTS: readonly string[];
|
|
13
21
|
/** True for a host grant Brydio knows how to honour: a named capability, or one connection. */
|
package/src/grants.js
CHANGED
|
@@ -7,8 +7,30 @@
|
|
|
7
7
|
* `apps/api/src/apps/publishing/app-publish.service.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import { BUNDLE_MANIFEST } from "./bundle.js";
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The host grants with a name of their own. A connection's is `connection:<name>`.
|
|
12
|
+
*
|
|
13
|
+
* `chats` and `chat` differ: `chats` is the typed Brydio API's reach into the
|
|
14
|
+
* person's assistant conversations; `chat` is a handler's `chat.post`, the
|
|
15
|
+
* app posting one of its cards into a workspace room (FO03). `directory` is
|
|
16
|
+
* a handler's `directory.*` and a screen's `useProfile` (FO02); `webhooks`
|
|
17
|
+
* is a handler's `webhooks.send` (FO07).
|
|
18
|
+
*/
|
|
19
|
+
export const HOST_CAPABILITIES = [
|
|
20
|
+
'navigate',
|
|
21
|
+
'message',
|
|
22
|
+
'members',
|
|
23
|
+
'projects',
|
|
24
|
+
'files',
|
|
25
|
+
'chats',
|
|
26
|
+
'model',
|
|
27
|
+
'secrets',
|
|
28
|
+
'notify',
|
|
29
|
+
'approvals',
|
|
30
|
+
'chat',
|
|
31
|
+
'directory',
|
|
32
|
+
'webhooks',
|
|
33
|
+
];
|
|
12
34
|
/** The older name for `HOST_CAPABILITIES`, kept for what already imports it. */
|
|
13
35
|
export const KNOWN_HOST_GRANTS = HOST_CAPABILITIES;
|
|
14
36
|
const CONNECTION = /^connection:[a-z0-9][a-z0-9_-]{0,59}$/;
|
package/src/index.d.ts
CHANGED
|
@@ -7,9 +7,10 @@ export { APP_NAME_FORMAT, MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema, br
|
|
|
7
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
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
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
|
+
export { appManifestSchema, appToolPrefix, collectionOf, crossAppTool, 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';
|
|
11
11
|
export { OPEN_LIMITS, OPEN_TYPES, choicesProblem, definitionOf, definitionTypeProblem, describeField, fieldTypeOf, hasChoices, isPlainOpenType, keyFromName, keyProblem, missingRequired, moveValue, openSchemaProblems, openSpec, openValueProblem, type Moved, type OpenField, type OpenFieldChange, type OpenFieldValue, type OpenProblem, type OpenProblemCode, type OpenSchemaFlag, type OpenType, } from './open-schema.js';
|
|
12
12
|
export { CONFIRM_LIMITS, confirmEmailProblems, confirmEmailSpec, type ConfirmEmailDeclared, type ConfirmEmailSpec, type ConfirmProblemCode, } from './confirm-email.js';
|
|
13
|
+
export { ANONYMOUS_LIMITS, ANONYMOUS_WRITER, anonymousProblems, anonymousSpec, type AnonymousDeclared, type AnonymousProblemCode, type AnonymousSpec, } from './anonymous.js';
|
|
13
14
|
export { DOCUMENT_LIMITS } from './document-limits.js';
|
|
14
15
|
export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from './grants.js';
|
|
15
16
|
export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, type Migration, type MigrationCode, type MigrationProblem, type MigrationStep, type SchemaChange, } from './migrations.js';
|
package/src/index.js
CHANGED
|
@@ -7,9 +7,10 @@ export { APP_NAME_FORMAT, MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema, br
|
|
|
7
7
|
export { BRAND_FIELDS, BRAND_LIMITS, brandFilePath, brandPathsOf, brandProblems, brandShapeOf, isBrandPath, pngVerdict, svgColours, toolsProblem, } from "./requirements.js";
|
|
8
8
|
export { BUNDLE_MANIFEST, BUNDLE_MAX_BYTES, BUNDLE_PATH_MAX_CHARS, bundleBytes, bundleHash, brandOf, bundleProblem, codeOf, isBundlePath, isScriptPath, sizeOf, } from "./bundle.js";
|
|
9
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
|
+
export { appManifestSchema, appToolPrefix, collectionOf, crossAppTool, 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";
|
|
11
11
|
export { OPEN_LIMITS, OPEN_TYPES, choicesProblem, definitionOf, definitionTypeProblem, describeField, fieldTypeOf, hasChoices, isPlainOpenType, keyFromName, keyProblem, missingRequired, moveValue, openSchemaProblems, openSpec, openValueProblem, } from "./open-schema.js";
|
|
12
12
|
export { CONFIRM_LIMITS, confirmEmailProblems, confirmEmailSpec, } from "./confirm-email.js";
|
|
13
|
+
export { ANONYMOUS_LIMITS, ANONYMOUS_WRITER, anonymousProblems, anonymousSpec, } from "./anonymous.js";
|
|
13
14
|
export { DOCUMENT_LIMITS } from "./document-limits.js";
|
|
14
15
|
export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from "./grants.js";
|
|
15
16
|
export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, } from "./migrations.js";
|
package/src/migrations.d.ts
CHANGED
|
@@ -115,7 +115,7 @@ export type SchemaChange = {
|
|
|
115
115
|
* Hodler's `diffManifests` to show, and none of them moves a record.
|
|
116
116
|
*/
|
|
117
117
|
export declare function diffSchemas(from: unknown, to: unknown): SchemaChange[];
|
|
118
|
-
export type MigrationCode = 'migration_step_invalid' | 'migration_step_pointless' | 'migration_default_missing' | 'migration_default_invalid' | 'migration_unexplained' | 'migration_type_changed' | 'migration_value_removed';
|
|
118
|
+
export type MigrationCode = 'migration_step_invalid' | 'migration_step_pointless' | 'migration_default_missing' | 'migration_default_invalid' | 'migration_unexplained' | 'migration_type_changed' | 'migration_value_removed' | 'migration_anonymous_changed';
|
|
119
119
|
export interface MigrationProblem {
|
|
120
120
|
code: MigrationCode;
|
|
121
121
|
collection: string;
|
package/src/migrations.js
CHANGED
|
@@ -181,6 +181,35 @@ export function migrationProblems(from, to, steps) {
|
|
|
181
181
|
break;
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
+
problems.push(...anonymityChanges(from, to));
|
|
185
|
+
return problems;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A kept collection that becomes anonymous, stops being, or moves its group
|
|
189
|
+
* (P13): no step can say what its records become. Records kept before kept
|
|
190
|
+
* their writers and times, and records kept since were read only by group.
|
|
191
|
+
*/
|
|
192
|
+
function anonymityChanges(from, to) {
|
|
193
|
+
const before = record(record(from).data);
|
|
194
|
+
const after = record(record(to).data);
|
|
195
|
+
const problems = [];
|
|
196
|
+
for (const [name, declared] of Object.entries(after)) {
|
|
197
|
+
if (!(name in before))
|
|
198
|
+
continue;
|
|
199
|
+
const was = record(record(before[name]).anonymous).group;
|
|
200
|
+
const now = record(record(declared).anonymous).group;
|
|
201
|
+
if (was === now)
|
|
202
|
+
continue;
|
|
203
|
+
problems.push({
|
|
204
|
+
code: 'migration_anonymous_changed',
|
|
205
|
+
collection: name,
|
|
206
|
+
message: was === undefined
|
|
207
|
+
? `${name} already keeps records with their writers, so it can't become anonymous; keep anonymous answers in a new collection.`
|
|
208
|
+
: now === undefined
|
|
209
|
+
? `${name} keeps anonymous answers, so it stays anonymous.`
|
|
210
|
+
: `${name}'s anonymous answers stay grouped by ${String(was)}.`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
184
213
|
return problems;
|
|
185
214
|
}
|
|
186
215
|
/** The old schema with the steps applied, and every step that could not apply or changed nothing. */
|
package/src/schema.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { FieldTypeInvalid, type FieldType } from './field-types.js';
|
|
3
3
|
import { type OpenProblemCode } from './open-schema.js';
|
|
4
4
|
import { type ConfirmProblemCode } from './confirm-email.js';
|
|
5
|
+
import { type AnonymousProblemCode, type AnonymousSpec } from './anonymous.js';
|
|
5
6
|
export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, type FieldKind, type FieldType, } from './field-types.js';
|
|
6
7
|
/**
|
|
7
8
|
* What an app adds to `.brydio/app.json` to be more than a bundle of servers
|
|
@@ -101,6 +102,7 @@ declare const extensionShape: {
|
|
|
101
102
|
"project-sidebar": "project-sidebar";
|
|
102
103
|
"workspace-sidebar": "workspace-sidebar";
|
|
103
104
|
"public-page": "public-page";
|
|
105
|
+
"chat-card": "chat-card";
|
|
104
106
|
}>;
|
|
105
107
|
screen: z.ZodString;
|
|
106
108
|
sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
@@ -157,6 +159,10 @@ declare const extensionShape: {
|
|
|
157
159
|
message: z.ZodOptional<z.ZodString>;
|
|
158
160
|
link: z.ZodOptional<z.ZodBoolean>;
|
|
159
161
|
}, z.core.$strict>>;
|
|
162
|
+
anonymous: z.ZodOptional<z.ZodObject<{
|
|
163
|
+
group: z.ZodString;
|
|
164
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
165
|
+
}, z.core.$strict>>;
|
|
160
166
|
}, z.core.$strip>>>;
|
|
161
167
|
tools: z.ZodOptional<z.ZodObject<{
|
|
162
168
|
generated: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -235,7 +241,7 @@ export interface DataProblem {
|
|
|
235
241
|
field?: string;
|
|
236
242
|
message: string;
|
|
237
243
|
}
|
|
238
|
-
export type DataProblemCode = FieldTypeInvalid['code'] | OpenProblemCode | ConfirmProblemCode | '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_home_sizes' | 'placement_sizes_not_home' | 'placement_widget_sizes' | 'placement_tab_retired' | 'placement_public_shape' | 'placement_public_nothing' | 'placement_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
|
|
244
|
+
export type DataProblemCode = FieldTypeInvalid['code'] | OpenProblemCode | ConfirmProblemCode | AnonymousProblemCode | '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_home_sizes' | 'placement_sizes_not_home' | 'placement_widget_sizes' | 'placement_tab_retired' | 'placement_public_shape' | 'placement_public_nothing' | 'placement_chat_card_shape' | 'grant_tool_cross_app_self' | 'placement_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
|
|
239
245
|
type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
|
|
240
246
|
/**
|
|
241
247
|
* Everything wrong with an app's additions that a type cannot say.
|
|
@@ -251,6 +257,17 @@ export interface DataProblemOptions {
|
|
|
251
257
|
retired?: boolean;
|
|
252
258
|
}
|
|
253
259
|
export declare function dataProblems(additions: Additions, options?: DataProblemOptions): DataProblem[];
|
|
260
|
+
/**
|
|
261
|
+
* Another installed app's tool, as `grants.tools` names it (FO07): the name
|
|
262
|
+
* the assistant knows it by, `<slug>__<tool>`. Null for anything else.
|
|
263
|
+
* A handler calls it with `tools.call('<slug>__<tool>', input)`.
|
|
264
|
+
*/
|
|
265
|
+
export declare function crossAppTool(name: string): {
|
|
266
|
+
app: string;
|
|
267
|
+
tool: string;
|
|
268
|
+
} | null;
|
|
269
|
+
/** An app's name as the front of its tools' assistant names: `issue-tracker` → `issue_tracker`. */
|
|
270
|
+
export declare const appToolPrefix: (name: string) => string;
|
|
254
271
|
/** The additions alone, for code that has the rest of the manifest already. */
|
|
255
272
|
export declare const manifestExtensionsSchema: z.ZodObject<{
|
|
256
273
|
placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -262,6 +279,7 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
|
|
|
262
279
|
"project-sidebar": "project-sidebar";
|
|
263
280
|
"workspace-sidebar": "workspace-sidebar";
|
|
264
281
|
"public-page": "public-page";
|
|
282
|
+
"chat-card": "chat-card";
|
|
265
283
|
}>;
|
|
266
284
|
screen: z.ZodString;
|
|
267
285
|
sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
@@ -318,6 +336,10 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
|
|
|
318
336
|
message: z.ZodOptional<z.ZodString>;
|
|
319
337
|
link: z.ZodOptional<z.ZodBoolean>;
|
|
320
338
|
}, z.core.$strict>>;
|
|
339
|
+
anonymous: z.ZodOptional<z.ZodObject<{
|
|
340
|
+
group: z.ZodString;
|
|
341
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
342
|
+
}, z.core.$strict>>;
|
|
321
343
|
}, z.core.$strip>>>;
|
|
322
344
|
tools: z.ZodOptional<z.ZodObject<{
|
|
323
345
|
generated: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -393,6 +415,7 @@ export declare const storedExtensionsSchema: z.ZodObject<{
|
|
|
393
415
|
"project-sidebar": "project-sidebar";
|
|
394
416
|
"workspace-sidebar": "workspace-sidebar";
|
|
395
417
|
"public-page": "public-page";
|
|
418
|
+
"chat-card": "chat-card";
|
|
396
419
|
}>;
|
|
397
420
|
screen: z.ZodString;
|
|
398
421
|
sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
@@ -449,6 +472,10 @@ export declare const storedExtensionsSchema: z.ZodObject<{
|
|
|
449
472
|
message: z.ZodOptional<z.ZodString>;
|
|
450
473
|
link: z.ZodOptional<z.ZodBoolean>;
|
|
451
474
|
}, z.core.$strict>>;
|
|
475
|
+
anonymous: z.ZodOptional<z.ZodObject<{
|
|
476
|
+
group: z.ZodString;
|
|
477
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
478
|
+
}, z.core.$strict>>;
|
|
452
479
|
}, z.core.$strip>>>;
|
|
453
480
|
tools: z.ZodOptional<z.ZodObject<{
|
|
454
481
|
generated: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -559,6 +586,7 @@ export declare const appManifestSchema: z.ZodObject<{
|
|
|
559
586
|
"project-sidebar": "project-sidebar";
|
|
560
587
|
"workspace-sidebar": "workspace-sidebar";
|
|
561
588
|
"public-page": "public-page";
|
|
589
|
+
"chat-card": "chat-card";
|
|
562
590
|
}>;
|
|
563
591
|
screen: z.ZodString;
|
|
564
592
|
sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
@@ -615,6 +643,10 @@ export declare const appManifestSchema: z.ZodObject<{
|
|
|
615
643
|
message: z.ZodOptional<z.ZodString>;
|
|
616
644
|
link: z.ZodOptional<z.ZodBoolean>;
|
|
617
645
|
}, z.core.$strict>>;
|
|
646
|
+
anonymous: z.ZodOptional<z.ZodObject<{
|
|
647
|
+
group: z.ZodString;
|
|
648
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
649
|
+
}, z.core.$strict>>;
|
|
618
650
|
}, z.core.$strip>>>;
|
|
619
651
|
tools: z.ZodOptional<z.ZodObject<{
|
|
620
652
|
generated: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -729,6 +761,7 @@ export declare const newAppManifestSchema: z.ZodObject<{
|
|
|
729
761
|
"project-sidebar": "project-sidebar";
|
|
730
762
|
"workspace-sidebar": "workspace-sidebar";
|
|
731
763
|
"public-page": "public-page";
|
|
764
|
+
"chat-card": "chat-card";
|
|
732
765
|
}>;
|
|
733
766
|
screen: z.ZodString;
|
|
734
767
|
sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
@@ -785,6 +818,10 @@ export declare const newAppManifestSchema: z.ZodObject<{
|
|
|
785
818
|
message: z.ZodOptional<z.ZodString>;
|
|
786
819
|
link: z.ZodOptional<z.ZodBoolean>;
|
|
787
820
|
}, z.core.$strict>>;
|
|
821
|
+
anonymous: z.ZodOptional<z.ZodObject<{
|
|
822
|
+
group: z.ZodString;
|
|
823
|
+
minimum: z.ZodOptional<z.ZodNumber>;
|
|
824
|
+
}, z.core.$strict>>;
|
|
788
825
|
}, z.core.$strip>>>;
|
|
789
826
|
tools: z.ZodOptional<z.ZodObject<{
|
|
790
827
|
generated: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -875,6 +912,8 @@ export interface CollectionSpec {
|
|
|
875
912
|
};
|
|
876
913
|
/** On a companion: the open collection whose fields its records define (P5). */
|
|
877
914
|
definesFieldsOf?: string;
|
|
915
|
+
/** Answers kept with no writer, read only a whole group of at least `minimum` at a time (P13). */
|
|
916
|
+
anonymous?: AnonymousSpec;
|
|
878
917
|
}
|
|
879
918
|
/**
|
|
880
919
|
* The singular a collection's tools are named with: the manifest's, else the
|
package/src/schema.js
CHANGED
|
@@ -4,6 +4,7 @@ import { COLLECTION_NAME, FIELD_LIMITS, FIELD_NAME, FieldTypeInvalid, isSearchab
|
|
|
4
4
|
import { migrationsSchema } from "./migrations.js";
|
|
5
5
|
import { openSchemaProblems } from "./open-schema.js";
|
|
6
6
|
import { confirmEmailProblems, confirmEmailSchema } from "./confirm-email.js";
|
|
7
|
+
import { anonymousProblems, anonymousSchema, anonymousSpec } from "./anonymous.js";
|
|
7
8
|
export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, } from "./field-types.js";
|
|
8
9
|
/**
|
|
9
10
|
* What an app adds to `.brydio/app.json` to be more than a bundle of servers
|
|
@@ -71,8 +72,12 @@ const placementSchema = z.object({
|
|
|
71
72
|
* `children` or `settings` (`placement_public_shape`), and it reaches only
|
|
72
73
|
* collections marked `publicRead` or `publicSubmit` and tools marked
|
|
73
74
|
* `public`.
|
|
75
|
+
* `chat-card`: a screen drawn inside a chat message the app posted with
|
|
76
|
+
* a handler's `chat.post` (FO03). Nobody places it by hand, so it has no
|
|
77
|
+
* `sizes`, `children` or `settings` (`placement_chat_card_shape`); its
|
|
78
|
+
* screen sees `placement.kind === 'chat-card'` and the card's `route`.
|
|
74
79
|
*/
|
|
75
|
-
kind: z.enum(['project-tab', 'project-widget', 'project-sidebar', 'workspace-sidebar', 'home', 'public-page']),
|
|
80
|
+
kind: z.enum(['project-tab', 'project-widget', 'project-sidebar', 'workspace-sidebar', 'home', 'public-page', 'chat-card']),
|
|
76
81
|
screen: z.string().min(1).max(FIELD_LIMITS.nameChars),
|
|
77
82
|
/** For `home` and `project-widget` only, and required there: the sizes the screen draws well at. */
|
|
78
83
|
sizes: z.array(z.enum(HOME_CARD_SIZES)).max(3).optional(),
|
|
@@ -138,6 +143,15 @@ const collectionSchema = z.object({
|
|
|
138
143
|
* Needs `publicSubmit`. Brydio decides when, how often and from whom.
|
|
139
144
|
*/
|
|
140
145
|
confirmEmail: confirmEmailSchema.optional(),
|
|
146
|
+
/**
|
|
147
|
+
* Answers nobody can tie to who gave them (P13): `group` names a
|
|
148
|
+
* structured field (a number, choice, date, boolean, token or project)
|
|
149
|
+
* the answers are read by, a whole group at a time and only once it holds
|
|
150
|
+
* `minimum` answers (at least 5, the default). Brydio keeps no writer,
|
|
151
|
+
* cuts times to the day, tells no live change, and refuses an update, a
|
|
152
|
+
* batch, a watch and a create naming its own writer.
|
|
153
|
+
*/
|
|
154
|
+
anonymous: anonymousSchema.optional(),
|
|
141
155
|
});
|
|
142
156
|
const screenSchema = z.object({
|
|
143
157
|
/**
|
|
@@ -371,6 +385,10 @@ export function dataProblems(additions, options = {}) {
|
|
|
371
385
|
for (const [collection, declared] of collections) {
|
|
372
386
|
problems.push(...confirmEmailProblems(collection, declared));
|
|
373
387
|
}
|
|
388
|
+
// An anonymous collection groups its answers by one of its structured fields, names nobody (P13).
|
|
389
|
+
for (const [collection, declared] of collections) {
|
|
390
|
+
problems.push(...anonymousProblems(collection, declared));
|
|
391
|
+
}
|
|
374
392
|
// What the app keeps must be what it asks to keep (A3-F06-S01): a
|
|
375
393
|
// collection the grants leave out would be data a workspace never agreed to
|
|
376
394
|
// hold, found only when the first write is refused.
|
|
@@ -440,6 +458,14 @@ export function dataProblems(additions, options = {}) {
|
|
|
440
458
|
message: `The public page "${placement.screen}" can have no children or settings: it is one screen at its own address.`,
|
|
441
459
|
});
|
|
442
460
|
}
|
|
461
|
+
// A chat card is drawn at the message's own size, in a message nobody
|
|
462
|
+
// places by hand (FO03): nothing to size, configure or list.
|
|
463
|
+
if (placement.kind === 'chat-card' && (placement.sizes?.length || placement.children || placement.settings)) {
|
|
464
|
+
problems.push({
|
|
465
|
+
code: 'placement_chat_card_shape',
|
|
466
|
+
message: `The chat card "${placement.screen}" cannot have sizes, children or settings.`,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
443
469
|
}
|
|
444
470
|
// A public page with nothing public could show a visitor nothing at all.
|
|
445
471
|
if ((additions.placements ?? []).some(placement => placement.kind === 'public-page')) {
|
|
@@ -537,6 +563,20 @@ function customToolProblems(additions, options) {
|
|
|
537
563
|
}));
|
|
538
564
|
const seen = new Set();
|
|
539
565
|
const granted = additions.grants?.tools ?? [];
|
|
566
|
+
// Another app's tool, asked for by its assistant name `<slug>__<tool>`
|
|
567
|
+
// (FO07): never an unknown custom tool of this app, but naming the app
|
|
568
|
+
// itself would be a way round its own grants.
|
|
569
|
+
const name = additions.name;
|
|
570
|
+
const own = typeof name === 'string' ? appToolPrefix(name) : null;
|
|
571
|
+
for (const grant of granted) {
|
|
572
|
+
const qualified = crossAppTool(grant);
|
|
573
|
+
if (qualified && own !== null && qualified.app === own) {
|
|
574
|
+
problems.push({
|
|
575
|
+
code: 'grant_tool_cross_app_self',
|
|
576
|
+
message: `${grant} names this app's own tool: ask for ${qualified.tool} instead.`,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
540
580
|
for (const tool of custom) {
|
|
541
581
|
if (generated.has(tool.name) || seen.has(tool.name)) {
|
|
542
582
|
problems.push({
|
|
@@ -573,6 +613,22 @@ function customToolProblems(additions, options) {
|
|
|
573
613
|
}
|
|
574
614
|
return problems;
|
|
575
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* Another installed app's tool, as `grants.tools` names it (FO07): the name
|
|
618
|
+
* the assistant knows it by, `<slug>__<tool>`. Null for anything else.
|
|
619
|
+
* A handler calls it with `tools.call('<slug>__<tool>', input)`.
|
|
620
|
+
*/
|
|
621
|
+
export function crossAppTool(name) {
|
|
622
|
+
const at = name.indexOf('__');
|
|
623
|
+
if (at <= 0 || at + 2 >= name.length)
|
|
624
|
+
return null;
|
|
625
|
+
return { app: name.slice(0, at), tool: name.slice(at + 2) };
|
|
626
|
+
}
|
|
627
|
+
/** An app's name as the front of its tools' assistant names: `issue-tracker` → `issue_tracker`. */
|
|
628
|
+
export const appToolPrefix = (name) => name
|
|
629
|
+
.replace(/[^A-Za-z0-9_]+/g, '_')
|
|
630
|
+
.replace(/_+/g, '_')
|
|
631
|
+
.replace(/^_+|_+$/g, '');
|
|
576
632
|
/**
|
|
577
633
|
* Each secret declared once, and asked for (ADR-A24): an app that declares
|
|
578
634
|
* secrets without the `secrets` host grant could never read one.
|
|
@@ -605,7 +661,7 @@ const refuse = (additions, ctx, options = {}) => {
|
|
|
605
661
|
ctx.addIssue({
|
|
606
662
|
code: 'custom',
|
|
607
663
|
message: problem.message,
|
|
608
|
-
path: problem.code === 'grant_tool_missing'
|
|
664
|
+
path: problem.code === 'grant_tool_missing' || problem.code === 'grant_tool_cross_app_self'
|
|
609
665
|
? ['grants', 'tools']
|
|
610
666
|
: problem.code === 'grant_secrets_missing'
|
|
611
667
|
? ['grants', 'host']
|
|
@@ -681,6 +737,7 @@ export function collectionsOf(manifest) {
|
|
|
681
737
|
? { openSchema: { fields: declared.openSchema.fields, table: declared.openSchema.table ?? null } }
|
|
682
738
|
: {}),
|
|
683
739
|
...(owner ? { definesFieldsOf: owner.name } : {}),
|
|
740
|
+
...(declared.anonymous ? { anonymous: anonymousSpec(declared.anonymous) } : {}),
|
|
684
741
|
};
|
|
685
742
|
});
|
|
686
743
|
}
|