@skillcdn/console 0.4.3 → 0.4.5
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 +6 -6
- package/dist/api.d.ts +4 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +255 -33
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +20 -5
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +98 -26
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +39 -15
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +5 -1
- package/dist/cli/plugin-check.d.ts.map +1 -1
- package/dist/cli/plugin-check.js +23 -4
- package/dist/cli/plugin-check.js.map +1 -1
- package/dist/cli/plugin-cli.d.ts.map +1 -1
- package/dist/cli/plugin-cli.js +10 -1
- package/dist/cli/plugin-cli.js.map +1 -1
- package/dist/cli/plugin-init.d.ts +2 -0
- package/dist/cli/plugin-init.d.ts.map +1 -1
- package/dist/cli/plugin-init.js +6 -0
- package/dist/cli/plugin-init.js.map +1 -1
- package/dist/client.d.ts +32 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -2
- 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 +49 -18
- package/dist/components/board.js.map +1 -1
- package/dist/components/decision-form.d.ts +6 -3
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +24 -18
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +5 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +25 -5
- 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 +34 -8
- 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 +20 -3
- package/dist/components/my-decisions.d.ts.map +1 -1
- package/dist/components/my-decisions.js +14 -1
- 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 +45 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +125 -4
- package/dist/components/people.js.map +1 -1
- package/dist/components/picker.d.ts +8 -0
- package/dist/components/picker.d.ts.map +1 -1
- package/dist/components/picker.js +2 -2
- package/dist/components/picker.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +3 -13
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +75 -21
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts +5 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +283 -45
- 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 +4 -14
- package/dist/components/projects.js.map +1 -1
- package/dist/components/searches.d.ts +11 -0
- package/dist/components/searches.d.ts.map +1 -0
- package/dist/components/searches.js +30 -0
- package/dist/components/searches.js.map +1 -0
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +6 -5
- package/dist/components/task-form.js.map +1 -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 +16 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +15 -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 +1123 -258
- package/dist/console.d.ts +12 -4
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +123 -27
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +18 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +41 -1
- 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 +264 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +260 -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 +255 -4
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +12 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +12 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/args.d.ts +6 -1
- package/dist/plugin/args.d.ts.map +1 -1
- package/dist/plugin/args.js +16 -11
- package/dist/plugin/args.js.map +1 -1
- package/dist/plugin/bridge.d.ts +63 -3
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +54 -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 +24 -1
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +174 -12
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +14 -1
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +18 -2
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +9 -2
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +9 -2
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +98 -6
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +171 -10
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +120 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +78 -5
- 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 +15 -1
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +42 -5
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +16 -1
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +44 -1
- 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/router.d.ts +19 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +18 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +11 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +14 -1
- package/dist/routes.js.map +1 -1
- package/dist/rules.d.ts +2 -2
- package/dist/rules.js +3 -3
- package/dist/schemas.d.ts +864 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +156 -9
- 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 +9 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +10 -6
- package/docs/specs/plugins.md +83 -62
- package/docs/specs/rest.md +34 -16
- package/package.json +1 -1
- package/skills/building-a-plugin/SKILL.md +15 -12
- package/skills/planning-the-board/references/setting-up.md +2 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/src/api.ts +23 -0
- package/src/cli/cli.ts +323 -30
- package/src/cli/format.ts +122 -26
- package/src/cli/help.ts +43 -14
- package/src/cli/plugin-check.ts +41 -2
- package/src/cli/plugin-cli.ts +10 -1
- package/src/cli/plugin-init.ts +8 -0
- package/src/client.ts +78 -0
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +86 -45
- package/src/components/decision-form.tsx +73 -36
- package/src/components/decision-list.tsx +45 -13
- package/src/components/event-feed.tsx +40 -7
- package/src/components/goals.tsx +4 -1
- package/src/components/home.tsx +615 -0
- package/src/components/my-decisions.tsx +50 -5
- package/src/components/pack-connect.tsx +5 -4
- package/src/components/people.tsx +500 -61
- package/src/components/picker.tsx +11 -2
- package/src/components/plugin-frame.tsx +85 -63
- package/src/components/plugin-settings.tsx +735 -185
- package/src/components/projects.tsx +14 -69
- package/src/components/searches.ts +47 -0
- package/src/components/task-form.tsx +23 -28
- package/src/components/task-view.tsx +7 -0
- package/src/components/ui.tsx +42 -2
- package/src/connect-page.tsx +5 -6
- package/src/console.tsx +303 -64
- package/src/data.ts +62 -3
- package/src/docs-page.tsx +6 -5
- package/src/home-tiles.ts +438 -0
- package/src/i18n/en.ts +274 -0
- package/src/i18n/index.ts +17 -14
- package/src/i18n/ko.ts +267 -4
- package/src/index.ts +18 -5
- package/src/limits.ts +12 -0
- package/src/plugin/args.ts +16 -11
- package/src/plugin/bridge.ts +122 -1
- package/src/plugin/capabilities.ts +28 -1
- package/src/plugin/context.ts +186 -9
- package/src/plugin/define.ts +38 -1
- package/src/plugin/limits.ts +9 -2
- package/src/plugin/manifest.ts +228 -13
- package/src/plugin/rest-schemas.ts +89 -6
- package/src/plugin/sandbox.ts +36 -8
- package/src/plugin/semver.ts +89 -0
- package/src/plugin/testing.ts +63 -6
- package/src/plugin/web.ts +66 -0
- package/src/plugin.ts +19 -0
- package/src/router.ts +25 -0
- package/src/routes.ts +17 -2
- package/src/rules.ts +3 -3
- package/src/schemas.ts +197 -10
- package/src/styles/console.css +1123 -258
- package/src/use-action.ts +16 -7
- package/src/vocabulary.ts +10 -0
- package/template/plugin/console-plugin.json +3 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MAX_PROJECT_DESCRIPTION_LENGTH,
|
|
8
8
|
MAX_PROJECT_KEY_LENGTH,
|
|
9
9
|
MAX_PROJECT_NAME_LENGTH,
|
|
10
|
+
MAX_TEAM_KEY_LENGTH,
|
|
10
11
|
} from "../limits.js";
|
|
11
12
|
import { PLUGIN_STATES } from "../vocabulary.js";
|
|
12
13
|
import { ARG_TYPES } from "./args.js";
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
PLUGIN_PAGE_LEVELS,
|
|
22
23
|
PLUGIN_SETTING_TYPES,
|
|
23
24
|
PLUGIN_WIDGET_PLACES,
|
|
25
|
+
PLUGIN_WIDGET_SIZE_NAMES,
|
|
24
26
|
PLUGIN_WIDGET_SIZES,
|
|
25
27
|
SETTING_KEY_PATTERN,
|
|
26
28
|
} from "./manifest.js";
|
|
@@ -91,7 +93,11 @@ export const restPluginWidgetSchema = z.object({
|
|
|
91
93
|
key: z.string(),
|
|
92
94
|
name: localized,
|
|
93
95
|
place: z.enum(PLUGIN_WIDGET_PLACES),
|
|
96
|
+
/** The size of before nearest to the one it starts at, for a reader of before. */
|
|
94
97
|
size: z.enum(PLUGIN_WIDGET_SIZES),
|
|
98
|
+
/** The sizes the widget comes in (ADR-0059) and the one it starts at; answered from 0.4.4 and read as optional. */
|
|
99
|
+
sizes: z.optional(z.array(z.enum(PLUGIN_WIDGET_SIZE_NAMES))),
|
|
100
|
+
defaultSize: z.optional(z.enum(PLUGIN_WIDGET_SIZE_NAMES)),
|
|
95
101
|
});
|
|
96
102
|
export type RestPluginWidget = z.infer<typeof restPluginWidgetSchema>;
|
|
97
103
|
|
|
@@ -103,13 +109,28 @@ export const restPluginSchema = z.object({
|
|
|
103
109
|
/** The sentence agents read with the rules, or `null` for the description. */
|
|
104
110
|
agentNote: z.nullable(localized),
|
|
105
111
|
version: z.string(),
|
|
112
|
+
/**
|
|
113
|
+
* The consoles the plugin declares it runs on, a semver range of the console's version, or
|
|
114
|
+
* `null` where the manifest names none; a console outside it fails the plugin with the reason
|
|
115
|
+
* in `problems`. Answered from 0.4.4 and read as optional, so that a console built on this
|
|
116
|
+
* package still reads an older console's answer.
|
|
117
|
+
*/
|
|
118
|
+
console: z.optional(z.nullable(z.string())),
|
|
106
119
|
state: z.enum(PLUGIN_STATES),
|
|
107
120
|
/** Why the plugin failed, or waits for consent; empty otherwise. */
|
|
108
121
|
problems: z.array(z.string()),
|
|
109
122
|
/** The asker's roles in the plugin; every role for an administrator. */
|
|
110
123
|
roles: z.array(z.string()),
|
|
111
124
|
/** The roles the plugin declares, by key, with their names. */
|
|
112
|
-
declaredRoles: z.array(
|
|
125
|
+
declaredRoles: z.array(
|
|
126
|
+
z.object({
|
|
127
|
+
key: z.string(),
|
|
128
|
+
name: localized,
|
|
129
|
+
description: localized,
|
|
130
|
+
/** What the role follows, held with no grant: `reports`, whoever manages someone (ADR-0068); answered from 0.4.5 and read as optional. */
|
|
131
|
+
follows: z.optional(z.nullable(z.literal("reports"))),
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
113
134
|
/** The role every member holds without a grant, or `null` where the manifest names none. */
|
|
114
135
|
defaultRole: z.nullable(z.string()),
|
|
115
136
|
menu: z.nullable(z.object({ group: z.string(), order: z.int(), name: z.nullable(localized) })),
|
|
@@ -121,6 +142,8 @@ export const restPluginSchema = z.object({
|
|
|
121
142
|
boardNeeds: z.enum(PLUGIN_BOARD_NEEDS),
|
|
122
143
|
/** What the plugin asks, and what was consented to; `consent` is `null` to a member. */
|
|
123
144
|
capabilities: z.array(z.string()),
|
|
145
|
+
/** What it can go without (ADR-0060), an administrator's to turn on; a console older than them names none. */
|
|
146
|
+
optionalCapabilities: z.optional(z.array(z.string())),
|
|
124
147
|
consent: z.nullable(z.array(z.string())),
|
|
125
148
|
/** The projects the plugin is on: all, their keys, or `null` to a member. */
|
|
126
149
|
projects: z.nullable(z.union([z.literal("all"), z.array(z.string())])),
|
|
@@ -149,11 +172,21 @@ export const restPluginSettingSchema = z.object({
|
|
|
149
172
|
max: z.nullable(z.number()),
|
|
150
173
|
options: z.nullable(z.array(z.object({ value: z.string(), name: localized }))),
|
|
151
174
|
of: z.nullable(z.string()),
|
|
175
|
+
/** The group the setting is shown under, or `null` for above every group; answered from 0.4.4 and read as optional. */
|
|
176
|
+
group: z.optional(z.nullable(z.string())),
|
|
152
177
|
/** Whether a value is set; a secret's value is never here. */
|
|
153
178
|
set: z.boolean(),
|
|
154
179
|
value: z.nullable(z.unknown()),
|
|
155
180
|
/** When a secret was set, or `null`. */
|
|
156
181
|
setAt: z.nullable(instant),
|
|
182
|
+
/** When a secret expires, or `null` (ADR-0070); answered from 0.4.5 and read as optional. */
|
|
183
|
+
expiresAt: z.optional(z.nullable(instant)),
|
|
184
|
+
/** `administrator` for a secret an administrator set, the host of the answer it was sealed from otherwise, `null` while unset. */
|
|
185
|
+
source: z.optional(z.nullable(z.string())),
|
|
186
|
+
/** The hosts a secret is pinned to, or `null` for any the plugin reaches. */
|
|
187
|
+
hosts: z.optional(z.nullable(z.array(z.string()))),
|
|
188
|
+
/** `verify` for a secret that verifies and signs nothing, or `null`. */
|
|
189
|
+
use: z.optional(z.nullable(z.literal("verify"))),
|
|
157
190
|
});
|
|
158
191
|
export type RestPluginSetting = z.infer<typeof restPluginSettingSchema>;
|
|
159
192
|
|
|
@@ -164,6 +197,10 @@ export const restPluginGrantSchema = z.object({
|
|
|
164
197
|
person: z.nullable(z.object({ id: uuid, login: z.string(), name: z.nullable(z.string()) })),
|
|
165
198
|
project: z.nullable(z.object({ key: z.string(), name: z.string() })),
|
|
166
199
|
everyone: z.boolean(),
|
|
200
|
+
/** The plugin the role went to (ADR-0072), by key, or `null`; answered from 0.4.5 and read as optional. */
|
|
201
|
+
plugin: z.optional(z.nullable(z.string())),
|
|
202
|
+
/** The team the role went to (ADR-0073), whose members hold it, or `null`; answered from 0.4.5 and read as optional. */
|
|
203
|
+
team: z.optional(z.nullable(z.object({ key: z.string(), name: z.string() }))),
|
|
167
204
|
createdAt: instant,
|
|
168
205
|
});
|
|
169
206
|
export type RestPluginGrant = z.infer<typeof restPluginGrantSchema>;
|
|
@@ -179,16 +216,42 @@ export const restPluginDetailSchema = z.extend(restPluginSchema, {
|
|
|
179
216
|
/** The board project the manifest proposes, for an administrator who makes it; `null` where it proposes none. */
|
|
180
217
|
boardDefaults: z.nullable(z.object({ key: z.string(), name: localized, description: localized })),
|
|
181
218
|
settings: z.array(restPluginSettingSchema),
|
|
219
|
+
/** The groups the settings form is divided into, in the manifest's order; answered from 0.4.4 and read as optional. */
|
|
220
|
+
settingGroups: z.optional(
|
|
221
|
+
z.array(z.object({ key: z.string(), name: localized, description: z.nullable(localized) })),
|
|
222
|
+
),
|
|
182
223
|
/** The grants, to an administrator; `null` to a member. */
|
|
183
224
|
grants: z.nullable(z.array(restPluginGrantSchema)),
|
|
184
225
|
/** The plugin's own events are at this path. */
|
|
185
226
|
events: z.string(),
|
|
227
|
+
/**
|
|
228
|
+
* Whether the plugin reads a time of day (a job at a time, a `time` setting) while the
|
|
229
|
+
* workspace has no time zone, so that its times run on UTC with nobody having said so: the
|
|
230
|
+
* pages and the command tell an administrator to set one. Answered from 0.4.5 and read as optional.
|
|
231
|
+
*/
|
|
232
|
+
needsTimeZone: z.optional(z.boolean()),
|
|
233
|
+
/** The collections the plugin keeps for a while only, each with its days, so that an administrator reads what it forgets; answered from 0.4.5 and read as optional. */
|
|
234
|
+
keeps: z.optional(z.array(z.object({ collection: z.string(), days: z.int() }))),
|
|
235
|
+
/**
|
|
236
|
+
* What the manifest asks of other plugins (ADR-0072): a role of a plugin, why, and how it
|
|
237
|
+
* stands: granted, not yet, or the plugin is not enabled. Answered from 0.4.5 and read as optional.
|
|
238
|
+
*/
|
|
239
|
+
asks: z.optional(
|
|
240
|
+
z.array(
|
|
241
|
+
z.object({
|
|
242
|
+
plugin: z.string(),
|
|
243
|
+
role: z.string(),
|
|
244
|
+
why: localized,
|
|
245
|
+
state: z.enum(["granted", "ungranted", "unavailable"]),
|
|
246
|
+
}),
|
|
247
|
+
),
|
|
248
|
+
),
|
|
186
249
|
});
|
|
187
250
|
export type RestPluginDetail = z.infer<typeof restPluginDetailSchema>;
|
|
188
251
|
|
|
189
252
|
/** What `POST /api/v1/plugins/<key>/enable` is sent. */
|
|
190
253
|
export const restPluginEnableInputSchema = z.object({
|
|
191
|
-
/** Every capability the manifest
|
|
254
|
+
/** Every capability the manifest requires, named again, and the optional ones to turn on: consent. */
|
|
192
255
|
consent: z.array(z.string().check(z.maxLength(300))).check(z.maxLength(50)),
|
|
193
256
|
/** The board project: one to make, one that exists, or none. */
|
|
194
257
|
board: z.nullable(
|
|
@@ -216,6 +279,12 @@ export const restPluginSettingsPatchSchema = z.object({
|
|
|
216
279
|
});
|
|
217
280
|
export type RestPluginSettingsPatch = z.infer<typeof restPluginSettingsPatchSchema>;
|
|
218
281
|
|
|
282
|
+
/** What `PATCH /api/v1/plugins/<key>/consent` is sent: what the plugin holds from now on, those it requires named again. */
|
|
283
|
+
export const restPluginConsentPatchSchema = z.object({
|
|
284
|
+
consent: z.array(z.string().check(z.maxLength(300))).check(z.maxLength(50)),
|
|
285
|
+
});
|
|
286
|
+
export type RestPluginConsentPatch = z.infer<typeof restPluginConsentPatchSchema>;
|
|
287
|
+
|
|
219
288
|
/** What `PUT /api/v1/plugins/<key>/secrets/<name>` is sent. */
|
|
220
289
|
export const restPluginSecretInputSchema = z.object({
|
|
221
290
|
value: z.string().check(z.minLength(1), z.maxLength(MAX_PLUGIN_SECRET_LENGTH)),
|
|
@@ -228,15 +297,23 @@ export const restPluginGrantInputSchema = z
|
|
|
228
297
|
role: pluginKey,
|
|
229
298
|
person: z.optional(uuid),
|
|
230
299
|
project: z.optional(projectKey),
|
|
300
|
+
/** A plugin, by key, to hold the role as itself (ADR-0072). */
|
|
301
|
+
plugin: z.optional(pluginKey),
|
|
302
|
+
/** A team, by its key (ADR-0074), whose members hold the role (ADR-0073). */
|
|
303
|
+
team: z.optional(z.string().check(z.minLength(1), z.maxLength(MAX_TEAM_KEY_LENGTH))),
|
|
231
304
|
everyone: z.optional(z.literal(true)),
|
|
232
305
|
})
|
|
233
306
|
.check(
|
|
234
307
|
z.refine(
|
|
235
308
|
(input) =>
|
|
236
|
-
[
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
309
|
+
[
|
|
310
|
+
input.person !== undefined,
|
|
311
|
+
input.project !== undefined,
|
|
312
|
+
input.plugin !== undefined,
|
|
313
|
+
input.team !== undefined,
|
|
314
|
+
input.everyone === true,
|
|
315
|
+
].filter(Boolean).length === 1,
|
|
316
|
+
"names a person, a project, a plugin, a team or everyone, one of the five",
|
|
240
317
|
),
|
|
241
318
|
);
|
|
242
319
|
export type RestPluginGrantInput = z.infer<typeof restPluginGrantInputSchema>;
|
|
@@ -285,6 +362,12 @@ export const restPluginInvocationSchema = z.object({
|
|
|
285
362
|
text: z.string(),
|
|
286
363
|
data: z.unknown(),
|
|
287
364
|
waiting: z.nullable(z.string()),
|
|
365
|
+
/**
|
|
366
|
+
* The key of the project the decision in `waiting` waits in, the one it was raised in, where
|
|
367
|
+
* the caller reaches it, else `null`; with nothing waiting, the plugin's board project's, or
|
|
368
|
+
* `null`. Answered from 0.4.5 and read as optional.
|
|
369
|
+
*/
|
|
370
|
+
project: z.optional(z.nullable(z.string())),
|
|
288
371
|
});
|
|
289
372
|
export type RestPluginInvocation = z.infer<typeof restPluginInvocationSchema>;
|
|
290
373
|
|
package/src/plugin/sandbox.ts
CHANGED
|
@@ -13,6 +13,15 @@ export const FORBIDDEN_BUILTINS: readonly string[] = [
|
|
|
13
13
|
"dgram",
|
|
14
14
|
"dns",
|
|
15
15
|
"dns/promises",
|
|
16
|
+
// The same network, under the names Node keeps for compatibility.
|
|
17
|
+
"_http_agent",
|
|
18
|
+
"_http_client",
|
|
19
|
+
"_http_common",
|
|
20
|
+
"_http_incoming",
|
|
21
|
+
"_http_outgoing",
|
|
22
|
+
"_http_server",
|
|
23
|
+
"_tls_common",
|
|
24
|
+
"_tls_wrap",
|
|
16
25
|
"child_process",
|
|
17
26
|
"cluster",
|
|
18
27
|
"worker_threads",
|
|
@@ -67,6 +76,15 @@ export interface HardenedThread {
|
|
|
67
76
|
readonly removed: readonly string[];
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
/** What a plugin is told of a built-in it may not have, by whichever way it asked. */
|
|
80
|
+
function refusedBuiltin(specifier: string): Error {
|
|
81
|
+
const error = new Error(
|
|
82
|
+
`the host refuses ${specifier}: a plugin reaches the console through ctx alone`,
|
|
83
|
+
);
|
|
84
|
+
(error as { code?: string }).code = "ERR_PLUGIN_FORBIDDEN_BUILTIN";
|
|
85
|
+
return error;
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
/** The module hook: refuses the forbidden built-ins and lets everything else resolve as it would. */
|
|
71
89
|
export function forbiddingResolve<C, R>(
|
|
72
90
|
specifier: string,
|
|
@@ -74,19 +92,16 @@ export function forbiddingResolve<C, R>(
|
|
|
74
92
|
nextResolve: (specifier: string, context: C) => R,
|
|
75
93
|
): R {
|
|
76
94
|
if (isForbiddenBuiltin(specifier)) {
|
|
77
|
-
|
|
78
|
-
`the host refuses ${specifier}: a plugin reaches the console through ctx alone`,
|
|
79
|
-
);
|
|
80
|
-
(error as { code?: string }).code = "ERR_PLUGIN_FORBIDDEN_BUILTIN";
|
|
81
|
-
throw error;
|
|
95
|
+
throw refusedBuiltin(specifier);
|
|
82
96
|
}
|
|
83
97
|
return nextResolve(specifier, context);
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
/**
|
|
87
|
-
* Hardens the current thread: refuses the forbidden built-ins through the module hooks
|
|
88
|
-
* the globals, empties the environment,
|
|
89
|
-
* before importing plugin code; it reaches
|
|
101
|
+
* Hardens the current thread: refuses the forbidden built-ins through the module hooks and through
|
|
102
|
+
* `process.getBuiltinModule`, which goes round them, removes the globals, empties the environment,
|
|
103
|
+
* and blocks `process.binding`. Call it in a worker thread before importing plugin code; it reaches
|
|
104
|
+
* nothing outside that thread.
|
|
90
105
|
*/
|
|
91
106
|
export function hardenThread(
|
|
92
107
|
moduleApi: Pick<typeof import("node:module"), "registerHooks">,
|
|
@@ -112,6 +127,19 @@ export function hardenThread(
|
|
|
112
127
|
for (const name of ["binding", "_linkedBinding", "dlopen"]) {
|
|
113
128
|
Object.defineProperty(proc, name, { value: refuse, writable: false, configurable: false });
|
|
114
129
|
}
|
|
130
|
+
const getBuiltinModule = proc.getBuiltinModule;
|
|
131
|
+
if (typeof getBuiltinModule === "function") {
|
|
132
|
+
Object.defineProperty(proc, "getBuiltinModule", {
|
|
133
|
+
value: (specifier: string): unknown => {
|
|
134
|
+
if (isForbiddenBuiltin(specifier)) {
|
|
135
|
+
throw refusedBuiltin(specifier);
|
|
136
|
+
}
|
|
137
|
+
return getBuiltinModule.call(proc, specifier);
|
|
138
|
+
},
|
|
139
|
+
writable: false,
|
|
140
|
+
configurable: false,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
115
143
|
const env = proc.env as Record<string, unknown> | undefined;
|
|
116
144
|
if (env !== undefined) {
|
|
117
145
|
for (const key of Object.keys(env)) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Versions and ranges as a manifest spells them (docs/specs/plugins.md, "The manifest"): the
|
|
2
|
+
// range of another plugin's version in `requires`, and the range of the console's version in
|
|
3
|
+
// `console`. Read loosely, as npm spells the common ones, with no library: a plugin declares
|
|
4
|
+
// `>=0.4.3 <0.6`, and the console, the authoring command and the tests compare the same way.
|
|
5
|
+
|
|
6
|
+
export type Triple = readonly [number, number, number];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A version read loosely: `0.4.3`, `v0.4.3`, `0.6` (as `0.6.0`), `1` (as `1.0.0`), a prerelease or
|
|
10
|
+
* a build after the numbers ignored; nothing for anything else.
|
|
11
|
+
*/
|
|
12
|
+
export function parseLooseVersion(text: string): Triple | undefined {
|
|
13
|
+
const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:[-+].*)?$/.exec(text.trim());
|
|
14
|
+
return match === null
|
|
15
|
+
? undefined
|
|
16
|
+
: [Number(match[1]), Number(match[2] ?? "0"), Number(match[3] ?? "0")];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function compareTriples(a: Triple, b: Triple): number {
|
|
20
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether a version is within a range as npm spells the common ones: `*`, exact, `^`, `~`, `>=`,
|
|
25
|
+
* `>`, `<=`, `<`, several with spaces between (all must hold) and alternatives with `||` between
|
|
26
|
+
* (one must hold). A partial version in the range is read as `.0` filled in: `<0.6` is `<0.6.0`.
|
|
27
|
+
*/
|
|
28
|
+
export function satisfiesRange(version: string, range: string): boolean {
|
|
29
|
+
const have = parseLooseVersion(version);
|
|
30
|
+
const trimmed = range.trim();
|
|
31
|
+
if (trimmed === "*" || trimmed === "" || trimmed === "x" || trimmed === "latest") {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (have === undefined) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return trimmed.split(/\s*\|\|\s*/).some((alternative) =>
|
|
38
|
+
alternative.split(/\s+/).every((part) => {
|
|
39
|
+
const operator = /^(\^|~|>=|<=|>|<|=)?(.*)$/.exec(part);
|
|
40
|
+
const wanted = parseLooseVersion(operator?.[2] ?? "");
|
|
41
|
+
if (wanted === undefined) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
switch (operator?.[1]) {
|
|
45
|
+
case "^":
|
|
46
|
+
return (
|
|
47
|
+
compareTriples(have, wanted) >= 0 &&
|
|
48
|
+
(wanted[0] === 0 ? have[0] === 0 && have[1] === wanted[1] : have[0] === wanted[0])
|
|
49
|
+
);
|
|
50
|
+
case "~":
|
|
51
|
+
return (
|
|
52
|
+
compareTriples(have, wanted) >= 0 && have[0] === wanted[0] && have[1] === wanted[1]
|
|
53
|
+
);
|
|
54
|
+
case ">=":
|
|
55
|
+
return compareTriples(have, wanted) >= 0;
|
|
56
|
+
case ">":
|
|
57
|
+
return compareTriples(have, wanted) > 0;
|
|
58
|
+
case "<=":
|
|
59
|
+
return compareTriples(have, wanted) <= 0;
|
|
60
|
+
case "<":
|
|
61
|
+
return compareTriples(have, wanted) < 0;
|
|
62
|
+
default:
|
|
63
|
+
return compareTriples(have, wanted) === 0;
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The version a dependency spells, read loosely: `^0.4.3`, `~0.4.3`, `>=0.4.3` and `0.4.3` are
|
|
71
|
+
* all `0.4.3`; `workspace:*`, `*`, `latest` and a tag name none.
|
|
72
|
+
*/
|
|
73
|
+
export function versionOfDependency(spec: string): Triple | undefined {
|
|
74
|
+
return parseLooseVersion(spec.trim().replace(/^(?:\^|~|>=|=)\s*/, ""));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function formatTriple(version: Triple): string {
|
|
78
|
+
return `${version[0]}.${version[1]}.${version[2]}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The range of consoles a plugin written against `version` runs on, as `console-plugin init`
|
|
83
|
+
* writes it: from that version on, within its line, since the plugin API moves with the console's
|
|
84
|
+
* line before 1.0 (`>=0.4.3 <0.5.0`), and within its major from 1.0 (`>=1.2.0 <2.0.0`).
|
|
85
|
+
*/
|
|
86
|
+
export function lineRangeFrom(version: Triple): string {
|
|
87
|
+
const below: Triple = version[0] === 0 ? [0, version[1] + 1, 0] : [version[0] + 1, 0, 0];
|
|
88
|
+
return `>=${formatTriple(version)} <${formatTriple(below)}`;
|
|
89
|
+
}
|
package/src/plugin/testing.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { PluginRefusal } from "./refusals.js";
|
|
|
38
38
|
export interface TestConsoleOptions {
|
|
39
39
|
readonly workspace?: PluginConsole["workspace"];
|
|
40
40
|
readonly people?: Partial<PluginConsole["people"]>;
|
|
41
|
+
readonly teams?: Partial<PluginConsole["teams"]>;
|
|
41
42
|
readonly projects?: Partial<PluginConsole["projects"]>;
|
|
42
43
|
readonly tasks?: Partial<PluginConsole["tasks"]>;
|
|
43
44
|
readonly decisions?: Partial<PluginConsole["decisions"]>;
|
|
@@ -45,6 +46,16 @@ export interface TestConsoleOptions {
|
|
|
45
46
|
readonly runs?: Partial<PluginConsole["runs"]>;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Another plugin as a test gives it: its version and its api; and, where `for` is given, what it
|
|
51
|
+
* answers when called for a person. Without `for`, it answers the same for anyone.
|
|
52
|
+
*/
|
|
53
|
+
export interface TestOtherPlugin {
|
|
54
|
+
readonly version: string;
|
|
55
|
+
readonly api: OtherPlugin["api"];
|
|
56
|
+
readonly for?: ((personId: string) => TestOtherPlugin) | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
export interface TestContextOptions {
|
|
49
60
|
readonly plugin?: Partial<PluginInfo>;
|
|
50
61
|
readonly caller?: PluginCaller;
|
|
@@ -56,7 +67,11 @@ export interface TestContextOptions {
|
|
|
56
67
|
readonly fetch?: (url: string, init?: PluginFetchInit) => Promise<PluginFetchResponse>;
|
|
57
68
|
/** The secrets by name; `sign` and `verify` use them, and never hand them over. */
|
|
58
69
|
readonly secrets?: Readonly<Record<string, string>>;
|
|
59
|
-
readonly plugins?: Readonly<Record<string,
|
|
70
|
+
readonly plugins?: Readonly<Record<string, TestOtherPlugin>>;
|
|
71
|
+
/** The console's public origin, from which `ctx.endpoints.url` is answered; left out, it is `plugin.unavailable`. */
|
|
72
|
+
readonly publicUrl?: string;
|
|
73
|
+
/** The endpoints the plugin defines, by name, which `ctx.endpoints.url` answers for; any name when left out. */
|
|
74
|
+
readonly endpoints?: readonly string[];
|
|
60
75
|
readonly now?: () => Date;
|
|
61
76
|
readonly language?: string;
|
|
62
77
|
}
|
|
@@ -226,7 +241,7 @@ function memoryRecords<T extends Record<string, unknown>>(
|
|
|
226
241
|
}
|
|
227
242
|
return found;
|
|
228
243
|
};
|
|
229
|
-
const by = caller
|
|
244
|
+
const by = actingPersonOf(caller)?.id ?? null;
|
|
230
245
|
const indexed = (field: string) => {
|
|
231
246
|
if (
|
|
232
247
|
collection !== undefined &&
|
|
@@ -519,15 +534,35 @@ function refusingConsole(given: TestConsoleOptions | undefined): PluginConsole {
|
|
|
519
534
|
return {
|
|
520
535
|
workspace:
|
|
521
536
|
given?.workspace ?? (async () => forbidden("console.workspace", "console.workspace")),
|
|
522
|
-
people: fill("people", ["list", "get"], given?.people),
|
|
537
|
+
people: fill("people", ["list", "get", "granted"], given?.people),
|
|
538
|
+
teams: fill("teams", ["list", "get"], given?.teams),
|
|
523
539
|
projects: fill("projects", ["list", "get", "members"], given?.projects),
|
|
524
540
|
tasks: fill("tasks", ["list", "get", "create", "update", "comment"], given?.tasks),
|
|
525
|
-
decisions: fill("decisions", ["list", "get", "ask"], given?.decisions),
|
|
541
|
+
decisions: fill("decisions", ["list", "get", "ask", "answer"], given?.decisions),
|
|
526
542
|
documents: fill("documents", ["list", "read", "write"], given?.documents),
|
|
527
543
|
runs: fill("runs", ["list", "get"], given?.runs),
|
|
528
544
|
};
|
|
529
545
|
}
|
|
530
546
|
|
|
547
|
+
/** The person a caller acts as: themselves, or the one a plugin acts for; none for the console or a plugin as itself. */
|
|
548
|
+
function actingPersonOf(caller: PluginCaller) {
|
|
549
|
+
return caller.kind === "person"
|
|
550
|
+
? caller.person
|
|
551
|
+
: caller.kind === "plugin"
|
|
552
|
+
? (caller.for ?? undefined)
|
|
553
|
+
: undefined;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Another plugin as the context hands it over: `for` answers as the test said, or the same as for anyone. */
|
|
557
|
+
function withFor(other: TestOtherPlugin): OtherPlugin {
|
|
558
|
+
const built: OtherPlugin = {
|
|
559
|
+
version: other.version,
|
|
560
|
+
api: other.api,
|
|
561
|
+
for: (personId) => (other.for === undefined ? built : withFor(other.for(personId))),
|
|
562
|
+
};
|
|
563
|
+
return built;
|
|
564
|
+
}
|
|
565
|
+
|
|
531
566
|
function build(store: Store, options: TestContextOptions, caller: PluginCaller): TestContext {
|
|
532
567
|
const now = options.now ?? (() => new Date());
|
|
533
568
|
const collections = new Map(
|
|
@@ -538,8 +573,10 @@ function build(store: Store, options: TestContextOptions, caller: PluginCaller):
|
|
|
538
573
|
version: "0.0.0",
|
|
539
574
|
dataVersion: 1,
|
|
540
575
|
settings: {},
|
|
576
|
+
capabilities: [],
|
|
541
577
|
board: null,
|
|
542
578
|
projects: "all",
|
|
579
|
+
grants: [],
|
|
543
580
|
...options.plugin,
|
|
544
581
|
};
|
|
545
582
|
const secrets = options.secrets ?? {};
|
|
@@ -570,6 +607,12 @@ function build(store: Store, options: TestContextOptions, caller: PluginCaller):
|
|
|
570
607
|
fetch: options.fetch ?? (async () => forbidden("fetch", "fetch")),
|
|
571
608
|
secrets: {
|
|
572
609
|
has: async (name) => name in secrets,
|
|
610
|
+
status: async (name) => ({
|
|
611
|
+
set: name in secrets,
|
|
612
|
+
setAt: name in secrets ? now().toISOString() : null,
|
|
613
|
+
expiresAt: null,
|
|
614
|
+
source: name in secrets ? "administrator" : null,
|
|
615
|
+
}),
|
|
573
616
|
sign: async (name, payload) => {
|
|
574
617
|
const secret = secrets[name];
|
|
575
618
|
if (secret === undefined) {
|
|
@@ -590,12 +633,26 @@ function build(store: Store, options: TestContextOptions, caller: PluginCaller):
|
|
|
590
633
|
if (other === undefined) {
|
|
591
634
|
forbidden(`plugins.get(${key})`, `plugins.${key}`);
|
|
592
635
|
}
|
|
593
|
-
return other;
|
|
636
|
+
return withFor(other);
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
endpoints: {
|
|
640
|
+
async url(name) {
|
|
641
|
+
if (options.endpoints !== undefined && !options.endpoints.includes(name)) {
|
|
642
|
+
throw new PluginRefusal("plugin.not_found", `the plugin defines no endpoint ${name}`);
|
|
643
|
+
}
|
|
644
|
+
if (options.publicUrl === undefined) {
|
|
645
|
+
throw new PluginRefusal(
|
|
646
|
+
"plugin.unavailable",
|
|
647
|
+
"this console has no public address (PUBLIC_URL) to reach its endpoints at",
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
return `${options.publicUrl}/api/v1/plugins/${encodeURIComponent(plugin.key)}/x/${encodeURIComponent(name)}`;
|
|
594
651
|
},
|
|
595
652
|
},
|
|
596
653
|
log: { info: log("info"), warn: log("warn"), error: log("error") },
|
|
597
654
|
now,
|
|
598
|
-
language: options.language ?? (caller
|
|
655
|
+
language: options.language ?? actingPersonOf(caller)?.language ?? "en",
|
|
599
656
|
signal: new AbortController().signal,
|
|
600
657
|
emitted: store.emitted,
|
|
601
658
|
logged: store.logged,
|
package/src/plugin/web.ts
CHANGED
|
@@ -11,11 +11,14 @@ import {
|
|
|
11
11
|
type BridgeFile,
|
|
12
12
|
type BridgeInit,
|
|
13
13
|
type BridgeMethod,
|
|
14
|
+
type BridgePicture,
|
|
14
15
|
type BridgeToPage,
|
|
15
16
|
isBridgePath,
|
|
16
17
|
isBridgeToFrame,
|
|
18
|
+
isBridgeUrl,
|
|
17
19
|
MAX_BRIDGE_TITLE_LENGTH,
|
|
18
20
|
PLUGIN_BRIDGE_VERSION,
|
|
21
|
+
type PluginWidgetSizeName,
|
|
19
22
|
} from "./bridge.js";
|
|
20
23
|
import type { PluginRecord, RecordPage, RecordVersion } from "./context.js";
|
|
21
24
|
|
|
@@ -111,8 +114,21 @@ export interface PluginFrame {
|
|
|
111
114
|
type: string,
|
|
112
115
|
): Promise<{ readonly handle: string }>;
|
|
113
116
|
};
|
|
117
|
+
readonly people: {
|
|
118
|
+
/**
|
|
119
|
+
* An address to put in an image, of the person of that id (ADR-0063), with `people:read`; or
|
|
120
|
+
* `null` for a person with no picture to show. Kept for the life of the frame.
|
|
121
|
+
*/
|
|
122
|
+
picture(id: string): Promise<string | null>;
|
|
123
|
+
};
|
|
114
124
|
/** The console's page follows a path of the console's in place; a URL is refused. */
|
|
115
125
|
navigate(path: string): void;
|
|
126
|
+
/**
|
|
127
|
+
* The console's page opens a web or a mail address in a new tab (ADR-0067), for a link a
|
|
128
|
+
* person pressed: call it from the press itself, since the page opens nothing but right after
|
|
129
|
+
* one. Anything else, a path of the console's included, is refused; `navigate` takes those.
|
|
130
|
+
*/
|
|
131
|
+
open(url: string): void;
|
|
116
132
|
/** The console's page downloads a file of this plugin. */
|
|
117
133
|
download(id: string): void;
|
|
118
134
|
setTitle(text: string): void;
|
|
@@ -125,12 +141,21 @@ export interface PluginFrame {
|
|
|
125
141
|
onTheme(handler: (theme: Readonly<Record<string, string>>) => void): () => void;
|
|
126
142
|
/** The console navigated within this page's address. */
|
|
127
143
|
onPath(handler: (path: string) => void): () => void;
|
|
144
|
+
/** The person gave the widget another size (ADR-0059); `context.size` was the first. */
|
|
145
|
+
onSize(handler: (size: PluginWidgetSizeName) => void): () => void;
|
|
128
146
|
/** Stops listening to the console's page. */
|
|
129
147
|
close(): void;
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
const DEFAULT_CONNECT_MS = 10_000;
|
|
133
151
|
|
|
152
|
+
/** What a page is told for a person with no picture to show, which is no failure of the page's. */
|
|
153
|
+
const NO_PICTURE: ReadonlySet<string> = new Set([
|
|
154
|
+
"plugin.not_found",
|
|
155
|
+
"picture.refused",
|
|
156
|
+
"picture.unavailable",
|
|
157
|
+
]);
|
|
158
|
+
|
|
134
159
|
function globalWindow(): FrameWindow {
|
|
135
160
|
const candidate = (globalThis as { window?: FrameWindow }).window;
|
|
136
161
|
if (candidate === undefined) {
|
|
@@ -219,7 +244,9 @@ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
|
|
|
219
244
|
const eventHandlers = new Set<(kind: BridgeEventKind, data: unknown) => void>();
|
|
220
245
|
const themeHandlers = new Set<(theme: Readonly<Record<string, string>>) => void>();
|
|
221
246
|
const pathHandlers = new Set<(path: string) => void>();
|
|
247
|
+
const sizeHandlers = new Set<(size: PluginWidgetSizeName) => void>();
|
|
222
248
|
const subscribed = new Set<BridgeEventKind>();
|
|
249
|
+
const pictures = new Map<string, Promise<string | null>>();
|
|
223
250
|
let counter = 0;
|
|
224
251
|
let stopHeight: (() => void) | undefined;
|
|
225
252
|
|
|
@@ -301,6 +328,26 @@ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
|
|
|
301
328
|
}>;
|
|
302
329
|
},
|
|
303
330
|
},
|
|
331
|
+
people: {
|
|
332
|
+
picture: (id) => {
|
|
333
|
+
const kept = pictures.get(id);
|
|
334
|
+
if (kept !== undefined) {
|
|
335
|
+
return kept;
|
|
336
|
+
}
|
|
337
|
+
const asked = (call("people.picture", { id }) as Promise<BridgePicture>).then(
|
|
338
|
+
({ type, bytes }) => URL.createObjectURL(new Blob([bytes], { type })),
|
|
339
|
+
(error: unknown) => {
|
|
340
|
+
if (error instanceof PluginBridgeError && NO_PICTURE.has(error.code)) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
pictures.delete(id);
|
|
344
|
+
throw error;
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
pictures.set(id, asked);
|
|
348
|
+
return asked;
|
|
349
|
+
},
|
|
350
|
+
},
|
|
304
351
|
navigate: (path) => {
|
|
305
352
|
if (!isBridgePath(path)) {
|
|
306
353
|
throw new PluginBridgeError({
|
|
@@ -310,6 +357,16 @@ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
|
|
|
310
357
|
}
|
|
311
358
|
post({ v: PLUGIN_BRIDGE_VERSION, t: "navigate", path });
|
|
312
359
|
},
|
|
360
|
+
open: (url) => {
|
|
361
|
+
if (!isBridgeUrl(url)) {
|
|
362
|
+
throw new PluginBridgeError({
|
|
363
|
+
code: "plugin.forbidden",
|
|
364
|
+
message:
|
|
365
|
+
"open takes a web or a mail address; navigate takes a path of the console's",
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "open", url });
|
|
369
|
+
},
|
|
313
370
|
download: (id) => post({ v: PLUGIN_BRIDGE_VERSION, t: "download", id }),
|
|
314
371
|
setTitle: (text) => {
|
|
315
372
|
const title = text.slice(0, MAX_BRIDGE_TITLE_LENGTH).replace(/[\r\n]+/g, " ");
|
|
@@ -338,6 +395,10 @@ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
|
|
|
338
395
|
pathHandlers.add(handler);
|
|
339
396
|
return () => pathHandlers.delete(handler);
|
|
340
397
|
},
|
|
398
|
+
onSize: (handler) => {
|
|
399
|
+
sizeHandlers.add(handler);
|
|
400
|
+
return () => sizeHandlers.delete(handler);
|
|
401
|
+
},
|
|
341
402
|
close: () => {
|
|
342
403
|
win.removeEventListener("message", listener);
|
|
343
404
|
stopHeight?.();
|
|
@@ -393,6 +454,11 @@ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
|
|
|
393
454
|
handler(message.path);
|
|
394
455
|
}
|
|
395
456
|
return;
|
|
457
|
+
case "size":
|
|
458
|
+
for (const handler of sizeHandlers) {
|
|
459
|
+
handler(message.size);
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
396
462
|
default:
|
|
397
463
|
return;
|
|
398
464
|
}
|