@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.
Files changed (268) hide show
  1. package/README.md +6 -6
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +255 -33
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +20 -5
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +98 -26
  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 +39 -15
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/plugin-check.d.ts +5 -1
  17. package/dist/cli/plugin-check.d.ts.map +1 -1
  18. package/dist/cli/plugin-check.js +23 -4
  19. package/dist/cli/plugin-check.js.map +1 -1
  20. package/dist/cli/plugin-cli.d.ts.map +1 -1
  21. package/dist/cli/plugin-cli.js +10 -1
  22. package/dist/cli/plugin-cli.js.map +1 -1
  23. package/dist/cli/plugin-init.d.ts +2 -0
  24. package/dist/cli/plugin-init.d.ts.map +1 -1
  25. package/dist/cli/plugin-init.js +6 -0
  26. package/dist/cli/plugin-init.js.map +1 -1
  27. package/dist/client.d.ts +32 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +14 -2
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.js +1 -1
  32. package/dist/components/board.d.ts +8 -0
  33. package/dist/components/board.d.ts.map +1 -1
  34. package/dist/components/board.js +49 -18
  35. package/dist/components/board.js.map +1 -1
  36. package/dist/components/decision-form.d.ts +6 -3
  37. package/dist/components/decision-form.d.ts.map +1 -1
  38. package/dist/components/decision-form.js +24 -18
  39. package/dist/components/decision-form.js.map +1 -1
  40. package/dist/components/decision-list.d.ts +5 -1
  41. package/dist/components/decision-list.d.ts.map +1 -1
  42. package/dist/components/decision-list.js +25 -5
  43. package/dist/components/decision-list.js.map +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +34 -8
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/goals.d.ts.map +1 -1
  48. package/dist/components/goals.js +2 -2
  49. package/dist/components/goals.js.map +1 -1
  50. package/dist/components/home.d.ts +35 -0
  51. package/dist/components/home.d.ts.map +1 -0
  52. package/dist/components/home.js +326 -0
  53. package/dist/components/home.js.map +1 -0
  54. package/dist/components/my-decisions.d.ts +20 -3
  55. package/dist/components/my-decisions.d.ts.map +1 -1
  56. package/dist/components/my-decisions.js +14 -1
  57. package/dist/components/my-decisions.js.map +1 -1
  58. package/dist/components/pack-connect.d.ts.map +1 -1
  59. package/dist/components/pack-connect.js +5 -4
  60. package/dist/components/pack-connect.js.map +1 -1
  61. package/dist/components/people.d.ts +45 -1
  62. package/dist/components/people.d.ts.map +1 -1
  63. package/dist/components/people.js +125 -4
  64. package/dist/components/people.js.map +1 -1
  65. package/dist/components/picker.d.ts +8 -0
  66. package/dist/components/picker.d.ts.map +1 -1
  67. package/dist/components/picker.js +2 -2
  68. package/dist/components/picker.js.map +1 -1
  69. package/dist/components/plugin-frame.d.ts +3 -13
  70. package/dist/components/plugin-frame.d.ts.map +1 -1
  71. package/dist/components/plugin-frame.js +75 -21
  72. package/dist/components/plugin-frame.js.map +1 -1
  73. package/dist/components/plugin-settings.d.ts +5 -1
  74. package/dist/components/plugin-settings.d.ts.map +1 -1
  75. package/dist/components/plugin-settings.js +283 -45
  76. package/dist/components/plugin-settings.js.map +1 -1
  77. package/dist/components/projects.d.ts +0 -8
  78. package/dist/components/projects.d.ts.map +1 -1
  79. package/dist/components/projects.js +4 -14
  80. package/dist/components/projects.js.map +1 -1
  81. package/dist/components/searches.d.ts +11 -0
  82. package/dist/components/searches.d.ts.map +1 -0
  83. package/dist/components/searches.js +30 -0
  84. package/dist/components/searches.js.map +1 -0
  85. package/dist/components/task-form.d.ts.map +1 -1
  86. package/dist/components/task-form.js +6 -5
  87. package/dist/components/task-form.js.map +1 -1
  88. package/dist/components/task-view.d.ts.map +1 -1
  89. package/dist/components/task-view.js +2 -1
  90. package/dist/components/task-view.js.map +1 -1
  91. package/dist/components/ui.d.ts +16 -0
  92. package/dist/components/ui.d.ts.map +1 -1
  93. package/dist/components/ui.js +15 -2
  94. package/dist/components/ui.js.map +1 -1
  95. package/dist/connect-page.d.ts.map +1 -1
  96. package/dist/connect-page.js +5 -6
  97. package/dist/connect-page.js.map +1 -1
  98. package/dist/console.css +1123 -258
  99. package/dist/console.d.ts +12 -4
  100. package/dist/console.d.ts.map +1 -1
  101. package/dist/console.js +123 -27
  102. package/dist/console.js.map +1 -1
  103. package/dist/data.d.ts +18 -3
  104. package/dist/data.d.ts.map +1 -1
  105. package/dist/data.js +41 -1
  106. package/dist/data.js.map +1 -1
  107. package/dist/docs-page.d.ts.map +1 -1
  108. package/dist/docs-page.js +6 -5
  109. package/dist/docs-page.js.map +1 -1
  110. package/dist/home-tiles.d.ts +92 -0
  111. package/dist/home-tiles.d.ts.map +1 -0
  112. package/dist/home-tiles.js +294 -0
  113. package/dist/home-tiles.js.map +1 -0
  114. package/dist/i18n/en.d.ts +264 -1
  115. package/dist/i18n/en.d.ts.map +1 -1
  116. package/dist/i18n/en.js +260 -1
  117. package/dist/i18n/en.js.map +1 -1
  118. package/dist/i18n/index.d.ts +4 -2
  119. package/dist/i18n/index.d.ts.map +1 -1
  120. package/dist/i18n/index.js +10 -11
  121. package/dist/i18n/index.js.map +1 -1
  122. package/dist/i18n/ko.d.ts.map +1 -1
  123. package/dist/i18n/ko.js +255 -4
  124. package/dist/i18n/ko.js.map +1 -1
  125. package/dist/index.d.ts +7 -5
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +7 -5
  128. package/dist/index.js.map +1 -1
  129. package/dist/limits.d.ts +12 -0
  130. package/dist/limits.d.ts.map +1 -1
  131. package/dist/limits.js +12 -0
  132. package/dist/limits.js.map +1 -1
  133. package/dist/plugin/args.d.ts +6 -1
  134. package/dist/plugin/args.d.ts.map +1 -1
  135. package/dist/plugin/args.js +16 -11
  136. package/dist/plugin/args.js.map +1 -1
  137. package/dist/plugin/bridge.d.ts +63 -3
  138. package/dist/plugin/bridge.d.ts.map +1 -1
  139. package/dist/plugin/bridge.js +54 -0
  140. package/dist/plugin/bridge.js.map +1 -1
  141. package/dist/plugin/capabilities.d.ts +7 -1
  142. package/dist/plugin/capabilities.d.ts.map +1 -1
  143. package/dist/plugin/capabilities.js +24 -1
  144. package/dist/plugin/capabilities.js.map +1 -1
  145. package/dist/plugin/context.d.ts +174 -12
  146. package/dist/plugin/context.d.ts.map +1 -1
  147. package/dist/plugin/define.d.ts +14 -1
  148. package/dist/plugin/define.d.ts.map +1 -1
  149. package/dist/plugin/define.js +18 -2
  150. package/dist/plugin/define.js.map +1 -1
  151. package/dist/plugin/limits.d.ts +9 -2
  152. package/dist/plugin/limits.d.ts.map +1 -1
  153. package/dist/plugin/limits.js +9 -2
  154. package/dist/plugin/limits.js.map +1 -1
  155. package/dist/plugin/manifest.d.ts +98 -6
  156. package/dist/plugin/manifest.d.ts.map +1 -1
  157. package/dist/plugin/manifest.js +171 -10
  158. package/dist/plugin/manifest.js.map +1 -1
  159. package/dist/plugin/rest-schemas.d.ts +120 -0
  160. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  161. package/dist/plugin/rest-schemas.js +78 -5
  162. package/dist/plugin/rest-schemas.js.map +1 -1
  163. package/dist/plugin/sandbox.d.ts +4 -3
  164. package/dist/plugin/sandbox.d.ts.map +1 -1
  165. package/dist/plugin/sandbox.js +33 -6
  166. package/dist/plugin/sandbox.js.map +1 -1
  167. package/dist/plugin/semver.d.ts +26 -0
  168. package/dist/plugin/semver.d.ts.map +1 -0
  169. package/dist/plugin/semver.js +76 -0
  170. package/dist/plugin/semver.js.map +1 -0
  171. package/dist/plugin/testing.d.ts +15 -1
  172. package/dist/plugin/testing.d.ts.map +1 -1
  173. package/dist/plugin/testing.js +42 -5
  174. package/dist/plugin/testing.js.map +1 -1
  175. package/dist/plugin/web.d.ts +16 -1
  176. package/dist/plugin/web.d.ts.map +1 -1
  177. package/dist/plugin/web.js +44 -1
  178. package/dist/plugin/web.js.map +1 -1
  179. package/dist/plugin.d.ts +3 -2
  180. package/dist/plugin.d.ts.map +1 -1
  181. package/dist/plugin.js +3 -2
  182. package/dist/plugin.js.map +1 -1
  183. package/dist/router.d.ts +19 -0
  184. package/dist/router.d.ts.map +1 -1
  185. package/dist/router.js +18 -0
  186. package/dist/router.js.map +1 -1
  187. package/dist/routes.d.ts +11 -2
  188. package/dist/routes.d.ts.map +1 -1
  189. package/dist/routes.js +14 -1
  190. package/dist/routes.js.map +1 -1
  191. package/dist/rules.d.ts +2 -2
  192. package/dist/rules.js +3 -3
  193. package/dist/schemas.d.ts +864 -2
  194. package/dist/schemas.d.ts.map +1 -1
  195. package/dist/schemas.js +156 -9
  196. package/dist/schemas.js.map +1 -1
  197. package/dist/use-action.d.ts +8 -2
  198. package/dist/use-action.d.ts.map +1 -1
  199. package/dist/use-action.js +13 -6
  200. package/dist/use-action.js.map +1 -1
  201. package/dist/vocabulary.d.ts +3 -1
  202. package/dist/vocabulary.d.ts.map +1 -1
  203. package/dist/vocabulary.js +9 -0
  204. package/dist/vocabulary.js.map +1 -1
  205. package/docs/specs/cli.md +10 -6
  206. package/docs/specs/plugins.md +83 -62
  207. package/docs/specs/rest.md +34 -16
  208. package/package.json +1 -1
  209. package/skills/building-a-plugin/SKILL.md +15 -12
  210. package/skills/planning-the-board/references/setting-up.md +2 -1
  211. package/skills/working-the-board/SKILL.md +3 -3
  212. package/src/api.ts +23 -0
  213. package/src/cli/cli.ts +323 -30
  214. package/src/cli/format.ts +122 -26
  215. package/src/cli/help.ts +43 -14
  216. package/src/cli/plugin-check.ts +41 -2
  217. package/src/cli/plugin-cli.ts +10 -1
  218. package/src/cli/plugin-init.ts +8 -0
  219. package/src/client.ts +78 -0
  220. package/src/compat.ts +1 -1
  221. package/src/components/board.tsx +86 -45
  222. package/src/components/decision-form.tsx +73 -36
  223. package/src/components/decision-list.tsx +45 -13
  224. package/src/components/event-feed.tsx +40 -7
  225. package/src/components/goals.tsx +4 -1
  226. package/src/components/home.tsx +615 -0
  227. package/src/components/my-decisions.tsx +50 -5
  228. package/src/components/pack-connect.tsx +5 -4
  229. package/src/components/people.tsx +500 -61
  230. package/src/components/picker.tsx +11 -2
  231. package/src/components/plugin-frame.tsx +85 -63
  232. package/src/components/plugin-settings.tsx +735 -185
  233. package/src/components/projects.tsx +14 -69
  234. package/src/components/searches.ts +47 -0
  235. package/src/components/task-form.tsx +23 -28
  236. package/src/components/task-view.tsx +7 -0
  237. package/src/components/ui.tsx +42 -2
  238. package/src/connect-page.tsx +5 -6
  239. package/src/console.tsx +303 -64
  240. package/src/data.ts +62 -3
  241. package/src/docs-page.tsx +6 -5
  242. package/src/home-tiles.ts +438 -0
  243. package/src/i18n/en.ts +274 -0
  244. package/src/i18n/index.ts +17 -14
  245. package/src/i18n/ko.ts +267 -4
  246. package/src/index.ts +18 -5
  247. package/src/limits.ts +12 -0
  248. package/src/plugin/args.ts +16 -11
  249. package/src/plugin/bridge.ts +122 -1
  250. package/src/plugin/capabilities.ts +28 -1
  251. package/src/plugin/context.ts +186 -9
  252. package/src/plugin/define.ts +38 -1
  253. package/src/plugin/limits.ts +9 -2
  254. package/src/plugin/manifest.ts +228 -13
  255. package/src/plugin/rest-schemas.ts +89 -6
  256. package/src/plugin/sandbox.ts +36 -8
  257. package/src/plugin/semver.ts +89 -0
  258. package/src/plugin/testing.ts +63 -6
  259. package/src/plugin/web.ts +66 -0
  260. package/src/plugin.ts +19 -0
  261. package/src/router.ts +25 -0
  262. package/src/routes.ts +17 -2
  263. package/src/rules.ts +3 -3
  264. package/src/schemas.ts +197 -10
  265. package/src/styles/console.css +1123 -258
  266. package/src/use-action.ts +16 -7
  267. package/src/vocabulary.ts +10 -0
  268. package/template/plugin/console-plugin.json +3 -1
@@ -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;
@@ -121,19 +160,41 @@ export interface BridgeDownload {
121
160
  readonly id: string;
122
161
  }
123
162
 
163
+ /**
164
+ * A link a person pressed in the frame, for the console's page to open in a new tab (ADR-0067):
165
+ * the frame's sandbox opens no window itself, and the page opens one only for a web or a mail
166
+ * address, and only right after a person pressed.
167
+ */
168
+ export interface BridgeOpen {
169
+ readonly v: 1;
170
+ readonly t: "open";
171
+ readonly url: string;
172
+ }
173
+
124
174
  export interface BridgeSubscribe {
125
175
  readonly v: 1;
126
176
  readonly t: "subscribe";
127
177
  readonly kinds: readonly BridgeEventKind[];
128
178
  }
129
179
 
180
+ /**
181
+ * The frame has nothing to show: the plugin is off, or the page is not one of its own. The console
182
+ * serves such a frame a document with no words, and the console's page says so in its own.
183
+ */
184
+ export interface BridgeUnavailable {
185
+ readonly v: 1;
186
+ readonly t: "unavailable";
187
+ }
188
+
130
189
  export type BridgeToPage =
131
190
  | BridgeReady
191
+ | BridgeUnavailable
132
192
  | BridgeCall
133
193
  | BridgeNavigate
134
194
  | BridgeResize
135
195
  | BridgeTitle
136
196
  | BridgeDownload
197
+ | BridgeOpen
137
198
  | BridgeSubscribe;
138
199
 
139
200
  /** A path of the console's, as `navigate` and `path` carry one: absolute, no scheme, no host, one line. */
@@ -163,6 +224,37 @@ export function isBridgePath(value: unknown): value is string {
163
224
  );
164
225
  }
165
226
 
227
+ /** The schemes a link pressed in a frame may open in a new tab: the web, and mail. Nothing that runs. */
228
+ const OPENABLE_SCHEMES: ReadonlySet<string> = new Set(["https:", "http:", "mailto:"]);
229
+ export const MAX_BRIDGE_URL_LENGTH = 2048;
230
+
231
+ /** True for an address the page may open in a new tab for a frame: an absolute web or mail address, one line. */
232
+ export function isBridgeUrl(value: unknown): value is string {
233
+ if (
234
+ typeof value !== "string" ||
235
+ value.length === 0 ||
236
+ value.length > MAX_BRIDGE_URL_LENGTH ||
237
+ /\s/.test(value) ||
238
+ [...value].some((character) => {
239
+ const code = character.codePointAt(0) ?? 0;
240
+ return code < 0x20 || code === 0x7f;
241
+ }) ||
242
+ !URL.canParse(value)
243
+ ) {
244
+ return false;
245
+ }
246
+ return OPENABLE_SCHEMES.has(new URL(value).protocol);
247
+ }
248
+
249
+ /**
250
+ * True for an address the page opens for a frame (ADR-0067): a web or a mail address that is not
251
+ * the console's own, given as `origin`, since a page of the console's is `navigate`'s, in the
252
+ * person's tab.
253
+ */
254
+ export function isOpenableFrom(value: unknown, origin: string): value is string {
255
+ return isBridgeUrl(value) && new URL(value).origin !== origin;
256
+ }
257
+
166
258
  function isTheme(value: unknown): value is Record<string, string> {
167
259
  if (!isRecord(value)) {
168
260
  return false;
@@ -177,6 +269,21 @@ function isTheme(value: unknown): value is Record<string, string> {
177
269
  );
178
270
  }
179
271
 
272
+ function isWorkspace(value: unknown): boolean {
273
+ return (
274
+ isRecord(value) &&
275
+ typeof value.name === "string" &&
276
+ Array.isArray(value.languages) &&
277
+ value.languages.every((language) => typeof language === "string") &&
278
+ isLine(value.timeZone, 100) &&
279
+ (WEEK_STARTS as readonly unknown[]).includes(value.weekStart) &&
280
+ typeof value.firstDay === "number" &&
281
+ Number.isInteger(value.firstDay) &&
282
+ value.firstDay >= 1 &&
283
+ value.firstDay <= 7
284
+ );
285
+ }
286
+
180
287
  function isEnvelope(value: unknown): value is Record<string, unknown> & { t: string } {
181
288
  return isRecord(value) && value.v === PLUGIN_BRIDGE_VERSION && typeof value.t === "string";
182
289
  }
@@ -194,6 +301,8 @@ export function isBridgeToFrame(value: unknown): value is BridgeToFrame {
194
301
  isLine(value.plugin.version, 64) &&
195
302
  (value.page === undefined || isLine(value.page, 32)) &&
196
303
  (value.widget === undefined || isLine(value.widget, 32)) &&
304
+ (value.size === undefined ||
305
+ (PLUGIN_WIDGET_SIZE_NAMES as readonly unknown[]).includes(value.size)) &&
197
306
  (value.project === null ||
198
307
  (isRecord(value.project) &&
199
308
  isLine(value.project.key, 64) &&
@@ -204,6 +313,7 @@ export function isBridgeToFrame(value: unknown): value is BridgeToFrame {
204
313
  (value.person.name === null || typeof value.person.name === "string") &&
205
314
  typeof value.person.language === "string" &&
206
315
  typeof value.person.admin === "boolean" &&
316
+ (value.workspace === undefined || isWorkspace(value.workspace)) &&
207
317
  Array.isArray(value.roles) &&
208
318
  value.roles.every((role) => typeof role === "string") &&
209
319
  typeof value.path === "string" &&
@@ -229,6 +339,8 @@ export function isBridgeToFrame(value: unknown): value is BridgeToFrame {
229
339
  return isTheme(value.theme);
230
340
  case "path":
231
341
  return typeof value.path === "string" && value.path.length <= MAX_BRIDGE_PATH_LENGTH;
342
+ case "size":
343
+ return (PLUGIN_WIDGET_SIZE_NAMES as readonly unknown[]).includes(value.size);
232
344
  default:
233
345
  return false;
234
346
  }
@@ -241,6 +353,7 @@ export function isBridgeToPage(value: unknown): value is BridgeToPage {
241
353
  }
242
354
  switch (value.t) {
243
355
  case "ready":
356
+ case "unavailable":
244
357
  return true;
245
358
  case "call":
246
359
  return (
@@ -261,6 +374,8 @@ export function isBridgeToPage(value: unknown): value is BridgeToPage {
261
374
  );
262
375
  case "download":
263
376
  return isLine(value.id, MAX_ID_LENGTH);
377
+ case "open":
378
+ return isBridgeUrl(value.url);
264
379
  case "subscribe":
265
380
  return (
266
381
  Array.isArray(value.kinds) &&
@@ -307,6 +422,12 @@ export interface BridgeFilesPutParams {
307
422
  }
308
423
 
309
424
  /** What `files.get` answers: the bytes, transferred, with the file's name and type. */
425
+ /** A person's picture, as the page hands it to the frame (ADR-0063). */
426
+ export interface BridgePicture {
427
+ readonly type: string;
428
+ readonly bytes: ArrayBuffer;
429
+ }
430
+
310
431
  export interface BridgeFile {
311
432
  readonly name: string;
312
433
  readonly type: string;
@@ -8,6 +8,8 @@ export const PLUGIN_CAPABILITIES = [
8
8
  "files",
9
9
  "people:read",
10
10
  "people:read:all",
11
+ "people:read:email",
12
+ "people:act",
11
13
  "projects:read",
12
14
  "projects:read:all",
13
15
  "board:read",
@@ -19,10 +21,18 @@ export const PLUGIN_CAPABILITIES = [
19
21
  "jobs",
20
22
  "endpoints",
21
23
  "storage:sql",
24
+ "outbound:webrtc",
22
25
  ] as const;
23
26
 
24
27
  export type PluginCapability = (typeof PLUGIN_CAPABILITIES)[number];
25
28
 
29
+ /**
30
+ * WebRTC on a plugin's pages (ADR-0066). It stands with `outbound:<host>` as a way to the internet,
31
+ * and it is no host: a connection goes from a person's browser to any address a page names, which
32
+ * no host could bound, so that consent to a host does not open it, and it opens no host.
33
+ */
34
+ export const OUTBOUND_WEBRTC: PluginCapability = "outbound:webrtc";
35
+
26
36
  /** The capabilities that carry a parameter: a host to reach, a plugin to call. */
27
37
  export const PLUGIN_CAPABILITY_PREFIXES = ["outbound:", "plugins:"] as const;
28
38
 
@@ -52,6 +62,15 @@ export const CAPABILITY_WORDS: Readonly<Record<PluginCapability, CapabilityWords
52
62
  opens: "everyone in the workspace, for its jobs and hooks too",
53
63
  reach: "the directory",
54
64
  },
65
+ "people:read:email": {
66
+ opens: "the work email of the people it may read, to match them to accounts elsewhere",
67
+ reach: "the addresses of the people its role holders may see",
68
+ },
69
+ "people:act": {
70
+ opens:
71
+ "acting for a person who did something elsewhere: a command run and a decision answered as them, within what they may do, recorded as theirs through the plugin",
72
+ reach: "what its role holders may do, done in their name from elsewhere",
73
+ },
55
74
  "projects:read": {
56
75
  opens: "the projects and members the caller may see",
57
76
  reach: "what the person sees",
@@ -97,6 +116,10 @@ export const CAPABILITY_WORDS: Readonly<Record<PluginCapability, CapabilityWords
97
116
  opens: "a schema and a login role of its own in the console's database",
98
117
  reach: "statements up to the timeout, within its schema",
99
118
  },
119
+ "outbound:webrtc": {
120
+ opens: "WebRTC on its pages: connections from each person's browser to any address",
121
+ reach: "what its pages are shown, sent anywhere from the person's browser; their address",
122
+ },
100
123
  };
101
124
 
102
125
  /** True for a capability the catalogue knows, parameter included. */
@@ -137,7 +160,11 @@ export function capabilityWords(capability: string): CapabilityWords | undefined
137
160
 
138
161
  /** True when the request's host is within what `outbound:<host>` names: the host itself, or a subdomain where the capability starts with `*.`. */
139
162
  export function outboundAllows(capability: string, host: string): boolean {
140
- if (!capability.startsWith("outbound:") || !isPluginCapability(capability)) {
163
+ if (
164
+ !capability.startsWith("outbound:") ||
165
+ capability === OUTBOUND_WEBRTC ||
166
+ !isPluginCapability(capability)
167
+ ) {
141
168
  return false;
142
169
  }
143
170
  const named = capability.slice("outbound:".length);
@@ -15,7 +15,9 @@ import type {
15
15
  RestTask,
16
16
  RestTaskInput,
17
17
  RestTaskPatch,
18
+ RestTeam,
18
19
  } from "../schemas.js";
20
+ import type { WeekStart } from "../vocabulary.js";
19
21
  import type { PluginFileHandle } from "./args.js";
20
22
 
21
23
  /** 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. */
@@ -40,6 +42,16 @@ export type RecordSchemaResult<T> =
40
42
  /** The data a record of a collection holds, from its schema. */
41
43
  export type RecordData<S> = S extends RecordSchema<infer T> ? T : never;
42
44
 
45
+ /**
46
+ * How long a collection keeps a record: `days` after the moment `by` names, `createdAt` or
47
+ * `updatedAt` of the record, or an indexed field of its data that holds a day or an instant as
48
+ * ISO 8601 text; then the console removes the record, with its versions, links and files.
49
+ */
50
+ export interface CollectionRetention {
51
+ readonly days: number;
52
+ readonly by: string;
53
+ }
54
+
43
55
  /** A collection as `defineCollection` yields it: the name, the schema, and what the console indexes and who reads. */
44
56
  export interface CollectionDefinition<S extends RecordSchema = RecordSchema> {
45
57
  readonly name: string;
@@ -51,6 +63,8 @@ export interface CollectionDefinition<S extends RecordSchema = RecordSchema> {
51
63
  readonly read: readonly string[];
52
64
  readonly readOwn: readonly string[];
53
65
  readonly history: boolean;
66
+ /** How long a record is kept, or `null` for as long as the plugin is. */
67
+ readonly retention: CollectionRetention | null;
54
68
  }
55
69
 
56
70
  export interface PluginRecord<T = Record<string, unknown>> {
@@ -199,10 +213,23 @@ export interface PluginInfo {
199
213
  readonly dataVersion: number;
200
214
  /** The settings as the administrator set them, by key, without the secrets. */
201
215
  readonly settings: Readonly<Record<string, unknown>>;
216
+ /**
217
+ * The capabilities the plugin holds: those it requires, and the optional ones an administrator
218
+ * turned on (ADR-0060), so that it offers what it can do. One it does not hold is refused.
219
+ */
220
+ readonly capabilities: readonly string[];
202
221
  /** The board project, or none. */
203
222
  readonly board: { readonly id: string; readonly key: string } | null;
204
223
  /** The projects the plugin is on: all, or their keys. */
205
224
  readonly projects: "all" | readonly string[];
225
+ /** The roles other plugins granted to this one (ADR-0072), by their key, so that a plugin says what it lacks. */
226
+ readonly grants: readonly PluginGrantHeld[];
227
+ }
228
+
229
+ /** A role of another plugin, held by this one. */
230
+ export interface PluginGrantHeld {
231
+ readonly plugin: string;
232
+ readonly role: string;
206
233
  }
207
234
 
208
235
  export interface CallerPerson {
@@ -239,12 +266,43 @@ export interface ConsoleCaller {
239
266
  readonly project: CallerProject | null;
240
267
  }
241
268
 
242
- export type PluginCaller = PersonCaller | ConsoleCaller;
269
+ /**
270
+ * Another plugin, calling with the roles granted to it (ADR-0072): from its own job, hook or
271
+ * webhook, as itself; or for a person who did something elsewhere, with `for`, and then with the
272
+ * roles the plugin and the person both hold, in the person's language. A plugin that knows only
273
+ * `person` and `console` refuses the kind, which fails safe.
274
+ */
275
+ export interface PluginAsCaller {
276
+ readonly kind: "plugin";
277
+ /** The calling plugin's key. */
278
+ readonly plugin: string;
279
+ readonly roles: readonly string[];
280
+ /** The person the plugin acts for, or `null` when it acts as itself; never an administrator by that. */
281
+ readonly for: CallerPerson | null;
282
+ /** The project the call is on, if any: the one the person's own call was on. */
283
+ readonly project: CallerProject | null;
284
+ }
285
+
286
+ export type PluginCaller = PersonCaller | ConsoleCaller | PluginAsCaller;
243
287
 
244
288
  export interface PluginWorkspace {
245
289
  readonly name: string;
246
290
  readonly languages: readonly string[];
291
+ /** The zone the workspace's times of day are read in; `UTC` where none is set, as `timeZoneSet` says. */
247
292
  readonly timeZone: string;
293
+ /**
294
+ * Whether an administrator set the time zone: `false` where the workspace runs on UTC because
295
+ * nobody said otherwise, so that a plugin that reads a time of day says so, instead of taking
296
+ * UTC for a choice.
297
+ */
298
+ readonly timeZoneSet: boolean;
299
+ /** The day the week begins on (ADR-0035): `auto` leaves it to the language of whoever reads. */
300
+ readonly weekStart: WeekStart;
301
+ /**
302
+ * The console's public origin (`PUBLIC_URL`), for a link to the console in what a plugin
303
+ * writes elsewhere; `null` where the deployment names none.
304
+ */
305
+ readonly url: string | null;
248
306
  readonly categories: readonly {
249
307
  readonly key: string;
250
308
  readonly name: string;
@@ -252,7 +310,15 @@ export interface PluginWorkspace {
252
310
  }[];
253
311
  }
254
312
 
313
+ /** An answer given for a person (ADR-0072): the option by its key or by its label, one of the two, and a note. */
314
+ export interface DecisionAnswer {
315
+ readonly optionKey?: string;
316
+ readonly option?: string;
317
+ readonly note?: string;
318
+ }
319
+
255
320
  export interface DecisionAsk {
321
+ /** In the language of whoever is to answer, as `people.get` says it (ADR-0065). */
256
322
  readonly question: string;
257
323
  readonly body?: string;
258
324
  /**
@@ -261,8 +327,10 @@ export interface DecisionAsk {
261
327
  * without comparing labels.
262
328
  */
263
329
  readonly options: readonly { readonly label: string; readonly key?: string }[];
264
- /** Who is to answer: a person who may work in the project; the project's owners when left out. */
330
+ /** Who is to answer: a person who may work in the project; the administrator who enabled the plugin when left out. */
265
331
  readonly deciderId?: string;
332
+ /** Several to answer (ADR-0057), in place of `deciderId`: any of them may, and the first answer ends it; up to MAX_DECIDERS. */
333
+ readonly deciderIds?: readonly string[];
266
334
  readonly taskId?: string;
267
335
  /** The record the decision is for, by which the plugin maps the answer back. */
268
336
  readonly about: { readonly collection: string; readonly id: string };
@@ -274,6 +342,19 @@ export interface PluginConsole {
274
342
  people: {
275
343
  list(options?: { readonly q?: string }): Promise<readonly RestPerson[]>;
276
344
  get(id: string): Promise<RestPerson | null>;
345
+ /**
346
+ * The active people a role of the plugin was granted to, by name, through a project or with
347
+ * everyone, and everyone for `defaultRole` (ADR-0063); for a role that follows the reporting
348
+ * line, whoever manages someone besides (ADR-0068). An administrator holds every role, and is
349
+ * listed only where they were granted it or manage someone.
350
+ */
351
+ granted(role: string): Promise<readonly RestPerson[]>;
352
+ };
353
+ /** The teams of the workspace (ADR-0073): a name and its members, among the people the caller may see. */
354
+ teams: {
355
+ list(): Promise<readonly RestTeam[]>;
356
+ /** One team by its key (ADR-0074), what a grant and a person's `teams` name it by. */
357
+ get(key: string): Promise<RestTeam | null>;
277
358
  };
278
359
  projects: {
279
360
  list(): Promise<readonly RestProject[]>;
@@ -291,6 +372,14 @@ export interface PluginConsole {
291
372
  list(project: string): Promise<readonly RestDecision[]>;
292
373
  get(id: string): Promise<RestDecision | null>;
293
374
  ask(project: string, decision: DecisionAsk): Promise<RestDecision>;
375
+ /**
376
+ * Answers a decision for a person who answered elsewhere (ADR-0072), with `people:act`: one on
377
+ * the person's own plate, addressed to them or a plugin's addressed to someone they manage, up
378
+ * the reporting line (ADR-0068), and `plugin.forbidden` for any other, whatever they
379
+ * administer or are a member of. The answer is theirs, through this plugin. A plugin answers
380
+ * no decision itself.
381
+ */
382
+ answer(id: string, answer: DecisionAnswer, as: { readonly for: string }): Promise<RestDecision>;
294
383
  };
295
384
  documents: {
296
385
  list(project: string, folder?: string): Promise<readonly RestDocumentSummary[]>;
@@ -325,28 +414,90 @@ export interface PluginSql extends PluginSqlClient {
325
414
  transaction<T>(work: (tx: PluginSqlClient) => Promise<T>): Promise<T>;
326
415
  }
327
416
 
328
- /** Where the console puts a secret into an outbound request. */
329
- export type SecretPlacement = `header:${string}` | `query:${string}` | "basic";
417
+ /**
418
+ * Where the console puts a secret into an outbound request: a header, as `Authorization: Bearer`,
419
+ * a query parameter, as basic authentication (`user:password`), or a field of a JSON or a form
420
+ * body.
421
+ */
422
+ export type SecretPlacement =
423
+ | `header:${string}`
424
+ | "bearer"
425
+ | `query:${string}`
426
+ | "basic"
427
+ | `body:${string}`;
428
+
429
+ /** One secret setting and where it goes. */
430
+ export interface SecretUse {
431
+ readonly name: string;
432
+ readonly as: SecretPlacement;
433
+ }
434
+
435
+ /**
436
+ * A value of the answer to seal as a secret setting (ADR-0070): the path to it in the answer's
437
+ * JSON (`$.access_token`, `$.credentials.signing_secret`, `$.items.0.token`), and, where it
438
+ * expires, how long it lasts, in seconds or as a path to the seconds (`$.expires_in`). The
439
+ * console seals the value and hands the plugin the answer with `"[sealed]"` in its place.
440
+ */
441
+ export type SecretSeal =
442
+ | string
443
+ | { readonly path: string; readonly expiresIn?: number | string | undefined };
444
+
445
+ /**
446
+ * What became of a value `seal` named (ADR-0070). `sealed`: sealed as the setting, the answer
447
+ * saying `"[sealed]"` in its place. `absent`: nothing at the path (no such key, `null` or an
448
+ * empty string); nothing sealed, nothing taken out. `not_a_string`, `too_long` and `echoed`: a
449
+ * value that is no string, is over the bound of a secret (4,096 characters), or holds a secret
450
+ * the request carried from another setting; never sealed and never handed over, the answer saying
451
+ * `"[withheld]"` in its place. `not_json`: an answer that is no JSON object or array, whose body
452
+ * is withheld whole. Only `sealed` changed the setting.
453
+ */
454
+ export type SealOutcome = "sealed" | "absent" | "not_a_string" | "too_long" | "echoed" | "not_json";
330
455
 
331
456
  export interface PluginFetchInit {
457
+ /** `GET` unless said; `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` or `OPTIONS`, and no other. */
332
458
  readonly method?: string;
333
459
  readonly headers?: Readonly<Record<string, string>>;
334
460
  readonly body?: string | Uint8Array;
335
- /** A secret setting, put where `as` says: a header, a query parameter, or as basic authentication (`user:password`). */
336
- readonly secret?: { readonly name: string; readonly as: SecretPlacement };
337
- }
338
-
339
- /** An outbound answer, whole: the host reads it to the bound before handing it over. */
461
+ /** A secret setting, put where `as` says. */
462
+ readonly secret?: SecretUse;
463
+ /** Several, where a request carries more than one: a client secret in the body and a token as a bearer, say. */
464
+ readonly secrets?: readonly SecretUse[];
465
+ /** Values of the answer to seal as secret settings, by the setting's name, never handed to the plugin. */
466
+ readonly seal?: Readonly<Record<string, SecretSeal>>;
467
+ }
468
+
469
+ /**
470
+ * An outbound answer, whole: the host reads it to the bound before handing it over, a redirect as
471
+ * it is, never followed. Every form of a secret the request carried, and of a value sealed or
472
+ * withheld, is taken out of its headers and its body first, `"[withheld]"` in its place.
473
+ */
340
474
  export interface PluginFetchResponse {
341
475
  readonly status: number;
342
476
  readonly headers: Readonly<Record<string, string>>;
343
477
  readonly bytes: Uint8Array;
478
+ /** What became of each value `seal` named, by the setting's name; there where the request named `seal`. */
479
+ readonly sealed?: Readonly<Record<string, SealOutcome>>;
344
480
  text(): string;
345
481
  json(): unknown;
346
482
  }
347
483
 
484
+ /** How a secret stands: whether it is set and not expired, when it expires, and where it came from. */
485
+ export interface SecretStatus {
486
+ /** Set, and not past its expiry. */
487
+ readonly set: boolean;
488
+ readonly setAt: string | null;
489
+ /** When it expires, or `null` for a secret that does not. */
490
+ readonly expiresAt: string | null;
491
+ /** `administrator` for one an administrator set; the host of the answer it was sealed from otherwise; `null` while unset. */
492
+ readonly source: string | null;
493
+ }
494
+
348
495
  export interface PluginSecrets {
496
+ /** Whether the secret is set and not past its expiry. */
349
497
  has(name: string): Promise<boolean>;
498
+ /** How the secret stands, for a settings page to say and a job to decide whether to renew it. */
499
+ status(name: string): Promise<SecretStatus>;
500
+ /** Signs with the secret; refused for a secret declared `use: "verify"`, which signs nothing. */
350
501
  sign(name: string, payload: string | Uint8Array, algorithm: "hmac-sha256"): Promise<string>;
351
502
  verify(
352
503
  name: string,
@@ -359,6 +510,23 @@ export interface PluginSecrets {
359
510
  export interface OtherPlugin {
360
511
  readonly version: string;
361
512
  readonly api: Readonly<Record<string, (input: unknown) => Promise<unknown>>>;
513
+ /**
514
+ * The same plugin, called for a person who did something elsewhere (ADR-0072): with the roles
515
+ * this plugin and the person both hold in it, in the person's language, what it reads of the
516
+ * console the person's as a member's, whatever they administer, recorded as theirs through this
517
+ * plugin. Needs `people:act`; the person is active, or the call is `plugin.not_found`.
518
+ */
519
+ for(personId: string): OtherPlugin;
520
+ }
521
+
522
+ /** The plugin's own endpoints, as a service elsewhere is told to call them. */
523
+ export interface PluginEndpoints {
524
+ /**
525
+ * The full address of an endpoint the plugin defines, under the console's public origin, to
526
+ * register a callback or a webhook with a service elsewhere; `plugin.not_found` for a name the
527
+ * plugin does not define, `plugin.unavailable` where the console has no public address.
528
+ */
529
+ url(name: string): Promise<string>;
362
530
  }
363
531
 
364
532
  export interface PluginLog {
@@ -382,6 +550,7 @@ export interface PluginContext {
382
550
  fetch(url: string, init?: PluginFetchInit): Promise<PluginFetchResponse>;
383
551
  readonly secrets: PluginSecrets;
384
552
  readonly plugins: { get(key: string): Promise<OtherPlugin> };
553
+ readonly endpoints: PluginEndpoints;
385
554
  readonly log: PluginLog;
386
555
  now(): Date;
387
556
  /** The caller's language, `en` for the console. */
@@ -393,6 +562,14 @@ export interface PluginContext {
393
562
  /** An event a hook receives: the console's, of the plugin's board projects, or another plugin's. */
394
563
  export interface PluginHookEvent {
395
564
  readonly source: "console" | "plugin";
565
+ /**
566
+ * The event's id, one per event across both streams (`console:<number>` or `plugin:<number>`):
567
+ * delivery is at least once, so a hook that must not act twice keeps the ids it acted on and
568
+ * passes over one it has seen.
569
+ */
570
+ readonly id: string;
571
+ /** Which delivery of the event this is: 1 the first time, more after a failure and its retries. */
572
+ readonly attempt: number;
396
573
  /** `console:<kind>` or `plugin:<key>:<kind>`, as the hook was named. */
397
574
  readonly kind: string;
398
575
  readonly at: string;