@skillcdn/console 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +120 -33
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +4 -2
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +38 -10
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +22 -15
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +10 -1
- package/dist/cli/plugin-check.d.ts.map +1 -1
- package/dist/cli/plugin-check.js +45 -4
- package/dist/cli/plugin-check.js.map +1 -1
- package/dist/cli/plugin-cli.d.ts +1 -1
- package/dist/cli/plugin-cli.d.ts.map +1 -1
- package/dist/cli/plugin-cli.js +80 -5
- package/dist/cli/plugin-cli.js.map +1 -1
- package/dist/cli/plugin-init.d.ts +19 -0
- package/dist/cli/plugin-init.d.ts.map +1 -1
- package/dist/cli/plugin-init.js +141 -3
- package/dist/cli/plugin-init.js.map +1 -1
- package/dist/client.d.ts +7 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +18 -5
- package/dist/client.js.map +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +3 -3
- package/dist/components/board.js.map +1 -1
- package/dist/components/comments.d.ts.map +1 -1
- package/dist/components/comments.js +1 -1
- package/dist/components/comments.js.map +1 -1
- package/dist/components/decision-form.d.ts +8 -1
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +57 -7
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +9 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +53 -7
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +7 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +2 -2
- package/dist/components/goals.js.map +1 -1
- package/dist/components/home.d.ts +35 -0
- package/dist/components/home.d.ts.map +1 -0
- package/dist/components/home.js +326 -0
- package/dist/components/home.js.map +1 -0
- package/dist/components/my-decisions.d.ts +2 -0
- package/dist/components/my-decisions.d.ts.map +1 -1
- package/dist/components/my-decisions.js +2 -2
- package/dist/components/my-decisions.js.map +1 -1
- package/dist/components/pack-connect.d.ts.map +1 -1
- package/dist/components/pack-connect.js +5 -4
- package/dist/components/pack-connect.js.map +1 -1
- package/dist/components/people.d.ts +2 -0
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +15 -2
- package/dist/components/people.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +3 -12
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +53 -20
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +151 -26
- package/dist/components/plugin-settings.js.map +1 -1
- package/dist/components/projects.d.ts +0 -8
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +1 -13
- package/dist/components/projects.js.map +1 -1
- package/dist/components/task-view.d.ts +2 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +2 -1
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +5 -2
- package/dist/components/ui.js.map +1 -1
- package/dist/connect-page.d.ts.map +1 -1
- package/dist/connect-page.js +5 -6
- package/dist/connect-page.js.map +1 -1
- package/dist/console.css +764 -192
- package/dist/console.d.ts +10 -3
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +49 -28
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +6 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +31 -7
- package/dist/data.js.map +1 -1
- package/dist/docs-page.d.ts.map +1 -1
- package/dist/docs-page.js +6 -5
- package/dist/docs-page.js.map +1 -1
- package/dist/home-tiles.d.ts +92 -0
- package/dist/home-tiles.d.ts.map +1 -0
- package/dist/home-tiles.js +294 -0
- package/dist/home-tiles.js.map +1 -0
- package/dist/i18n/en.d.ts +180 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +181 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/index.d.ts +4 -2
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +10 -11
- package/dist/i18n/index.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +176 -3
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +8 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +8 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/bridge.d.ts +44 -3
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +26 -0
- package/dist/plugin/bridge.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +7 -1
- package/dist/plugin/capabilities.d.ts.map +1 -1
- package/dist/plugin/capabilities.js +14 -1
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +24 -1
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +1 -0
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +1 -1
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +5 -2
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +5 -2
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +83 -6
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +111 -7
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +76 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +23 -2
- package/dist/plugin/rest-schemas.js.map +1 -1
- package/dist/plugin/sandbox.d.ts +4 -3
- package/dist/plugin/sandbox.d.ts.map +1 -1
- package/dist/plugin/sandbox.js +33 -6
- package/dist/plugin/sandbox.js.map +1 -1
- package/dist/plugin/semver.d.ts +26 -0
- package/dist/plugin/semver.d.ts.map +1 -0
- package/dist/plugin/semver.js +76 -0
- package/dist/plugin/semver.js.map +1 -0
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +2 -1
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +10 -1
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +34 -0
- package/dist/plugin/web.js.map +1 -1
- package/dist/plugin.d.ts +3 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3 -2
- package/dist/plugin.js.map +1 -1
- package/dist/rules.d.ts +2 -2
- package/dist/rules.js +3 -3
- package/dist/schemas.d.ts +574 -11
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +154 -25
- package/dist/schemas.js.map +1 -1
- package/dist/use-action.d.ts +8 -2
- package/dist/use-action.d.ts.map +1 -1
- package/dist/use-action.js +13 -6
- package/dist/use-action.js.map +1 -1
- package/dist/vocabulary.d.ts +3 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +3 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +4 -3
- package/docs/specs/plugins.md +49 -35
- package/docs/specs/rest.md +20 -18
- package/package.json +7 -7
- package/skills/building-a-plugin/SKILL.md +10 -9
- package/skills/planning-the-board/references/setting-up.md +2 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/src/api.ts +10 -0
- package/src/cli/cli.ts +162 -39
- package/src/cli/format.ts +54 -10
- package/src/cli/help.ts +23 -14
- package/src/cli/plugin-check.ts +68 -2
- package/src/cli/plugin-cli.ts +104 -6
- package/src/cli/plugin-init.ts +178 -3
- package/src/client.ts +36 -9
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +3 -3
- package/src/components/comments.tsx +1 -0
- package/src/components/decision-form.tsx +166 -29
- package/src/components/decision-list.tsx +116 -15
- package/src/components/event-feed.tsx +7 -1
- package/src/components/goals.tsx +4 -1
- package/src/components/home.tsx +615 -0
- package/src/components/my-decisions.tsx +4 -1
- package/src/components/pack-connect.tsx +5 -4
- package/src/components/people.tsx +136 -54
- package/src/components/plugin-frame.tsx +62 -63
- package/src/components/plugin-settings.tsx +428 -87
- package/src/components/projects.tsx +0 -55
- package/src/components/task-view.tsx +9 -1
- package/src/components/ui.tsx +6 -2
- package/src/connect-page.tsx +5 -6
- package/src/console.tsx +98 -42
- package/src/data.ts +41 -11
- package/src/docs-page.tsx +6 -5
- package/src/home-tiles.ts +438 -0
- package/src/i18n/en.ts +185 -0
- package/src/i18n/index.ts +17 -14
- package/src/i18n/ko.ts +181 -3
- package/src/index.ts +7 -5
- package/src/limits.ts +8 -0
- package/src/plugin/bridge.ts +77 -1
- package/src/plugin/capabilities.ts +17 -1
- package/src/plugin/context.ts +24 -2
- package/src/plugin/define.ts +4 -1
- package/src/plugin/limits.ts +5 -2
- package/src/plugin/manifest.ts +154 -10
- package/src/plugin/rest-schemas.ts +27 -1
- package/src/plugin/sandbox.ts +36 -8
- package/src/plugin/semver.ts +89 -0
- package/src/plugin/testing.ts +2 -1
- package/src/plugin/web.ts +49 -0
- package/src/plugin.ts +18 -0
- package/src/rules.ts +3 -3
- package/src/schemas.ts +190 -28
- package/src/styles/console.css +764 -192
- package/src/use-action.ts +16 -7
- package/src/vocabulary.ts +4 -0
- package/template/plugin/console-plugin.json +3 -1
- package/template/plugin/package.json +3 -3
package/src/plugin/bridge.ts
CHANGED
|
@@ -3,8 +3,18 @@
|
|
|
3
3
|
// frame the console's origin alone. The shapes and their guards, with no DOM in them, so that the
|
|
4
4
|
// page's side and the frame's side read the same words and the tests need no browser.
|
|
5
5
|
|
|
6
|
+
import { WEEK_STARTS, type WeekStart } from "../vocabulary.js";
|
|
7
|
+
|
|
6
8
|
export const PLUGIN_BRIDGE_VERSION = 1;
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The sizes a widget comes in (ADR-0061): columns and rows of the front page's grid of square
|
|
12
|
+
* cells, `small` 2×2, `medium` 4×2, `large` 4×4, `wide` 8×2, `full` 8×4. Here, so that a bundle
|
|
13
|
+
* learns them without the manifest's schema.
|
|
14
|
+
*/
|
|
15
|
+
export const PLUGIN_WIDGET_SIZE_NAMES = ["small", "medium", "large", "wide", "full"] as const;
|
|
16
|
+
export type PluginWidgetSizeName = (typeof PLUGIN_WIDGET_SIZE_NAMES)[number];
|
|
17
|
+
|
|
8
18
|
/** What the frame may ask the page to perform, each under `/api/v1/plugins/<key>/` and nothing else. */
|
|
9
19
|
export const BRIDGE_METHODS = [
|
|
10
20
|
"command",
|
|
@@ -14,6 +24,7 @@ export const BRIDGE_METHODS = [
|
|
|
14
24
|
"records.versions",
|
|
15
25
|
"files.get",
|
|
16
26
|
"files.put",
|
|
27
|
+
"people.picture",
|
|
17
28
|
] as const;
|
|
18
29
|
|
|
19
30
|
export type BridgeMethod = (typeof BRIDGE_METHODS)[number];
|
|
@@ -31,6 +42,17 @@ export interface BridgePerson {
|
|
|
31
42
|
readonly admin: boolean;
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
/** The workspace as a page lays itself out for it: what it is called, the languages it writes in, its clock and the day its week begins on. */
|
|
46
|
+
export interface BridgeWorkspace {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly languages: readonly string[];
|
|
49
|
+
readonly timeZone: string;
|
|
50
|
+
/** The workspace's setting (ADR-0035): `auto` leaves the week to the language of the pages. */
|
|
51
|
+
readonly weekStart: WeekStart;
|
|
52
|
+
/** The day a week begins on with the setting applied, for a calendar to start from: 1 for Monday, 7 for Sunday. */
|
|
53
|
+
readonly firstDay: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
34
56
|
/** What the page tells the frame once it is ready: who looks, at what, in which words and colours. */
|
|
35
57
|
export interface BridgeInit {
|
|
36
58
|
readonly v: 1;
|
|
@@ -38,8 +60,12 @@ export interface BridgeInit {
|
|
|
38
60
|
readonly plugin: { readonly key: string; readonly version: string };
|
|
39
61
|
readonly page?: string;
|
|
40
62
|
readonly widget?: string;
|
|
63
|
+
/** The size the widget's frame has (ADR-0059), one of the sizes the manifest declared for it; none for a page. */
|
|
64
|
+
readonly size?: PluginWidgetSizeName;
|
|
41
65
|
readonly project: { readonly key: string; readonly name: string } | null;
|
|
42
66
|
readonly person: BridgePerson;
|
|
67
|
+
/** The workspace; none from a console older than this. */
|
|
68
|
+
readonly workspace?: BridgeWorkspace;
|
|
43
69
|
readonly roles: readonly string[];
|
|
44
70
|
/** What follows the page's key in the address. */
|
|
45
71
|
readonly path: string;
|
|
@@ -82,7 +108,20 @@ export interface BridgePath {
|
|
|
82
108
|
readonly path: string;
|
|
83
109
|
}
|
|
84
110
|
|
|
85
|
-
|
|
111
|
+
/** The person gave the widget another size (ADR-0059): the frame lays itself out for it. */
|
|
112
|
+
export interface BridgeSize {
|
|
113
|
+
readonly v: 1;
|
|
114
|
+
readonly t: "size";
|
|
115
|
+
readonly size: PluginWidgetSizeName;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type BridgeToFrame =
|
|
119
|
+
| BridgeInit
|
|
120
|
+
| BridgeResult
|
|
121
|
+
| BridgeEvent
|
|
122
|
+
| BridgeTheme
|
|
123
|
+
| BridgePath
|
|
124
|
+
| BridgeSize;
|
|
86
125
|
|
|
87
126
|
export interface BridgeReady {
|
|
88
127
|
readonly v: 1;
|
|
@@ -127,8 +166,18 @@ export interface BridgeSubscribe {
|
|
|
127
166
|
readonly kinds: readonly BridgeEventKind[];
|
|
128
167
|
}
|
|
129
168
|
|
|
169
|
+
/**
|
|
170
|
+
* The frame has nothing to show: the plugin is off, or the page is not one of its own. The console
|
|
171
|
+
* serves such a frame a document with no words, and the console's page says so in its own.
|
|
172
|
+
*/
|
|
173
|
+
export interface BridgeUnavailable {
|
|
174
|
+
readonly v: 1;
|
|
175
|
+
readonly t: "unavailable";
|
|
176
|
+
}
|
|
177
|
+
|
|
130
178
|
export type BridgeToPage =
|
|
131
179
|
| BridgeReady
|
|
180
|
+
| BridgeUnavailable
|
|
132
181
|
| BridgeCall
|
|
133
182
|
| BridgeNavigate
|
|
134
183
|
| BridgeResize
|
|
@@ -177,6 +226,21 @@ function isTheme(value: unknown): value is Record<string, string> {
|
|
|
177
226
|
);
|
|
178
227
|
}
|
|
179
228
|
|
|
229
|
+
function isWorkspace(value: unknown): boolean {
|
|
230
|
+
return (
|
|
231
|
+
isRecord(value) &&
|
|
232
|
+
typeof value.name === "string" &&
|
|
233
|
+
Array.isArray(value.languages) &&
|
|
234
|
+
value.languages.every((language) => typeof language === "string") &&
|
|
235
|
+
isLine(value.timeZone, 100) &&
|
|
236
|
+
(WEEK_STARTS as readonly unknown[]).includes(value.weekStart) &&
|
|
237
|
+
typeof value.firstDay === "number" &&
|
|
238
|
+
Number.isInteger(value.firstDay) &&
|
|
239
|
+
value.firstDay >= 1 &&
|
|
240
|
+
value.firstDay <= 7
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
180
244
|
function isEnvelope(value: unknown): value is Record<string, unknown> & { t: string } {
|
|
181
245
|
return isRecord(value) && value.v === PLUGIN_BRIDGE_VERSION && typeof value.t === "string";
|
|
182
246
|
}
|
|
@@ -194,6 +258,8 @@ export function isBridgeToFrame(value: unknown): value is BridgeToFrame {
|
|
|
194
258
|
isLine(value.plugin.version, 64) &&
|
|
195
259
|
(value.page === undefined || isLine(value.page, 32)) &&
|
|
196
260
|
(value.widget === undefined || isLine(value.widget, 32)) &&
|
|
261
|
+
(value.size === undefined ||
|
|
262
|
+
(PLUGIN_WIDGET_SIZE_NAMES as readonly unknown[]).includes(value.size)) &&
|
|
197
263
|
(value.project === null ||
|
|
198
264
|
(isRecord(value.project) &&
|
|
199
265
|
isLine(value.project.key, 64) &&
|
|
@@ -204,6 +270,7 @@ export function isBridgeToFrame(value: unknown): value is BridgeToFrame {
|
|
|
204
270
|
(value.person.name === null || typeof value.person.name === "string") &&
|
|
205
271
|
typeof value.person.language === "string" &&
|
|
206
272
|
typeof value.person.admin === "boolean" &&
|
|
273
|
+
(value.workspace === undefined || isWorkspace(value.workspace)) &&
|
|
207
274
|
Array.isArray(value.roles) &&
|
|
208
275
|
value.roles.every((role) => typeof role === "string") &&
|
|
209
276
|
typeof value.path === "string" &&
|
|
@@ -229,6 +296,8 @@ export function isBridgeToFrame(value: unknown): value is BridgeToFrame {
|
|
|
229
296
|
return isTheme(value.theme);
|
|
230
297
|
case "path":
|
|
231
298
|
return typeof value.path === "string" && value.path.length <= MAX_BRIDGE_PATH_LENGTH;
|
|
299
|
+
case "size":
|
|
300
|
+
return (PLUGIN_WIDGET_SIZE_NAMES as readonly unknown[]).includes(value.size);
|
|
232
301
|
default:
|
|
233
302
|
return false;
|
|
234
303
|
}
|
|
@@ -241,6 +310,7 @@ export function isBridgeToPage(value: unknown): value is BridgeToPage {
|
|
|
241
310
|
}
|
|
242
311
|
switch (value.t) {
|
|
243
312
|
case "ready":
|
|
313
|
+
case "unavailable":
|
|
244
314
|
return true;
|
|
245
315
|
case "call":
|
|
246
316
|
return (
|
|
@@ -307,6 +377,12 @@ export interface BridgeFilesPutParams {
|
|
|
307
377
|
}
|
|
308
378
|
|
|
309
379
|
/** What `files.get` answers: the bytes, transferred, with the file's name and type. */
|
|
380
|
+
/** A person's picture, as the page hands it to the frame (ADR-0063). */
|
|
381
|
+
export interface BridgePicture {
|
|
382
|
+
readonly type: string;
|
|
383
|
+
readonly bytes: ArrayBuffer;
|
|
384
|
+
}
|
|
385
|
+
|
|
310
386
|
export interface BridgeFile {
|
|
311
387
|
readonly name: string;
|
|
312
388
|
readonly type: string;
|
|
@@ -19,10 +19,18 @@ export const PLUGIN_CAPABILITIES = [
|
|
|
19
19
|
"jobs",
|
|
20
20
|
"endpoints",
|
|
21
21
|
"storage:sql",
|
|
22
|
+
"outbound:webrtc",
|
|
22
23
|
] as const;
|
|
23
24
|
|
|
24
25
|
export type PluginCapability = (typeof PLUGIN_CAPABILITIES)[number];
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* WebRTC on a plugin's pages (ADR-0066). It stands with `outbound:<host>` as a way to the internet,
|
|
29
|
+
* and it is no host: a connection goes from a person's browser to any address a page names, which
|
|
30
|
+
* no host could bound, so that consent to a host does not open it, and it opens no host.
|
|
31
|
+
*/
|
|
32
|
+
export const OUTBOUND_WEBRTC: PluginCapability = "outbound:webrtc";
|
|
33
|
+
|
|
26
34
|
/** The capabilities that carry a parameter: a host to reach, a plugin to call. */
|
|
27
35
|
export const PLUGIN_CAPABILITY_PREFIXES = ["outbound:", "plugins:"] as const;
|
|
28
36
|
|
|
@@ -97,6 +105,10 @@ export const CAPABILITY_WORDS: Readonly<Record<PluginCapability, CapabilityWords
|
|
|
97
105
|
opens: "a schema and a login role of its own in the console's database",
|
|
98
106
|
reach: "statements up to the timeout, within its schema",
|
|
99
107
|
},
|
|
108
|
+
"outbound:webrtc": {
|
|
109
|
+
opens: "WebRTC on its pages: connections from each person's browser to any address",
|
|
110
|
+
reach: "what its pages are shown, sent anywhere from the person's browser; their address",
|
|
111
|
+
},
|
|
100
112
|
};
|
|
101
113
|
|
|
102
114
|
/** True for a capability the catalogue knows, parameter included. */
|
|
@@ -137,7 +149,11 @@ export function capabilityWords(capability: string): CapabilityWords | undefined
|
|
|
137
149
|
|
|
138
150
|
/** True when the request's host is within what `outbound:<host>` names: the host itself, or a subdomain where the capability starts with `*.`. */
|
|
139
151
|
export function outboundAllows(capability: string, host: string): boolean {
|
|
140
|
-
if (
|
|
152
|
+
if (
|
|
153
|
+
!capability.startsWith("outbound:") ||
|
|
154
|
+
capability === OUTBOUND_WEBRTC ||
|
|
155
|
+
!isPluginCapability(capability)
|
|
156
|
+
) {
|
|
141
157
|
return false;
|
|
142
158
|
}
|
|
143
159
|
const named = capability.slice("outbound:".length);
|
package/src/plugin/context.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
RestTaskInput,
|
|
17
17
|
RestTaskPatch,
|
|
18
18
|
} from "../schemas.js";
|
|
19
|
+
import type { WeekStart } from "../vocabulary.js";
|
|
19
20
|
import type { PluginFileHandle } from "./args.js";
|
|
20
21
|
|
|
21
22
|
/** A schema as the Standard Schema interface spells one: what every build of zod implements, so that a plugin's own copy validates its records. */
|
|
@@ -199,6 +200,11 @@ export interface PluginInfo {
|
|
|
199
200
|
readonly dataVersion: number;
|
|
200
201
|
/** The settings as the administrator set them, by key, without the secrets. */
|
|
201
202
|
readonly settings: Readonly<Record<string, unknown>>;
|
|
203
|
+
/**
|
|
204
|
+
* The capabilities the plugin holds: those it requires, and the optional ones an administrator
|
|
205
|
+
* turned on (ADR-0060), so that it offers what it can do. One it does not hold is refused.
|
|
206
|
+
*/
|
|
207
|
+
readonly capabilities: readonly string[];
|
|
202
208
|
/** The board project, or none. */
|
|
203
209
|
readonly board: { readonly id: string; readonly key: string } | null;
|
|
204
210
|
/** The projects the plugin is on: all, or their keys. */
|
|
@@ -245,6 +251,8 @@ export interface PluginWorkspace {
|
|
|
245
251
|
readonly name: string;
|
|
246
252
|
readonly languages: readonly string[];
|
|
247
253
|
readonly timeZone: string;
|
|
254
|
+
/** The day the week begins on (ADR-0035): `auto` leaves it to the language of whoever reads. */
|
|
255
|
+
readonly weekStart: WeekStart;
|
|
248
256
|
readonly categories: readonly {
|
|
249
257
|
readonly key: string;
|
|
250
258
|
readonly name: string;
|
|
@@ -253,11 +261,19 @@ export interface PluginWorkspace {
|
|
|
253
261
|
}
|
|
254
262
|
|
|
255
263
|
export interface DecisionAsk {
|
|
264
|
+
/** In the language of whoever is to answer, as `people.get` says it (ADR-0065). */
|
|
256
265
|
readonly question: string;
|
|
257
266
|
readonly body?: string;
|
|
258
|
-
|
|
259
|
-
|
|
267
|
+
/**
|
|
268
|
+
* The options, each a label and optionally a key (spelled as a plugin's key is, each once),
|
|
269
|
+
* which the answered event carries back as `optionKey`, so that the plugin reads the answer
|
|
270
|
+
* without comparing labels.
|
|
271
|
+
*/
|
|
272
|
+
readonly options: readonly { readonly label: string; readonly key?: string }[];
|
|
273
|
+
/** Who is to answer: a person who may work in the project; the administrator who enabled the plugin when left out. */
|
|
260
274
|
readonly deciderId?: string;
|
|
275
|
+
/** Several to answer (ADR-0057), in place of `deciderId`: any of them may, and the first answer ends it; up to MAX_DECIDERS. */
|
|
276
|
+
readonly deciderIds?: readonly string[];
|
|
261
277
|
readonly taskId?: string;
|
|
262
278
|
/** The record the decision is for, by which the plugin maps the answer back. */
|
|
263
279
|
readonly about: { readonly collection: string; readonly id: string };
|
|
@@ -269,6 +285,12 @@ export interface PluginConsole {
|
|
|
269
285
|
people: {
|
|
270
286
|
list(options?: { readonly q?: string }): Promise<readonly RestPerson[]>;
|
|
271
287
|
get(id: string): Promise<RestPerson | null>;
|
|
288
|
+
/**
|
|
289
|
+
* The active people a role of the plugin was granted to, by name, through a project or with
|
|
290
|
+
* everyone, and everyone for `defaultRole` (ADR-0063). An administrator holds every role, and
|
|
291
|
+
* is listed only where they were granted it.
|
|
292
|
+
*/
|
|
293
|
+
granted(role: string): Promise<readonly RestPerson[]>;
|
|
272
294
|
};
|
|
273
295
|
projects: {
|
|
274
296
|
list(): Promise<readonly RestProject[]>;
|
package/src/plugin/define.ts
CHANGED
|
@@ -714,6 +714,7 @@ export function descriptionProblems(
|
|
|
714
714
|
manifest: {
|
|
715
715
|
readonly roles: Readonly<Record<string, unknown>>;
|
|
716
716
|
readonly capabilities: readonly string[];
|
|
717
|
+
readonly optionalCapabilities?: readonly string[] | undefined;
|
|
717
718
|
readonly settings: readonly { readonly key: string; readonly type: string }[];
|
|
718
719
|
readonly dataVersion: number;
|
|
719
720
|
},
|
|
@@ -729,7 +730,9 @@ export function descriptionProblems(
|
|
|
729
730
|
}
|
|
730
731
|
};
|
|
731
732
|
const needs = (where: string, capability: string) => {
|
|
732
|
-
if (
|
|
733
|
+
if (
|
|
734
|
+
![...manifest.capabilities, ...(manifest.optionalCapabilities ?? [])].includes(capability)
|
|
735
|
+
) {
|
|
733
736
|
problems.push(
|
|
734
737
|
`${where} needs the capability ${capability}, which the manifest does not declare`,
|
|
735
738
|
);
|
package/src/plugin/limits.ts
CHANGED
|
@@ -8,11 +8,14 @@ export const MAX_PLUGIN_DESCRIPTION_LENGTH = 200;
|
|
|
8
8
|
export const MAX_PLUGIN_AGENT_NOTE_LENGTH = 200;
|
|
9
9
|
/** A plugin's key, a role's, a page's, a widget's, a setting's, a collection's, a command's: `^[a-z][a-z0-9-]{1,31}$`. */
|
|
10
10
|
export const MAX_PLUGIN_KEY_LENGTH = 32;
|
|
11
|
-
/** How many roles, settings, pages and widgets one manifest declares. */
|
|
11
|
+
/** How many roles, settings, groups of settings, pages and widgets one manifest declares. */
|
|
12
12
|
export const MAX_PLUGIN_ROLES = 10;
|
|
13
13
|
export const MAX_PLUGIN_SETTINGS = 50;
|
|
14
|
+
export const MAX_PLUGIN_SETTING_GROUPS = 10;
|
|
14
15
|
export const MAX_PLUGIN_PAGES = 12;
|
|
15
16
|
export const MAX_PLUGIN_WIDGETS = 4;
|
|
17
|
+
/** How many capabilities a plugin asks, the optional ones counted. */
|
|
18
|
+
export const MAX_PLUGIN_CAPABILITIES = 50;
|
|
16
19
|
/** How many collections, commands and queries together, endpoints, and indexes of one collection a server module defines. */
|
|
17
20
|
export const MAX_PLUGIN_COLLECTIONS = 50;
|
|
18
21
|
export const MAX_PLUGIN_OPERATIONS = 100;
|
|
@@ -64,7 +67,7 @@ export const MAX_PLUGIN_SQL_ROWS = 10_000;
|
|
|
64
67
|
export const PLUGIN_SQL_CONNECTIONS = 4;
|
|
65
68
|
/** The memory a plugin's thread may hold, in megabytes, where the deployment says nothing. */
|
|
66
69
|
export const DEFAULT_PLUGIN_MEMORY_MB = 256;
|
|
67
|
-
/** How tall a widget's frame
|
|
70
|
+
/** How tall a widget's frame grew, in pixels, before a widget had sizes of a family (ADR-0059); kept for the readers of before, read by nothing. */
|
|
68
71
|
export const MAX_PLUGIN_WIDGET_HEIGHT = 480;
|
|
69
72
|
/** How many retries a hook gets before its event is skipped with a `plugin.failed` event. */
|
|
70
73
|
export const PLUGIN_HOOK_RETRIES = 5;
|
package/src/plugin/manifest.ts
CHANGED
|
@@ -8,14 +8,17 @@ import {
|
|
|
8
8
|
MAX_PROJECT_NAME_LENGTH,
|
|
9
9
|
} from "../limits.js";
|
|
10
10
|
import { hasForbiddenCodePoint } from "../text.js";
|
|
11
|
+
import { PLUGIN_WIDGET_SIZE_NAMES, type PluginWidgetSizeName } from "./bridge.js";
|
|
11
12
|
import { isPluginCapability } from "./capabilities.js";
|
|
12
13
|
import {
|
|
13
14
|
MAX_PLUGIN_AGENT_NOTE_LENGTH,
|
|
15
|
+
MAX_PLUGIN_CAPABILITIES,
|
|
14
16
|
MAX_PLUGIN_DESCRIPTION_LENGTH,
|
|
15
17
|
MAX_PLUGIN_NAME_LENGTH,
|
|
16
18
|
MAX_PLUGIN_OPTIONS,
|
|
17
19
|
MAX_PLUGIN_PAGES,
|
|
18
20
|
MAX_PLUGIN_ROLES,
|
|
21
|
+
MAX_PLUGIN_SETTING_GROUPS,
|
|
19
22
|
MAX_PLUGIN_SETTINGS,
|
|
20
23
|
MAX_PLUGIN_WIDGETS,
|
|
21
24
|
} from "./limits.js";
|
|
@@ -135,6 +138,7 @@ export const PLUGIN_SETTING_TYPES = [
|
|
|
135
138
|
"boolean",
|
|
136
139
|
"enum",
|
|
137
140
|
"day",
|
|
141
|
+
"time",
|
|
138
142
|
"person",
|
|
139
143
|
"project",
|
|
140
144
|
"secret",
|
|
@@ -149,14 +153,68 @@ export type PluginPageLevel = (typeof PLUGIN_PAGE_LEVELS)[number];
|
|
|
149
153
|
export const PLUGIN_WIDGET_PLACES = ["home:top", "home:bottom"] as const;
|
|
150
154
|
export type PluginWidgetPlace = (typeof PLUGIN_WIDGET_PLACES)[number];
|
|
151
155
|
|
|
156
|
+
/** The sizes of before (ADR-0054), each read as one of the family (ADR-0059): a third as small, a half as medium, the width as wide. */
|
|
152
157
|
export const PLUGIN_WIDGET_SIZES = ["third", "half", "full"] as const;
|
|
153
158
|
export type PluginWidgetSize = (typeof PLUGIN_WIDGET_SIZES)[number];
|
|
154
159
|
|
|
160
|
+
export { PLUGIN_WIDGET_SIZE_NAMES, type PluginWidgetSizeName };
|
|
161
|
+
|
|
162
|
+
const LEGACY_WIDGET_SIZES: Readonly<Record<PluginWidgetSize, PluginWidgetSizeName>> = {
|
|
163
|
+
third: "small",
|
|
164
|
+
half: "medium",
|
|
165
|
+
full: "wide",
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** The columns and the rows of the front page's grid of square cells each size takes (ADR-0061). */
|
|
169
|
+
export const PLUGIN_WIDGET_SIZE_CELLS: Readonly<
|
|
170
|
+
Record<PluginWidgetSizeName, { readonly columns: number; readonly rows: number }>
|
|
171
|
+
> = {
|
|
172
|
+
small: { columns: 2, rows: 2 },
|
|
173
|
+
medium: { columns: 4, rows: 2 },
|
|
174
|
+
large: { columns: 4, rows: 4 },
|
|
175
|
+
wide: { columns: 8, rows: 2 },
|
|
176
|
+
full: { columns: 8, rows: 4 },
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** The most cells across a phone's grid has; a widget comes in a size that fits it (ADR-0062). */
|
|
180
|
+
const PLUGIN_WIDGET_PHONE_COLUMNS = 4;
|
|
181
|
+
|
|
182
|
+
/** Whether a size fits a phone's grid. */
|
|
183
|
+
const fitsPhone = (size: PluginWidgetSizeName): boolean =>
|
|
184
|
+
PLUGIN_WIDGET_SIZE_CELLS[size].columns <= PLUGIN_WIDGET_PHONE_COLUMNS;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The sizes a widget comes in and the one it starts at: `sizes` and `defaultSize` where the
|
|
188
|
+
* manifest declares them, else the one size of before read as one of the family, with a size a
|
|
189
|
+
* phone can hold beside it where that one is too wide.
|
|
190
|
+
*/
|
|
191
|
+
export function widgetSizesOf(widget: {
|
|
192
|
+
readonly size?: PluginWidgetSize | undefined;
|
|
193
|
+
readonly sizes?: readonly PluginWidgetSizeName[] | undefined;
|
|
194
|
+
readonly defaultSize?: PluginWidgetSizeName | undefined;
|
|
195
|
+
}): {
|
|
196
|
+
readonly sizes: readonly PluginWidgetSizeName[];
|
|
197
|
+
readonly defaultSize: PluginWidgetSizeName;
|
|
198
|
+
} {
|
|
199
|
+
if (widget.sizes !== undefined && widget.sizes.length > 0) {
|
|
200
|
+
return { sizes: widget.sizes, defaultSize: widget.defaultSize ?? widget.sizes[0] ?? "small" };
|
|
201
|
+
}
|
|
202
|
+
const one = LEGACY_WIDGET_SIZES[widget.size ?? "third"];
|
|
203
|
+
return { sizes: fitsPhone(one) ? [one] : ["medium", one], defaultSize: one };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The size of before nearest to one of the family, for a reader of before. */
|
|
207
|
+
export function legacyWidgetSize(size: PluginWidgetSizeName): PluginWidgetSize {
|
|
208
|
+
return size === "small" ? "third" : size === "medium" || size === "large" ? "half" : "full";
|
|
209
|
+
}
|
|
210
|
+
|
|
155
211
|
export const PLUGIN_BOARD_NEEDS = ["required", "optional", "none"] as const;
|
|
156
212
|
export type PluginBoardNeeds = (typeof PLUGIN_BOARD_NEEDS)[number];
|
|
157
213
|
|
|
158
214
|
const LANGUAGE_TAG = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
159
215
|
const DAY = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
|
|
216
|
+
/** A time of day as `HH:MM`, read in the workspace's time zone. */
|
|
217
|
+
export const TIME_OF_DAY = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
|
160
218
|
/** A path within the package: relative, forward slashes, never climbing out. */
|
|
161
219
|
const PACKAGE_PATH = /^(?!\/)(?!\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
|
|
162
220
|
/** A semver range as npm spells one, loosely: the console compares versions, the registry resolves. */
|
|
@@ -230,6 +288,8 @@ const pluginSettingSchema = z
|
|
|
230
288
|
z.array(settingOption).check(z.minLength(1), z.maxLength(MAX_PLUGIN_OPTIONS)),
|
|
231
289
|
),
|
|
232
290
|
of: z.optional(settingItemType),
|
|
291
|
+
/** The group of `settingGroups` the setting is shown under; left out, above every group. */
|
|
292
|
+
group: z.optional(key),
|
|
233
293
|
})
|
|
234
294
|
.check(
|
|
235
295
|
z.superRefine((setting, ctx) => {
|
|
@@ -241,6 +301,15 @@ const pluginSettingSchema = z
|
|
|
241
301
|
|
|
242
302
|
export type PluginSetting = z.infer<typeof pluginSettingSchema>;
|
|
243
303
|
|
|
304
|
+
/** A group the settings form is divided into: a panel with a name and, where given, a word under it. */
|
|
305
|
+
const pluginSettingGroupSchema = z.strictObject({
|
|
306
|
+
key,
|
|
307
|
+
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
308
|
+
description: z.optional(localized(MAX_PLUGIN_DESCRIPTION_LENGTH)),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
export type PluginSettingGroup = z.infer<typeof pluginSettingGroupSchema>;
|
|
312
|
+
|
|
244
313
|
/** True when `value` is what a setting of `type` holds, `options` being the values of an `enum`. */
|
|
245
314
|
function fitsSettingType(
|
|
246
315
|
type: Exclude<PluginSettingType, "secret" | "list">,
|
|
@@ -259,6 +328,8 @@ function fitsSettingType(
|
|
|
259
328
|
return typeof value === "string" && (options ?? []).includes(value);
|
|
260
329
|
case "day":
|
|
261
330
|
return typeof value === "string" && DAY.test(value);
|
|
331
|
+
case "time":
|
|
332
|
+
return typeof value === "string" && TIME_OF_DAY.test(value);
|
|
262
333
|
case "person":
|
|
263
334
|
case "project":
|
|
264
335
|
return false;
|
|
@@ -334,16 +405,60 @@ const pluginPageSchema = z.strictObject({
|
|
|
334
405
|
|
|
335
406
|
export type PluginPage = z.infer<typeof pluginPageSchema>;
|
|
336
407
|
|
|
337
|
-
const pluginWidgetSchema = z
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
408
|
+
const pluginWidgetSchema = z
|
|
409
|
+
.strictObject({
|
|
410
|
+
key,
|
|
411
|
+
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
412
|
+
place: z.enum(PLUGIN_WIDGET_PLACES),
|
|
413
|
+
/** The one size of before; `sizes` says it better. */
|
|
414
|
+
size: z.optional(z.enum(PLUGIN_WIDGET_SIZES)),
|
|
415
|
+
/** The sizes the widget comes in (ADR-0059), each once; the person picks among them. */
|
|
416
|
+
sizes: z.optional(
|
|
417
|
+
z.array(z.enum(PLUGIN_WIDGET_SIZE_NAMES)).check(z.minLength(1), z.maxLength(5)),
|
|
418
|
+
),
|
|
419
|
+
/** The size it starts at, one of `sizes`; the first of them when left out. */
|
|
420
|
+
defaultSize: z.optional(z.enum(PLUGIN_WIDGET_SIZE_NAMES)),
|
|
421
|
+
roles: z.optional(roleList),
|
|
422
|
+
})
|
|
423
|
+
.check(
|
|
424
|
+
z.superRefine((widget, ctx) => {
|
|
425
|
+
if (widget.size === undefined && widget.sizes === undefined) {
|
|
426
|
+
ctx.addIssue({
|
|
427
|
+
code: "custom",
|
|
428
|
+
message: "needs sizes, the sizes it comes in",
|
|
429
|
+
input: widget,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
if (widget.sizes !== undefined && new Set(widget.sizes).size !== widget.sizes.length) {
|
|
433
|
+
ctx.addIssue({ code: "custom", message: "sizes names a size twice", input: widget });
|
|
434
|
+
}
|
|
435
|
+
if (widget.sizes !== undefined && !widget.sizes.some(fitsPhone)) {
|
|
436
|
+
ctx.addIssue({
|
|
437
|
+
code: "custom",
|
|
438
|
+
message: "sizes needs small, medium or large: a phone's grid is four cells across",
|
|
439
|
+
input: widget,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
if (widget.defaultSize !== undefined && !(widget.sizes ?? []).includes(widget.defaultSize)) {
|
|
443
|
+
ctx.addIssue({
|
|
444
|
+
code: "custom",
|
|
445
|
+
message: "defaultSize is not one of sizes",
|
|
446
|
+
input: widget,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
344
451
|
|
|
345
452
|
export type PluginWidget = z.infer<typeof pluginWidgetSchema>;
|
|
346
453
|
|
|
454
|
+
/** Every capability a plugin asks: those it needs, then those it can go without (ADR-0060). */
|
|
455
|
+
export function capabilitiesAsked(manifest: {
|
|
456
|
+
readonly capabilities: readonly string[];
|
|
457
|
+
readonly optionalCapabilities?: readonly string[] | undefined;
|
|
458
|
+
}): string[] {
|
|
459
|
+
return [...manifest.capabilities, ...(manifest.optionalCapabilities ?? [])];
|
|
460
|
+
}
|
|
461
|
+
|
|
347
462
|
const pluginMenuSchema = z
|
|
348
463
|
.strictObject({
|
|
349
464
|
group: key,
|
|
@@ -409,12 +524,19 @@ export const pluginManifestSchema = z
|
|
|
409
524
|
description: localized(MAX_PLUGIN_DESCRIPTION_LENGTH),
|
|
410
525
|
agentNote: z.optional(localized(MAX_PLUGIN_AGENT_NOTE_LENGTH)),
|
|
411
526
|
pluginApi: z.int().check(z.positive()),
|
|
527
|
+
/** The consoles the plugin runs on, as a semver range of the console's version; left out, any console that serves `pluginApi`. */
|
|
528
|
+
console: z.optional(z.string().check(z.regex(SEMVER_RANGE, "is a semver range"))),
|
|
412
529
|
server: packagePath,
|
|
413
530
|
web: z.optional(packagePath),
|
|
414
531
|
skills: z.optional(packagePath),
|
|
415
532
|
migrations: z.optional(packagePath),
|
|
416
533
|
capabilities: z._default(
|
|
417
|
-
z.array(z.string().check(z.maxLength(300))).check(z.maxLength(
|
|
534
|
+
z.array(z.string().check(z.maxLength(300))).check(z.maxLength(MAX_PLUGIN_CAPABILITIES)),
|
|
535
|
+
[],
|
|
536
|
+
),
|
|
537
|
+
/** What the plugin can go without (ADR-0060): an administrator turns each on, or leaves it off. */
|
|
538
|
+
optionalCapabilities: z._default(
|
|
539
|
+
z.array(z.string().check(z.maxLength(300))).check(z.maxLength(MAX_PLUGIN_CAPABILITIES)),
|
|
418
540
|
[],
|
|
419
541
|
),
|
|
420
542
|
roles: z._default(z.record(key, pluginRoleSchema), {}),
|
|
@@ -425,6 +547,10 @@ export const pluginManifestSchema = z
|
|
|
425
547
|
),
|
|
426
548
|
board: z._default(pluginBoardSchema, { needs: "none" }),
|
|
427
549
|
settings: z._default(z.array(pluginSettingSchema).check(z.maxLength(MAX_PLUGIN_SETTINGS)), []),
|
|
550
|
+
settingGroups: z._default(
|
|
551
|
+
z.array(pluginSettingGroupSchema).check(z.maxLength(MAX_PLUGIN_SETTING_GROUPS)),
|
|
552
|
+
[],
|
|
553
|
+
),
|
|
428
554
|
menu: z.optional(pluginMenuSchema),
|
|
429
555
|
pages: z._default(z.array(pluginPageSchema).check(z.maxLength(MAX_PLUGIN_PAGES)), []),
|
|
430
556
|
widgets: z._default(z.array(pluginWidgetSchema).check(z.maxLength(MAX_PLUGIN_WIDGETS)), []),
|
|
@@ -465,12 +591,21 @@ function manifestProblems(
|
|
|
465
591
|
if (manifest.defaultRole !== undefined && !roles.includes(manifest.defaultRole)) {
|
|
466
592
|
problems.push(`defaultRole ${manifest.defaultRole} is not a declared role`);
|
|
467
593
|
}
|
|
468
|
-
|
|
594
|
+
const asked = capabilitiesAsked(manifest);
|
|
595
|
+
if (asked.length > MAX_PLUGIN_CAPABILITIES) {
|
|
596
|
+
problems.push(`capabilities: at most ${MAX_PLUGIN_CAPABILITIES}, the optional ones counted`);
|
|
597
|
+
}
|
|
598
|
+
if (manifest.optionalCapabilities.includes("storage:sql")) {
|
|
599
|
+
problems.push(
|
|
600
|
+
"optionalCapabilities: storage:sql cannot be optional: its schema is made when the plugin is enabled",
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
for (const capability of asked) {
|
|
469
604
|
if (!isPluginCapability(capability)) {
|
|
470
605
|
problems.push(`capabilities: ${capability} is not one the console knows`);
|
|
471
606
|
}
|
|
472
607
|
}
|
|
473
|
-
for (const capability of duplicates(
|
|
608
|
+
for (const capability of duplicates(asked)) {
|
|
474
609
|
problems.push(`capabilities: ${capability} is declared twice`);
|
|
475
610
|
}
|
|
476
611
|
const undeclared = (where: string, named: readonly string[] | undefined) => {
|
|
@@ -490,11 +625,20 @@ function manifestProblems(
|
|
|
490
625
|
["pages", manifest.pages.map((page) => page.key)],
|
|
491
626
|
["widgets", manifest.widgets.map((widget) => widget.key)],
|
|
492
627
|
["settings", manifest.settings.map((setting) => setting.key)],
|
|
628
|
+
["settingGroups", manifest.settingGroups.map((group) => group.key)],
|
|
493
629
|
] as const) {
|
|
494
630
|
for (const twice of duplicates(keys)) {
|
|
495
631
|
problems.push(`${what}: ${twice} is declared twice`);
|
|
496
632
|
}
|
|
497
633
|
}
|
|
634
|
+
const groups = manifest.settingGroups.map((group) => group.key);
|
|
635
|
+
for (const setting of manifest.settings) {
|
|
636
|
+
if (setting.group !== undefined && !groups.includes(setting.group)) {
|
|
637
|
+
problems.push(
|
|
638
|
+
`settings.${setting.key} names the group ${setting.group}, which settingGroups does not declare`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
498
642
|
if ((manifest.pages.length > 0 || manifest.widgets.length > 0) && manifest.web === undefined) {
|
|
499
643
|
problems.push("pages and widgets need web, the directory of the bundle");
|
|
500
644
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
PLUGIN_PAGE_LEVELS,
|
|
22
22
|
PLUGIN_SETTING_TYPES,
|
|
23
23
|
PLUGIN_WIDGET_PLACES,
|
|
24
|
+
PLUGIN_WIDGET_SIZE_NAMES,
|
|
24
25
|
PLUGIN_WIDGET_SIZES,
|
|
25
26
|
SETTING_KEY_PATTERN,
|
|
26
27
|
} from "./manifest.js";
|
|
@@ -91,7 +92,11 @@ export const restPluginWidgetSchema = z.object({
|
|
|
91
92
|
key: z.string(),
|
|
92
93
|
name: localized,
|
|
93
94
|
place: z.enum(PLUGIN_WIDGET_PLACES),
|
|
95
|
+
/** The size of before nearest to the one it starts at, for a reader of before. */
|
|
94
96
|
size: z.enum(PLUGIN_WIDGET_SIZES),
|
|
97
|
+
/** The sizes the widget comes in (ADR-0059) and the one it starts at; answered from 0.4.4 and read as optional. */
|
|
98
|
+
sizes: z.optional(z.array(z.enum(PLUGIN_WIDGET_SIZE_NAMES))),
|
|
99
|
+
defaultSize: z.optional(z.enum(PLUGIN_WIDGET_SIZE_NAMES)),
|
|
95
100
|
});
|
|
96
101
|
export type RestPluginWidget = z.infer<typeof restPluginWidgetSchema>;
|
|
97
102
|
|
|
@@ -103,6 +108,13 @@ export const restPluginSchema = z.object({
|
|
|
103
108
|
/** The sentence agents read with the rules, or `null` for the description. */
|
|
104
109
|
agentNote: z.nullable(localized),
|
|
105
110
|
version: z.string(),
|
|
111
|
+
/**
|
|
112
|
+
* The consoles the plugin declares it runs on, a semver range of the console's version, or
|
|
113
|
+
* `null` where the manifest names none; a console outside it fails the plugin with the reason
|
|
114
|
+
* in `problems`. Answered from 0.4.4 and read as optional, so that a console built on this
|
|
115
|
+
* package still reads an older console's answer.
|
|
116
|
+
*/
|
|
117
|
+
console: z.optional(z.nullable(z.string())),
|
|
106
118
|
state: z.enum(PLUGIN_STATES),
|
|
107
119
|
/** Why the plugin failed, or waits for consent; empty otherwise. */
|
|
108
120
|
problems: z.array(z.string()),
|
|
@@ -121,6 +133,8 @@ export const restPluginSchema = z.object({
|
|
|
121
133
|
boardNeeds: z.enum(PLUGIN_BOARD_NEEDS),
|
|
122
134
|
/** What the plugin asks, and what was consented to; `consent` is `null` to a member. */
|
|
123
135
|
capabilities: z.array(z.string()),
|
|
136
|
+
/** What it can go without (ADR-0060), an administrator's to turn on; a console older than them names none. */
|
|
137
|
+
optionalCapabilities: z.optional(z.array(z.string())),
|
|
124
138
|
consent: z.nullable(z.array(z.string())),
|
|
125
139
|
/** The projects the plugin is on: all, their keys, or `null` to a member. */
|
|
126
140
|
projects: z.nullable(z.union([z.literal("all"), z.array(z.string())])),
|
|
@@ -149,6 +163,8 @@ export const restPluginSettingSchema = z.object({
|
|
|
149
163
|
max: z.nullable(z.number()),
|
|
150
164
|
options: z.nullable(z.array(z.object({ value: z.string(), name: localized }))),
|
|
151
165
|
of: z.nullable(z.string()),
|
|
166
|
+
/** The group the setting is shown under, or `null` for above every group; answered from 0.4.4 and read as optional. */
|
|
167
|
+
group: z.optional(z.nullable(z.string())),
|
|
152
168
|
/** Whether a value is set; a secret's value is never here. */
|
|
153
169
|
set: z.boolean(),
|
|
154
170
|
value: z.nullable(z.unknown()),
|
|
@@ -179,6 +195,10 @@ export const restPluginDetailSchema = z.extend(restPluginSchema, {
|
|
|
179
195
|
/** The board project the manifest proposes, for an administrator who makes it; `null` where it proposes none. */
|
|
180
196
|
boardDefaults: z.nullable(z.object({ key: z.string(), name: localized, description: localized })),
|
|
181
197
|
settings: z.array(restPluginSettingSchema),
|
|
198
|
+
/** The groups the settings form is divided into, in the manifest's order; answered from 0.4.4 and read as optional. */
|
|
199
|
+
settingGroups: z.optional(
|
|
200
|
+
z.array(z.object({ key: z.string(), name: localized, description: z.nullable(localized) })),
|
|
201
|
+
),
|
|
182
202
|
/** The grants, to an administrator; `null` to a member. */
|
|
183
203
|
grants: z.nullable(z.array(restPluginGrantSchema)),
|
|
184
204
|
/** The plugin's own events are at this path. */
|
|
@@ -188,7 +208,7 @@ export type RestPluginDetail = z.infer<typeof restPluginDetailSchema>;
|
|
|
188
208
|
|
|
189
209
|
/** What `POST /api/v1/plugins/<key>/enable` is sent. */
|
|
190
210
|
export const restPluginEnableInputSchema = z.object({
|
|
191
|
-
/** Every capability the manifest
|
|
211
|
+
/** Every capability the manifest requires, named again, and the optional ones to turn on: consent. */
|
|
192
212
|
consent: z.array(z.string().check(z.maxLength(300))).check(z.maxLength(50)),
|
|
193
213
|
/** The board project: one to make, one that exists, or none. */
|
|
194
214
|
board: z.nullable(
|
|
@@ -216,6 +236,12 @@ export const restPluginSettingsPatchSchema = z.object({
|
|
|
216
236
|
});
|
|
217
237
|
export type RestPluginSettingsPatch = z.infer<typeof restPluginSettingsPatchSchema>;
|
|
218
238
|
|
|
239
|
+
/** What `PATCH /api/v1/plugins/<key>/consent` is sent: what the plugin holds from now on, those it requires named again. */
|
|
240
|
+
export const restPluginConsentPatchSchema = z.object({
|
|
241
|
+
consent: z.array(z.string().check(z.maxLength(300))).check(z.maxLength(50)),
|
|
242
|
+
});
|
|
243
|
+
export type RestPluginConsentPatch = z.infer<typeof restPluginConsentPatchSchema>;
|
|
244
|
+
|
|
219
245
|
/** What `PUT /api/v1/plugins/<key>/secrets/<name>` is sent. */
|
|
220
246
|
export const restPluginSecretInputSchema = z.object({
|
|
221
247
|
value: z.string().check(z.minLength(1), z.maxLength(MAX_PLUGIN_SECRET_LENGTH)),
|