@brydio/manifest 0.1.0-alpha.36 → 0.1.0-alpha.38

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brydio/manifest",
3
- "version": "0.1.0-alpha.36",
3
+ "version": "0.1.0-alpha.38",
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": "2d8e769d02c74fe201ce734f6d77def59ea99d53",
13
+ "gitHead": "4b6c80dcb849195171699f16022aab1f55d6e3b6",
14
14
  "exports": {
15
15
  ".": {
16
16
  "types": "./src/index.d.ts",
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
- /** 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", "files", "chats", "model", "secrets", "notify", "approvals"];
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
- /** The host grants with a name of their own. A connection's is `connection:<name>`. */
11
- export const HOST_CAPABILITIES = ['navigate', 'message', 'members', 'projects', 'files', 'chats', 'model', 'secrets', 'notify', 'approvals'];
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,10 +7,11 @@ 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
13
  export { ANONYMOUS_LIMITS, ANONYMOUS_WRITER, anonymousProblems, anonymousSpec, type AnonymousDeclared, type AnonymousProblemCode, type AnonymousSpec, } from './anonymous.js';
14
+ export { READERS_LIMIT, readersOf, readersProblems, type ReadersProblemCode } from './readers.js';
14
15
  export { DOCUMENT_LIMITS } from './document-limits.js';
15
16
  export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from './grants.js';
16
17
  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,10 +7,11 @@ 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
13
  export { ANONYMOUS_LIMITS, ANONYMOUS_WRITER, anonymousProblems, anonymousSpec, } from "./anonymous.js";
14
+ export { READERS_LIMIT, readersOf, readersProblems } from "./readers.js";
14
15
  export { DOCUMENT_LIMITS } from "./document-limits.js";
15
16
  export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from "./grants.js";
16
17
  export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, } from "./migrations.js";
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Record readers (P16): a record only the people it
4
+ * names may read.
5
+ *
6
+ * An app names one of the collection's own `string[]` fields, which holds
7
+ * user ids. When a record's list is empty, everyone who can read the
8
+ * instance reads it, as before; when it names anyone, nobody else finds it —
9
+ * not on a screen, not through a tool, not through the assistant, not on the
10
+ * co-edit socket. There is no admin past it: an app that wants its admins
11
+ * in puts them in the list. Brydio keeps the list in plain beside the record
12
+ * (`app_document.readers`), so every read can be narrowed before anything
13
+ * is decrypted. Mirrors Brydio's `apps/api/src/apps/manifest/readers.ts`;
14
+ * the two change together.
15
+ */
16
+ export declare const readersSchema: z.ZodString;
17
+ export type ReadersProblemCode = 'data_readers_field' | 'data_readers_anonymous';
18
+ /** The most people one record's list may name. */
19
+ export declare const READERS_LIMIT = 1000;
20
+ export declare function readersProblems(collection: string, declared: {
21
+ schema: Record<string, unknown>;
22
+ readers?: string;
23
+ anonymous?: unknown;
24
+ }): {
25
+ code: ReadersProblemCode;
26
+ collection: string;
27
+ field?: string;
28
+ message: string;
29
+ }[];
30
+ /** The plain list a record is kept with: its readers, or null for everyone who can read the instance. */
31
+ export declare function readersOf(field: string | undefined, body: Record<string, unknown>): string[] | null;
package/src/readers.js ADDED
@@ -0,0 +1,59 @@
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
+ }
package/src/schema.d.ts CHANGED
@@ -3,6 +3,7 @@ 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
5
  import { type AnonymousProblemCode, type AnonymousSpec } from './anonymous.js';
6
+ import { type ReadersProblemCode } from './readers.js';
6
7
  export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, type FieldKind, type FieldType, } from './field-types.js';
7
8
  /**
8
9
  * What an app adds to `.brydio/app.json` to be more than a bundle of servers
@@ -102,6 +103,7 @@ declare const extensionShape: {
102
103
  "project-sidebar": "project-sidebar";
103
104
  "workspace-sidebar": "workspace-sidebar";
104
105
  "public-page": "public-page";
106
+ "chat-card": "chat-card";
105
107
  }>;
106
108
  screen: z.ZodString;
107
109
  sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -162,6 +164,7 @@ declare const extensionShape: {
162
164
  group: z.ZodString;
163
165
  minimum: z.ZodOptional<z.ZodNumber>;
164
166
  }, z.core.$strict>>;
167
+ readers: z.ZodOptional<z.ZodString>;
165
168
  }, z.core.$strip>>>;
166
169
  tools: z.ZodOptional<z.ZodObject<{
167
170
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -240,7 +243,7 @@ export interface DataProblem {
240
243
  field?: string;
241
244
  message: string;
242
245
  }
243
- 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_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
246
+ export type DataProblemCode = FieldTypeInvalid['code'] | OpenProblemCode | ConfirmProblemCode | AnonymousProblemCode | ReadersProblemCode | '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';
244
247
  type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
245
248
  /**
246
249
  * Everything wrong with an app's additions that a type cannot say.
@@ -256,6 +259,17 @@ export interface DataProblemOptions {
256
259
  retired?: boolean;
257
260
  }
258
261
  export declare function dataProblems(additions: Additions, options?: DataProblemOptions): DataProblem[];
262
+ /**
263
+ * Another installed app's tool, as `grants.tools` names it (FO07): the name
264
+ * the assistant knows it by, `<slug>__<tool>`. Null for anything else.
265
+ * A handler calls it with `tools.call('<slug>__<tool>', input)`.
266
+ */
267
+ export declare function crossAppTool(name: string): {
268
+ app: string;
269
+ tool: string;
270
+ } | null;
271
+ /** An app's name as the front of its tools' assistant names: `issue-tracker` → `issue_tracker`. */
272
+ export declare const appToolPrefix: (name: string) => string;
259
273
  /** The additions alone, for code that has the rest of the manifest already. */
260
274
  export declare const manifestExtensionsSchema: z.ZodObject<{
261
275
  placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -267,6 +281,7 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
267
281
  "project-sidebar": "project-sidebar";
268
282
  "workspace-sidebar": "workspace-sidebar";
269
283
  "public-page": "public-page";
284
+ "chat-card": "chat-card";
270
285
  }>;
271
286
  screen: z.ZodString;
272
287
  sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -327,6 +342,7 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
327
342
  group: z.ZodString;
328
343
  minimum: z.ZodOptional<z.ZodNumber>;
329
344
  }, z.core.$strict>>;
345
+ readers: z.ZodOptional<z.ZodString>;
330
346
  }, z.core.$strip>>>;
331
347
  tools: z.ZodOptional<z.ZodObject<{
332
348
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -402,6 +418,7 @@ export declare const storedExtensionsSchema: z.ZodObject<{
402
418
  "project-sidebar": "project-sidebar";
403
419
  "workspace-sidebar": "workspace-sidebar";
404
420
  "public-page": "public-page";
421
+ "chat-card": "chat-card";
405
422
  }>;
406
423
  screen: z.ZodString;
407
424
  sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -462,6 +479,7 @@ export declare const storedExtensionsSchema: z.ZodObject<{
462
479
  group: z.ZodString;
463
480
  minimum: z.ZodOptional<z.ZodNumber>;
464
481
  }, z.core.$strict>>;
482
+ readers: z.ZodOptional<z.ZodString>;
465
483
  }, z.core.$strip>>>;
466
484
  tools: z.ZodOptional<z.ZodObject<{
467
485
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -572,6 +590,7 @@ export declare const appManifestSchema: z.ZodObject<{
572
590
  "project-sidebar": "project-sidebar";
573
591
  "workspace-sidebar": "workspace-sidebar";
574
592
  "public-page": "public-page";
593
+ "chat-card": "chat-card";
575
594
  }>;
576
595
  screen: z.ZodString;
577
596
  sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -632,6 +651,7 @@ export declare const appManifestSchema: z.ZodObject<{
632
651
  group: z.ZodString;
633
652
  minimum: z.ZodOptional<z.ZodNumber>;
634
653
  }, z.core.$strict>>;
654
+ readers: z.ZodOptional<z.ZodString>;
635
655
  }, z.core.$strip>>>;
636
656
  tools: z.ZodOptional<z.ZodObject<{
637
657
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -746,6 +766,7 @@ export declare const newAppManifestSchema: z.ZodObject<{
746
766
  "project-sidebar": "project-sidebar";
747
767
  "workspace-sidebar": "workspace-sidebar";
748
768
  "public-page": "public-page";
769
+ "chat-card": "chat-card";
749
770
  }>;
750
771
  screen: z.ZodString;
751
772
  sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -806,6 +827,7 @@ export declare const newAppManifestSchema: z.ZodObject<{
806
827
  group: z.ZodString;
807
828
  minimum: z.ZodOptional<z.ZodNumber>;
808
829
  }, z.core.$strict>>;
830
+ readers: z.ZodOptional<z.ZodString>;
809
831
  }, z.core.$strip>>>;
810
832
  tools: z.ZodOptional<z.ZodObject<{
811
833
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -898,6 +920,8 @@ export interface CollectionSpec {
898
920
  definesFieldsOf?: string;
899
921
  /** Answers kept with no writer, read only a whole group of at least `minimum` at a time (P13). */
900
922
  anonymous?: AnonymousSpec;
923
+ /** The string[] field naming who alone may read a record, when it names anyone (P16). */
924
+ readers?: string;
901
925
  }
902
926
  /**
903
927
  * The singular a collection's tools are named with: the manifest's, else the
package/src/schema.js CHANGED
@@ -5,6 +5,7 @@ import { migrationsSchema } from "./migrations.js";
5
5
  import { openSchemaProblems } from "./open-schema.js";
6
6
  import { confirmEmailProblems, confirmEmailSchema } from "./confirm-email.js";
7
7
  import { anonymousProblems, anonymousSchema, anonymousSpec } from "./anonymous.js";
8
+ import { readersProblems, readersSchema } from "./readers.js";
8
9
  export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, } from "./field-types.js";
9
10
  /**
10
11
  * What an app adds to `.brydio/app.json` to be more than a bundle of servers
@@ -72,8 +73,12 @@ const placementSchema = z.object({
72
73
  * `children` or `settings` (`placement_public_shape`), and it reaches only
73
74
  * collections marked `publicRead` or `publicSubmit` and tools marked
74
75
  * `public`.
76
+ * `chat-card`: a screen drawn inside a chat message the app posted with
77
+ * a handler's `chat.post` (FO03). Nobody places it by hand, so it has no
78
+ * `sizes`, `children` or `settings` (`placement_chat_card_shape`); its
79
+ * screen sees `placement.kind === 'chat-card'` and the card's `route`.
75
80
  */
76
- kind: z.enum(['project-tab', 'project-widget', 'project-sidebar', 'workspace-sidebar', 'home', 'public-page']),
81
+ kind: z.enum(['project-tab', 'project-widget', 'project-sidebar', 'workspace-sidebar', 'home', 'public-page', 'chat-card']),
77
82
  screen: z.string().min(1).max(FIELD_LIMITS.nameChars),
78
83
  /** For `home` and `project-widget` only, and required there: the sizes the screen draws well at. */
79
84
  sizes: z.array(z.enum(HOME_CARD_SIZES)).max(3).optional(),
@@ -148,6 +153,8 @@ const collectionSchema = z.object({
148
153
  * batch, a watch and a create naming its own writer.
149
154
  */
150
155
  anonymous: anonymousSchema.optional(),
156
+ /** One of its string[] fields: the user ids who alone may read a record, when it names any (P16). */
157
+ readers: readersSchema.optional(),
151
158
  });
152
159
  const screenSchema = z.object({
153
160
  /**
@@ -385,6 +392,10 @@ export function dataProblems(additions, options = {}) {
385
392
  for (const [collection, declared] of collections) {
386
393
  problems.push(...anonymousProblems(collection, declared));
387
394
  }
395
+ // A collection whose records name their readers names a list of user ids to keep them in (P16).
396
+ for (const [collection, declared] of collections) {
397
+ problems.push(...readersProblems(collection, declared));
398
+ }
388
399
  // What the app keeps must be what it asks to keep (A3-F06-S01): a
389
400
  // collection the grants leave out would be data a workspace never agreed to
390
401
  // hold, found only when the first write is refused.
@@ -454,6 +465,14 @@ export function dataProblems(additions, options = {}) {
454
465
  message: `The public page "${placement.screen}" can have no children or settings: it is one screen at its own address.`,
455
466
  });
456
467
  }
468
+ // A chat card is drawn at the message's own size, in a message nobody
469
+ // places by hand (FO03): nothing to size, configure or list.
470
+ if (placement.kind === 'chat-card' && (placement.sizes?.length || placement.children || placement.settings)) {
471
+ problems.push({
472
+ code: 'placement_chat_card_shape',
473
+ message: `The chat card "${placement.screen}" cannot have sizes, children or settings.`,
474
+ });
475
+ }
457
476
  }
458
477
  // A public page with nothing public could show a visitor nothing at all.
459
478
  if ((additions.placements ?? []).some(placement => placement.kind === 'public-page')) {
@@ -551,6 +570,20 @@ function customToolProblems(additions, options) {
551
570
  }));
552
571
  const seen = new Set();
553
572
  const granted = additions.grants?.tools ?? [];
573
+ // Another app's tool, asked for by its assistant name `<slug>__<tool>`
574
+ // (FO07): never an unknown custom tool of this app, but naming the app
575
+ // itself would be a way round its own grants.
576
+ const name = additions.name;
577
+ const own = typeof name === 'string' ? appToolPrefix(name) : null;
578
+ for (const grant of granted) {
579
+ const qualified = crossAppTool(grant);
580
+ if (qualified && own !== null && qualified.app === own) {
581
+ problems.push({
582
+ code: 'grant_tool_cross_app_self',
583
+ message: `${grant} names this app's own tool: ask for ${qualified.tool} instead.`,
584
+ });
585
+ }
586
+ }
554
587
  for (const tool of custom) {
555
588
  if (generated.has(tool.name) || seen.has(tool.name)) {
556
589
  problems.push({
@@ -587,6 +620,22 @@ function customToolProblems(additions, options) {
587
620
  }
588
621
  return problems;
589
622
  }
623
+ /**
624
+ * Another installed app's tool, as `grants.tools` names it (FO07): the name
625
+ * the assistant knows it by, `<slug>__<tool>`. Null for anything else.
626
+ * A handler calls it with `tools.call('<slug>__<tool>', input)`.
627
+ */
628
+ export function crossAppTool(name) {
629
+ const at = name.indexOf('__');
630
+ if (at <= 0 || at + 2 >= name.length)
631
+ return null;
632
+ return { app: name.slice(0, at), tool: name.slice(at + 2) };
633
+ }
634
+ /** An app's name as the front of its tools' assistant names: `issue-tracker` → `issue_tracker`. */
635
+ export const appToolPrefix = (name) => name
636
+ .replace(/[^A-Za-z0-9_]+/g, '_')
637
+ .replace(/_+/g, '_')
638
+ .replace(/^_+|_+$/g, '');
590
639
  /**
591
640
  * Each secret declared once, and asked for (ADR-A24): an app that declares
592
641
  * secrets without the `secrets` host grant could never read one.
@@ -619,7 +668,7 @@ const refuse = (additions, ctx, options = {}) => {
619
668
  ctx.addIssue({
620
669
  code: 'custom',
621
670
  message: problem.message,
622
- path: problem.code === 'grant_tool_missing'
671
+ path: problem.code === 'grant_tool_missing' || problem.code === 'grant_tool_cross_app_self'
623
672
  ? ['grants', 'tools']
624
673
  : problem.code === 'grant_secrets_missing'
625
674
  ? ['grants', 'host']
@@ -696,6 +745,7 @@ export function collectionsOf(manifest) {
696
745
  : {}),
697
746
  ...(owner ? { definesFieldsOf: owner.name } : {}),
698
747
  ...(declared.anonymous ? { anonymous: anonymousSpec(declared.anonymous) } : {}),
748
+ ...(declared.readers ? { readers: declared.readers } : {}),
699
749
  };
700
750
  });
701
751
  }