@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.
Files changed (243) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts +3 -3
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +3 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +120 -33
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +4 -2
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +38 -10
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +22 -15
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/plugin-check.d.ts +10 -1
  17. package/dist/cli/plugin-check.d.ts.map +1 -1
  18. package/dist/cli/plugin-check.js +45 -4
  19. package/dist/cli/plugin-check.js.map +1 -1
  20. package/dist/cli/plugin-cli.d.ts +1 -1
  21. package/dist/cli/plugin-cli.d.ts.map +1 -1
  22. package/dist/cli/plugin-cli.js +80 -5
  23. package/dist/cli/plugin-cli.js.map +1 -1
  24. package/dist/cli/plugin-init.d.ts +19 -0
  25. package/dist/cli/plugin-init.d.ts.map +1 -1
  26. package/dist/cli/plugin-init.js +141 -3
  27. package/dist/cli/plugin-init.js.map +1 -1
  28. package/dist/client.d.ts +7 -2
  29. package/dist/client.d.ts.map +1 -1
  30. package/dist/client.js +18 -5
  31. package/dist/client.js.map +1 -1
  32. package/dist/compat.js +1 -1
  33. package/dist/components/board.d.ts +8 -0
  34. package/dist/components/board.d.ts.map +1 -1
  35. package/dist/components/board.js +3 -3
  36. package/dist/components/board.js.map +1 -1
  37. package/dist/components/comments.d.ts.map +1 -1
  38. package/dist/components/comments.js +1 -1
  39. package/dist/components/comments.js.map +1 -1
  40. package/dist/components/decision-form.d.ts +8 -1
  41. package/dist/components/decision-form.d.ts.map +1 -1
  42. package/dist/components/decision-form.js +57 -7
  43. package/dist/components/decision-form.js.map +1 -1
  44. package/dist/components/decision-list.d.ts +9 -1
  45. package/dist/components/decision-list.d.ts.map +1 -1
  46. package/dist/components/decision-list.js +53 -7
  47. package/dist/components/decision-list.js.map +1 -1
  48. package/dist/components/event-feed.d.ts.map +1 -1
  49. package/dist/components/event-feed.js +7 -1
  50. package/dist/components/event-feed.js.map +1 -1
  51. package/dist/components/goals.d.ts.map +1 -1
  52. package/dist/components/goals.js +2 -2
  53. package/dist/components/goals.js.map +1 -1
  54. package/dist/components/home.d.ts +35 -0
  55. package/dist/components/home.d.ts.map +1 -0
  56. package/dist/components/home.js +326 -0
  57. package/dist/components/home.js.map +1 -0
  58. package/dist/components/my-decisions.d.ts +2 -0
  59. package/dist/components/my-decisions.d.ts.map +1 -1
  60. package/dist/components/my-decisions.js +2 -2
  61. package/dist/components/my-decisions.js.map +1 -1
  62. package/dist/components/pack-connect.d.ts.map +1 -1
  63. package/dist/components/pack-connect.js +5 -4
  64. package/dist/components/pack-connect.js.map +1 -1
  65. package/dist/components/people.d.ts +2 -0
  66. package/dist/components/people.d.ts.map +1 -1
  67. package/dist/components/people.js +15 -2
  68. package/dist/components/people.js.map +1 -1
  69. package/dist/components/plugin-frame.d.ts +3 -12
  70. package/dist/components/plugin-frame.d.ts.map +1 -1
  71. package/dist/components/plugin-frame.js +53 -20
  72. package/dist/components/plugin-frame.js.map +1 -1
  73. package/dist/components/plugin-settings.d.ts.map +1 -1
  74. package/dist/components/plugin-settings.js +151 -26
  75. package/dist/components/plugin-settings.js.map +1 -1
  76. package/dist/components/projects.d.ts +0 -8
  77. package/dist/components/projects.d.ts.map +1 -1
  78. package/dist/components/projects.js +1 -13
  79. package/dist/components/projects.js.map +1 -1
  80. package/dist/components/task-view.d.ts +2 -1
  81. package/dist/components/task-view.d.ts.map +1 -1
  82. package/dist/components/task-view.js +2 -1
  83. package/dist/components/task-view.js.map +1 -1
  84. package/dist/components/ui.d.ts.map +1 -1
  85. package/dist/components/ui.js +5 -2
  86. package/dist/components/ui.js.map +1 -1
  87. package/dist/connect-page.d.ts.map +1 -1
  88. package/dist/connect-page.js +5 -6
  89. package/dist/connect-page.js.map +1 -1
  90. package/dist/console.css +764 -192
  91. package/dist/console.d.ts +10 -3
  92. package/dist/console.d.ts.map +1 -1
  93. package/dist/console.js +49 -28
  94. package/dist/console.js.map +1 -1
  95. package/dist/data.d.ts +6 -3
  96. package/dist/data.d.ts.map +1 -1
  97. package/dist/data.js +31 -7
  98. package/dist/data.js.map +1 -1
  99. package/dist/docs-page.d.ts.map +1 -1
  100. package/dist/docs-page.js +6 -5
  101. package/dist/docs-page.js.map +1 -1
  102. package/dist/home-tiles.d.ts +92 -0
  103. package/dist/home-tiles.d.ts.map +1 -0
  104. package/dist/home-tiles.js +294 -0
  105. package/dist/home-tiles.js.map +1 -0
  106. package/dist/i18n/en.d.ts +180 -0
  107. package/dist/i18n/en.d.ts.map +1 -1
  108. package/dist/i18n/en.js +181 -1
  109. package/dist/i18n/en.js.map +1 -1
  110. package/dist/i18n/index.d.ts +4 -2
  111. package/dist/i18n/index.d.ts.map +1 -1
  112. package/dist/i18n/index.js +10 -11
  113. package/dist/i18n/index.js.map +1 -1
  114. package/dist/i18n/ko.d.ts.map +1 -1
  115. package/dist/i18n/ko.js +176 -3
  116. package/dist/i18n/ko.js.map +1 -1
  117. package/dist/index.d.ts +4 -3
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +4 -3
  120. package/dist/index.js.map +1 -1
  121. package/dist/limits.d.ts +8 -0
  122. package/dist/limits.d.ts.map +1 -1
  123. package/dist/limits.js +8 -0
  124. package/dist/limits.js.map +1 -1
  125. package/dist/plugin/bridge.d.ts +44 -3
  126. package/dist/plugin/bridge.d.ts.map +1 -1
  127. package/dist/plugin/bridge.js +26 -0
  128. package/dist/plugin/bridge.js.map +1 -1
  129. package/dist/plugin/capabilities.d.ts +7 -1
  130. package/dist/plugin/capabilities.d.ts.map +1 -1
  131. package/dist/plugin/capabilities.js +14 -1
  132. package/dist/plugin/capabilities.js.map +1 -1
  133. package/dist/plugin/context.d.ts +24 -1
  134. package/dist/plugin/context.d.ts.map +1 -1
  135. package/dist/plugin/define.d.ts +1 -0
  136. package/dist/plugin/define.d.ts.map +1 -1
  137. package/dist/plugin/define.js +1 -1
  138. package/dist/plugin/define.js.map +1 -1
  139. package/dist/plugin/limits.d.ts +5 -2
  140. package/dist/plugin/limits.d.ts.map +1 -1
  141. package/dist/plugin/limits.js +5 -2
  142. package/dist/plugin/limits.js.map +1 -1
  143. package/dist/plugin/manifest.d.ts +83 -6
  144. package/dist/plugin/manifest.d.ts.map +1 -1
  145. package/dist/plugin/manifest.js +111 -7
  146. package/dist/plugin/manifest.js.map +1 -1
  147. package/dist/plugin/rest-schemas.d.ts +76 -0
  148. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  149. package/dist/plugin/rest-schemas.js +23 -2
  150. package/dist/plugin/rest-schemas.js.map +1 -1
  151. package/dist/plugin/sandbox.d.ts +4 -3
  152. package/dist/plugin/sandbox.d.ts.map +1 -1
  153. package/dist/plugin/sandbox.js +33 -6
  154. package/dist/plugin/sandbox.js.map +1 -1
  155. package/dist/plugin/semver.d.ts +26 -0
  156. package/dist/plugin/semver.d.ts.map +1 -0
  157. package/dist/plugin/semver.js +76 -0
  158. package/dist/plugin/semver.js.map +1 -0
  159. package/dist/plugin/testing.d.ts.map +1 -1
  160. package/dist/plugin/testing.js +2 -1
  161. package/dist/plugin/testing.js.map +1 -1
  162. package/dist/plugin/web.d.ts +10 -1
  163. package/dist/plugin/web.d.ts.map +1 -1
  164. package/dist/plugin/web.js +34 -0
  165. package/dist/plugin/web.js.map +1 -1
  166. package/dist/plugin.d.ts +3 -2
  167. package/dist/plugin.d.ts.map +1 -1
  168. package/dist/plugin.js +3 -2
  169. package/dist/plugin.js.map +1 -1
  170. package/dist/rules.d.ts +2 -2
  171. package/dist/rules.js +3 -3
  172. package/dist/schemas.d.ts +574 -11
  173. package/dist/schemas.d.ts.map +1 -1
  174. package/dist/schemas.js +154 -25
  175. package/dist/schemas.js.map +1 -1
  176. package/dist/use-action.d.ts +8 -2
  177. package/dist/use-action.d.ts.map +1 -1
  178. package/dist/use-action.js +13 -6
  179. package/dist/use-action.js.map +1 -1
  180. package/dist/vocabulary.d.ts +3 -1
  181. package/dist/vocabulary.d.ts.map +1 -1
  182. package/dist/vocabulary.js +3 -0
  183. package/dist/vocabulary.js.map +1 -1
  184. package/docs/specs/cli.md +4 -3
  185. package/docs/specs/plugins.md +49 -35
  186. package/docs/specs/rest.md +20 -18
  187. package/package.json +7 -7
  188. package/skills/building-a-plugin/SKILL.md +10 -9
  189. package/skills/planning-the-board/references/setting-up.md +2 -1
  190. package/skills/working-the-board/SKILL.md +3 -3
  191. package/src/api.ts +10 -0
  192. package/src/cli/cli.ts +162 -39
  193. package/src/cli/format.ts +54 -10
  194. package/src/cli/help.ts +23 -14
  195. package/src/cli/plugin-check.ts +68 -2
  196. package/src/cli/plugin-cli.ts +104 -6
  197. package/src/cli/plugin-init.ts +178 -3
  198. package/src/client.ts +36 -9
  199. package/src/compat.ts +1 -1
  200. package/src/components/board.tsx +3 -3
  201. package/src/components/comments.tsx +1 -0
  202. package/src/components/decision-form.tsx +166 -29
  203. package/src/components/decision-list.tsx +116 -15
  204. package/src/components/event-feed.tsx +7 -1
  205. package/src/components/goals.tsx +4 -1
  206. package/src/components/home.tsx +615 -0
  207. package/src/components/my-decisions.tsx +4 -1
  208. package/src/components/pack-connect.tsx +5 -4
  209. package/src/components/people.tsx +136 -54
  210. package/src/components/plugin-frame.tsx +62 -63
  211. package/src/components/plugin-settings.tsx +428 -87
  212. package/src/components/projects.tsx +0 -55
  213. package/src/components/task-view.tsx +9 -1
  214. package/src/components/ui.tsx +6 -2
  215. package/src/connect-page.tsx +5 -6
  216. package/src/console.tsx +98 -42
  217. package/src/data.ts +41 -11
  218. package/src/docs-page.tsx +6 -5
  219. package/src/home-tiles.ts +438 -0
  220. package/src/i18n/en.ts +185 -0
  221. package/src/i18n/index.ts +17 -14
  222. package/src/i18n/ko.ts +181 -3
  223. package/src/index.ts +7 -5
  224. package/src/limits.ts +8 -0
  225. package/src/plugin/bridge.ts +77 -1
  226. package/src/plugin/capabilities.ts +17 -1
  227. package/src/plugin/context.ts +24 -2
  228. package/src/plugin/define.ts +4 -1
  229. package/src/plugin/limits.ts +5 -2
  230. package/src/plugin/manifest.ts +154 -10
  231. package/src/plugin/rest-schemas.ts +27 -1
  232. package/src/plugin/sandbox.ts +36 -8
  233. package/src/plugin/semver.ts +89 -0
  234. package/src/plugin/testing.ts +2 -1
  235. package/src/plugin/web.ts +49 -0
  236. package/src/plugin.ts +18 -0
  237. package/src/rules.ts +3 -3
  238. package/src/schemas.ts +190 -28
  239. package/src/styles/console.css +764 -192
  240. package/src/use-action.ts +16 -7
  241. package/src/vocabulary.ts +4 -0
  242. package/template/plugin/console-plugin.json +3 -1
  243. package/template/plugin/package.json +3 -3
@@ -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
- export type BridgeToFrame = BridgeInit | BridgeResult | BridgeEvent | BridgeTheme | BridgePath;
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 (!capability.startsWith("outbound:") || !isPluginCapability(capability)) {
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);
@@ -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
- readonly options: readonly { readonly label: string }[];
259
- /** Who is to answer: a person who may work in the project; the project's owners when left out. */
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[]>;
@@ -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 (!manifest.capabilities.includes(capability)) {
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
  );
@@ -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 may grow, in pixels. */
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;
@@ -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.strictObject({
338
- key,
339
- name: localized(MAX_PLUGIN_NAME_LENGTH),
340
- place: z.enum(PLUGIN_WIDGET_PLACES),
341
- size: z.enum(PLUGIN_WIDGET_SIZES),
342
- roles: z.optional(roleList),
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(50)),
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
- for (const capability of manifest.capabilities) {
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(manifest.capabilities)) {
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 asks, named again: consent. */
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)),