@skillcdn/console 0.4.4 → 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 (192) hide show
  1. package/README.md +3 -3
  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 +162 -15
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +16 -3
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +68 -19
  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 +26 -5
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/client.d.ts +28 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +12 -2
  19. package/dist/client.js.map +1 -1
  20. package/dist/compat.js +1 -1
  21. package/dist/components/board.d.ts.map +1 -1
  22. package/dist/components/board.js +46 -15
  23. package/dist/components/board.js.map +1 -1
  24. package/dist/components/decision-form.d.ts +0 -3
  25. package/dist/components/decision-form.d.ts.map +1 -1
  26. package/dist/components/decision-form.js +3 -15
  27. package/dist/components/decision-form.js.map +1 -1
  28. package/dist/components/event-feed.d.ts.map +1 -1
  29. package/dist/components/event-feed.js +30 -10
  30. package/dist/components/event-feed.js.map +1 -1
  31. package/dist/components/my-decisions.d.ts +20 -3
  32. package/dist/components/my-decisions.d.ts.map +1 -1
  33. package/dist/components/my-decisions.js +14 -1
  34. package/dist/components/my-decisions.js.map +1 -1
  35. package/dist/components/people.d.ts +43 -1
  36. package/dist/components/people.d.ts.map +1 -1
  37. package/dist/components/people.js +117 -9
  38. package/dist/components/people.js.map +1 -1
  39. package/dist/components/picker.d.ts +8 -0
  40. package/dist/components/picker.d.ts.map +1 -1
  41. package/dist/components/picker.js +2 -2
  42. package/dist/components/picker.js.map +1 -1
  43. package/dist/components/plugin-frame.d.ts +0 -1
  44. package/dist/components/plugin-frame.d.ts.map +1 -1
  45. package/dist/components/plugin-frame.js +22 -1
  46. package/dist/components/plugin-frame.js.map +1 -1
  47. package/dist/components/plugin-settings.d.ts +5 -1
  48. package/dist/components/plugin-settings.d.ts.map +1 -1
  49. package/dist/components/plugin-settings.js +143 -30
  50. package/dist/components/plugin-settings.js.map +1 -1
  51. package/dist/components/projects.d.ts.map +1 -1
  52. package/dist/components/projects.js +3 -1
  53. package/dist/components/projects.js.map +1 -1
  54. package/dist/components/searches.d.ts +11 -0
  55. package/dist/components/searches.d.ts.map +1 -0
  56. package/dist/components/searches.js +30 -0
  57. package/dist/components/searches.js.map +1 -0
  58. package/dist/components/task-form.d.ts.map +1 -1
  59. package/dist/components/task-form.js +6 -5
  60. package/dist/components/task-form.js.map +1 -1
  61. package/dist/components/ui.d.ts +16 -0
  62. package/dist/components/ui.d.ts.map +1 -1
  63. package/dist/components/ui.js +10 -0
  64. package/dist/components/ui.js.map +1 -1
  65. package/dist/console.css +362 -67
  66. package/dist/console.d.ts +2 -1
  67. package/dist/console.d.ts.map +1 -1
  68. package/dist/console.js +90 -14
  69. package/dist/console.js.map +1 -1
  70. package/dist/data.d.ts +14 -1
  71. package/dist/data.d.ts.map +1 -1
  72. package/dist/data.js +36 -1
  73. package/dist/data.js.map +1 -1
  74. package/dist/i18n/en.d.ts +89 -0
  75. package/dist/i18n/en.d.ts.map +1 -1
  76. package/dist/i18n/en.js +87 -2
  77. package/dist/i18n/en.js.map +1 -1
  78. package/dist/i18n/ko.d.ts.map +1 -1
  79. package/dist/i18n/ko.js +87 -3
  80. package/dist/i18n/ko.js.map +1 -1
  81. package/dist/index.d.ts +3 -2
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +3 -2
  84. package/dist/index.js.map +1 -1
  85. package/dist/limits.d.ts +6 -0
  86. package/dist/limits.d.ts.map +1 -1
  87. package/dist/limits.js +6 -0
  88. package/dist/limits.js.map +1 -1
  89. package/dist/plugin/args.d.ts +6 -1
  90. package/dist/plugin/args.d.ts.map +1 -1
  91. package/dist/plugin/args.js +16 -11
  92. package/dist/plugin/args.js.map +1 -1
  93. package/dist/plugin/bridge.d.ts +20 -1
  94. package/dist/plugin/bridge.d.ts.map +1 -1
  95. package/dist/plugin/bridge.js +28 -0
  96. package/dist/plugin/bridge.js.map +1 -1
  97. package/dist/plugin/capabilities.d.ts +1 -1
  98. package/dist/plugin/capabilities.d.ts.map +1 -1
  99. package/dist/plugin/capabilities.js +10 -0
  100. package/dist/plugin/capabilities.js.map +1 -1
  101. package/dist/plugin/context.d.ts +158 -13
  102. package/dist/plugin/context.d.ts.map +1 -1
  103. package/dist/plugin/define.d.ts +13 -1
  104. package/dist/plugin/define.d.ts.map +1 -1
  105. package/dist/plugin/define.js +17 -1
  106. package/dist/plugin/define.js.map +1 -1
  107. package/dist/plugin/limits.d.ts +4 -0
  108. package/dist/plugin/limits.d.ts.map +1 -1
  109. package/dist/plugin/limits.js +4 -0
  110. package/dist/plugin/limits.js.map +1 -1
  111. package/dist/plugin/manifest.d.ts +15 -0
  112. package/dist/plugin/manifest.d.ts.map +1 -1
  113. package/dist/plugin/manifest.js +61 -4
  114. package/dist/plugin/manifest.js.map +1 -1
  115. package/dist/plugin/rest-schemas.d.ts +44 -0
  116. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  117. package/dist/plugin/rest-schemas.js +55 -3
  118. package/dist/plugin/rest-schemas.js.map +1 -1
  119. package/dist/plugin/testing.d.ts +15 -1
  120. package/dist/plugin/testing.d.ts.map +1 -1
  121. package/dist/plugin/testing.js +40 -4
  122. package/dist/plugin/testing.js.map +1 -1
  123. package/dist/plugin/web.d.ts +6 -0
  124. package/dist/plugin/web.d.ts.map +1 -1
  125. package/dist/plugin/web.js +10 -1
  126. package/dist/plugin/web.js.map +1 -1
  127. package/dist/plugin.d.ts +1 -1
  128. package/dist/plugin.d.ts.map +1 -1
  129. package/dist/plugin.js +1 -1
  130. package/dist/plugin.js.map +1 -1
  131. package/dist/router.d.ts +19 -0
  132. package/dist/router.d.ts.map +1 -1
  133. package/dist/router.js +18 -0
  134. package/dist/router.js.map +1 -1
  135. package/dist/routes.d.ts +11 -2
  136. package/dist/routes.d.ts.map +1 -1
  137. package/dist/routes.js +14 -1
  138. package/dist/routes.js.map +1 -1
  139. package/dist/schemas.d.ts +367 -0
  140. package/dist/schemas.d.ts.map +1 -1
  141. package/dist/schemas.js +62 -2
  142. package/dist/schemas.js.map +1 -1
  143. package/dist/vocabulary.d.ts +1 -1
  144. package/dist/vocabulary.d.ts.map +1 -1
  145. package/dist/vocabulary.js +6 -0
  146. package/dist/vocabulary.js.map +1 -1
  147. package/docs/specs/cli.md +7 -4
  148. package/docs/specs/plugins.md +47 -39
  149. package/docs/specs/rest.md +22 -5
  150. package/package.json +1 -1
  151. package/skills/building-a-plugin/SKILL.md +8 -6
  152. package/skills/working-the-board/SKILL.md +1 -1
  153. package/src/api.ts +17 -0
  154. package/src/cli/cli.ts +200 -12
  155. package/src/cli/format.ts +82 -19
  156. package/src/cli/help.ts +30 -5
  157. package/src/client.ts +70 -0
  158. package/src/compat.ts +1 -1
  159. package/src/components/board.tsx +83 -42
  160. package/src/components/decision-form.tsx +13 -32
  161. package/src/components/event-feed.tsx +36 -9
  162. package/src/components/my-decisions.tsx +50 -5
  163. package/src/components/people.tsx +394 -37
  164. package/src/components/picker.tsx +11 -2
  165. package/src/components/plugin-frame.tsx +23 -0
  166. package/src/components/plugin-settings.tsx +346 -137
  167. package/src/components/projects.tsx +14 -14
  168. package/src/components/searches.ts +47 -0
  169. package/src/components/task-form.tsx +23 -28
  170. package/src/components/ui.tsx +36 -0
  171. package/src/console.tsx +229 -40
  172. package/src/data.ts +53 -1
  173. package/src/i18n/en.ts +98 -2
  174. package/src/i18n/ko.ts +95 -3
  175. package/src/index.ts +11 -0
  176. package/src/limits.ts +6 -0
  177. package/src/plugin/args.ts +16 -11
  178. package/src/plugin/bridge.ts +45 -0
  179. package/src/plugin/capabilities.ts +11 -0
  180. package/src/plugin/context.ts +170 -10
  181. package/src/plugin/define.ts +34 -0
  182. package/src/plugin/limits.ts +4 -0
  183. package/src/plugin/manifest.ts +74 -3
  184. package/src/plugin/rest-schemas.ts +62 -5
  185. package/src/plugin/testing.ts +61 -5
  186. package/src/plugin/web.ts +17 -0
  187. package/src/plugin.ts +1 -0
  188. package/src/router.ts +25 -0
  189. package/src/routes.ts +17 -2
  190. package/src/schemas.ts +81 -2
  191. package/src/styles/console.css +362 -67
  192. package/src/vocabulary.ts +6 -0
@@ -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";
@@ -121,7 +122,15 @@ export const restPluginSchema = z.object({
121
122
  /** The asker's roles in the plugin; every role for an administrator. */
122
123
  roles: z.array(z.string()),
123
124
  /** The roles the plugin declares, by key, with their names. */
124
- declaredRoles: z.array(z.object({ key: z.string(), name: localized, description: localized })),
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
+ ),
125
134
  /** The role every member holds without a grant, or `null` where the manifest names none. */
126
135
  defaultRole: z.nullable(z.string()),
127
136
  menu: z.nullable(z.object({ group: z.string(), order: z.int(), name: z.nullable(localized) })),
@@ -170,6 +179,14 @@ export const restPluginSettingSchema = z.object({
170
179
  value: z.nullable(z.unknown()),
171
180
  /** When a secret was set, or `null`. */
172
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"))),
173
190
  });
174
191
  export type RestPluginSetting = z.infer<typeof restPluginSettingSchema>;
175
192
 
@@ -180,6 +197,10 @@ export const restPluginGrantSchema = z.object({
180
197
  person: z.nullable(z.object({ id: uuid, login: z.string(), name: z.nullable(z.string()) })),
181
198
  project: z.nullable(z.object({ key: z.string(), name: z.string() })),
182
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() }))),
183
204
  createdAt: instant,
184
205
  });
185
206
  export type RestPluginGrant = z.infer<typeof restPluginGrantSchema>;
@@ -203,6 +224,28 @@ export const restPluginDetailSchema = z.extend(restPluginSchema, {
203
224
  grants: z.nullable(z.array(restPluginGrantSchema)),
204
225
  /** The plugin's own events are at this path. */
205
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
+ ),
206
249
  });
207
250
  export type RestPluginDetail = z.infer<typeof restPluginDetailSchema>;
208
251
 
@@ -254,15 +297,23 @@ export const restPluginGrantInputSchema = z
254
297
  role: pluginKey,
255
298
  person: z.optional(uuid),
256
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))),
257
304
  everyone: z.optional(z.literal(true)),
258
305
  })
259
306
  .check(
260
307
  z.refine(
261
308
  (input) =>
262
- [input.person !== undefined, input.project !== undefined, input.everyone === true].filter(
263
- Boolean,
264
- ).length === 1,
265
- "names a person, a project or everyone, one of the three",
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",
266
317
  ),
267
318
  );
268
319
  export type RestPluginGrantInput = z.infer<typeof restPluginGrantInputSchema>;
@@ -311,6 +362,12 @@ export const restPluginInvocationSchema = z.object({
311
362
  text: z.string(),
312
363
  data: z.unknown(),
313
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())),
314
371
  });
315
372
  export type RestPluginInvocation = z.infer<typeof restPluginInvocationSchema>;
316
373
 
@@ -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, OtherPlugin>>;
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.kind === "person" ? caller.person.id : null;
244
+ const by = actingPersonOf(caller)?.id ?? null;
230
245
  const indexed = (field: string) => {
231
246
  if (
232
247
  collection !== undefined &&
@@ -520,14 +535,34 @@ function refusingConsole(given: TestConsoleOptions | undefined): PluginConsole {
520
535
  workspace:
521
536
  given?.workspace ?? (async () => forbidden("console.workspace", "console.workspace")),
522
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(
@@ -541,6 +576,7 @@ function build(store: Store, options: TestContextOptions, caller: PluginCaller):
541
576
  capabilities: [],
542
577
  board: null,
543
578
  projects: "all",
579
+ grants: [],
544
580
  ...options.plugin,
545
581
  };
546
582
  const secrets = options.secrets ?? {};
@@ -571,6 +607,12 @@ function build(store: Store, options: TestContextOptions, caller: PluginCaller):
571
607
  fetch: options.fetch ?? (async () => forbidden("fetch", "fetch")),
572
608
  secrets: {
573
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
+ }),
574
616
  sign: async (name, payload) => {
575
617
  const secret = secrets[name];
576
618
  if (secret === undefined) {
@@ -591,12 +633,26 @@ function build(store: Store, options: TestContextOptions, caller: PluginCaller):
591
633
  if (other === undefined) {
592
634
  forbidden(`plugins.get(${key})`, `plugins.${key}`);
593
635
  }
594
- 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)}`;
595
651
  },
596
652
  },
597
653
  log: { info: log("info"), warn: log("warn"), error: log("error") },
598
654
  now,
599
- language: options.language ?? (caller.kind === "person" ? caller.person.language : "en"),
655
+ language: options.language ?? actingPersonOf(caller)?.language ?? "en",
600
656
  signal: new AbortController().signal,
601
657
  emitted: store.emitted,
602
658
  logged: store.logged,
package/src/plugin/web.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  type BridgeToPage,
16
16
  isBridgePath,
17
17
  isBridgeToFrame,
18
+ isBridgeUrl,
18
19
  MAX_BRIDGE_TITLE_LENGTH,
19
20
  PLUGIN_BRIDGE_VERSION,
20
21
  type PluginWidgetSizeName,
@@ -122,6 +123,12 @@ export interface PluginFrame {
122
123
  };
123
124
  /** The console's page follows a path of the console's in place; a URL is refused. */
124
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;
125
132
  /** The console's page downloads a file of this plugin. */
126
133
  download(id: string): void;
127
134
  setTitle(text: string): void;
@@ -350,6 +357,16 @@ export function connect(options: ConnectOptions = {}): Promise<PluginFrame> {
350
357
  }
351
358
  post({ v: PLUGIN_BRIDGE_VERSION, t: "navigate", path });
352
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
+ },
353
370
  download: (id) => post({ v: PLUGIN_BRIDGE_VERSION, t: "download", id }),
354
371
  setTitle: (text) => {
355
372
  const title = text.slice(0, MAX_BRIDGE_TITLE_LENGTH).replace(/[\r\n]+/g, " ");
package/src/plugin.ts CHANGED
@@ -74,6 +74,7 @@ export * from "./plugin/limits.js";
74
74
  export {
75
75
  CURRENT_PLUGIN_API,
76
76
  capabilitiesAsked,
77
+ isCalendarDay,
77
78
  isPluginKey,
78
79
  type Localized,
79
80
  legacyWidgetSize,
package/src/router.ts CHANGED
@@ -96,6 +96,10 @@ export type Route =
96
96
  | { readonly name: "people" }
97
97
  /** The form where an administrator lets someone in by login (ADR-0022). */
98
98
  | { readonly name: "new-listing" }
99
+ /** The teams of the workspace (ADR-0073), a view of the people's page. */
100
+ | { readonly name: "teams" }
101
+ /** The form where an administrator makes a team. */
102
+ | { readonly name: "new-team" }
99
103
  /** The agents of one person, for an administrator. */
100
104
  | { readonly name: "person-agents"; readonly id: string }
101
105
  /** Another person's plate, for whoever oversees them: a day of the week shown at the top, or today's. */
@@ -107,6 +111,8 @@ export type Route =
107
111
  | { readonly name: "work"; readonly week: string | undefined }
108
112
  /** The decisions addressed to the person (ADR-0042), across the projects they may see. */
109
113
  | { readonly name: "my-decisions" }
114
+ /** One decision wherever it is (ADR-0068), for whoever it reaches: in a project they may see or not. */
115
+ | { readonly name: "my-decision"; readonly id: string }
110
116
  /** The categories tasks are sorted into; an administrator keeps them here. */
111
117
  | { readonly name: "categories" }
112
118
  /** Where a person approves an agent's connection, by the code it showed, or types one. */
@@ -166,6 +172,10 @@ export const PATHS = {
166
172
  people: "/people",
167
173
  /** The form where an administrator lets someone in by login. */
168
174
  newListing: "/people/new",
175
+ /** The teams of the workspace, a view of the people's page. */
176
+ teams: "/people/teams",
177
+ /** The form where an administrator makes a team. */
178
+ newTeam: "/people/teams/new",
169
179
  /** The agents of whoever is signed in, which hold their tokens: their own page. */
170
180
  agents: "/agents",
171
181
  /** The form for a token made by hand, for a script or a console of one's own. */
@@ -293,12 +303,22 @@ export function matchRoute(pathname: string, search = ""): Route {
293
303
  if (path === PATHS.decisions) {
294
304
  return { name: "my-decisions" };
295
305
  }
306
+ if (path.startsWith(`${PATHS.decisions}/`)) {
307
+ const id = path.slice(PATHS.decisions.length + 1);
308
+ return UUID.test(id) ? { name: "my-decision", id } : NOT_FOUND;
309
+ }
296
310
  if (path === PATHS.people) {
297
311
  return { name: "people" };
298
312
  }
299
313
  if (path === PATHS.newListing) {
300
314
  return { name: "new-listing" };
301
315
  }
316
+ if (path === PATHS.teams) {
317
+ return { name: "teams" };
318
+ }
319
+ if (path === PATHS.newTeam) {
320
+ return { name: "new-team" };
321
+ }
302
322
  if (path === PATHS.agents) {
303
323
  return { name: "agents" };
304
324
  }
@@ -569,6 +589,11 @@ export function decisionsHref(key: string, page?: number): string {
569
589
  }
570
590
 
571
591
  /** Where the person's own work is, with the week of a day at the top, or this week's when none is named. */
592
+ /** Where one decision is read wherever it is (ADR-0068), by whoever it reaches: a person outside its project too. */
593
+ export function myDecisionHref(id: string): string {
594
+ return `${PATHS.decisions}/${id}`;
595
+ }
596
+
572
597
  export function workHref(week?: string): string {
573
598
  return week === undefined ? PATHS.work : `${PATHS.work}?week=${week}`;
574
599
  }
package/src/routes.ts CHANGED
@@ -17,6 +17,8 @@ export const REST_ROUTES = {
17
17
  workspacePackToken: "/api/v1/workspace/pack-token",
18
18
  /** The people of the workspace: everyone who has signed in, and the logins let in that have not. */
19
19
  people: "/api/v1/people",
20
+ /** The teams of the workspace (ADR-0073): a name and its members, kept by an administrator. */
21
+ teams: "/api/v1/teams",
20
22
  /** The logins an administrator let in (ADR-0022), made and removed by one. */
21
23
  listings: "/api/v1/listings",
22
24
  /** The projects the asker may see; the board of each is under its key. */
@@ -68,6 +70,19 @@ export function personWorkPath(personId: string): string {
68
70
  return `${restPath("people", personId)}/work`;
69
71
  }
70
72
 
73
+ /** The REST path of one decision wherever it is (ADR-0068), by its id; `/answer` under it answers. */
74
+ export function decisionPath(id: string): string {
75
+ return `${REST_ROUTES.decisions}/${encodeURIComponent(id)}`;
76
+ }
77
+
78
+ /**
79
+ * The REST path of a file put on a decision (ADR-0055), read at the decision's own address, for
80
+ * whoever the decision reaches wherever it is (ADR-0068), in a project they may see or not.
81
+ */
82
+ export function decisionFilePath(decisionId: string, fileId: string): string {
83
+ return `${decisionPath(decisionId)}/files/${encodeURIComponent(fileId)}`;
84
+ }
85
+
71
86
  /** The REST path of a person's tokens, or of one of them: for an administrator. */
72
87
  export function personTokensPath(personId: string, tokenId?: string): string {
73
88
  const base = `${restPath("people", personId)}/tokens`;
@@ -126,9 +141,9 @@ export function signInPath(page: string, failure: SignInFailure): string {
126
141
  return `${page}${page.includes("?") ? "&" : "?"}${SIGN_IN_PARAM}=${failure}`;
127
142
  }
128
143
 
129
- /** The REST path of one person, listing, token or project: the collection, then the id or the key. */
144
+ /** The REST path of one person, listing, token, project or team: the collection, then the id or the key. */
130
145
  export function restPath(
131
- collection: "people" | "listings" | "tokens" | "projects",
146
+ collection: "people" | "listings" | "tokens" | "projects" | "teams",
132
147
  id: string,
133
148
  ): string {
134
149
  return `${REST_ROUTES[collection]}/${encodeURIComponent(id)}`;
package/src/schemas.ts CHANGED
@@ -38,6 +38,8 @@ import {
38
38
  MAX_SECRET_LENGTH,
39
39
  MAX_SKILLS_ADDRESS_LENGTH,
40
40
  MAX_SUMMARY_LENGTH,
41
+ MAX_TEAM_KEY_LENGTH,
42
+ MAX_TEAM_NAME_LENGTH,
41
43
  MAX_TITLE_LENGTH,
42
44
  MAX_TOKEN_DAYS,
43
45
  MAX_TOKEN_NAME_LENGTH,
@@ -135,12 +137,21 @@ export const restPersonSchema = z.object({
135
137
  * a console built on this package still reads an older console's people.
136
138
  */
137
139
  managerId: z.optional(z.nullable(uuid)),
140
+ /** The teams the person is in (ADR-0073), by their keys (ADR-0074): on the people listing and to a plugin, from 0.4.5, and read as optional. */
141
+ teams: z.optional(z.array(z.string())),
138
142
  /**
139
143
  * The language the person reads in (ADR-0065), a tag: the one they chose, else the one the pages
140
144
  * first saw them in; `null` until they have opened the pages. What a plugin or an agent writes
141
145
  * for them in. Answered from 0.4.4 and read as optional, as `managerId` is.
142
146
  */
143
147
  language: z.optional(z.nullable(languageTag)),
148
+ /**
149
+ * The person's work email, lowercase: the address they sign in with at Google, else what an
150
+ * administrator wrote; `null` until known. Answered to an administrator and to the person
151
+ * themselves alone, and left out everywhere else, so that a task's owner or a comment's author
152
+ * carries none. Answered from 0.4.5 and read as optional.
153
+ */
154
+ email: z.optional(z.nullable(z.string())),
144
155
  });
145
156
  export type RestPerson = z.infer<typeof restPersonSchema>;
146
157
 
@@ -188,6 +199,47 @@ export const restListingInputSchema = z
188
199
  .check(z.refine((input) => isLoginAt(input.host, input.login), "must be a login at the host"));
189
200
  export type RestListingInput = z.infer<typeof restListingInputSchema>;
190
201
 
202
+ /**
203
+ * A team's key (ADR-0074): what its addresses, a grant, a plugin and the command call it. A
204
+ * project key's shape, one name among the teams' keys and the projects' keys of a workspace;
205
+ * immutable once the team is made.
206
+ */
207
+ const teamKey = z.string().check(
208
+ z.maxLength(MAX_TEAM_KEY_LENGTH),
209
+ z.regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, "must be lowercase letters, digits and hyphens"),
210
+ z.refine((key) => key !== "new", "is a word the pages reserve"),
211
+ );
212
+
213
+ /** A team of the workspace (ADR-0073): its key, its name and who is in it, among the people the asker may see. */
214
+ export const restTeamSchema = z.object({
215
+ id: uuid,
216
+ key: z.string(),
217
+ name: z.string(),
218
+ memberIds: z.array(uuid),
219
+ createdAt: instant,
220
+ });
221
+ export type RestTeam = z.infer<typeof restTeamSchema>;
222
+
223
+ export const restTeamsSchema = z.object({ items: z.array(restTeamSchema) });
224
+ export type RestTeams = z.infer<typeof restTeamsSchema>;
225
+
226
+ /** What `POST /api/v1/teams` is sent: the key, which stays, and the name. */
227
+ export const restTeamInputSchema = z.object({
228
+ key: teamKey,
229
+ name: line(MAX_TEAM_NAME_LENGTH),
230
+ });
231
+ export type RestTeamInput = z.infer<typeof restTeamInputSchema>;
232
+
233
+ /** What `PATCH /api/v1/teams/<key>` is sent: the name; the key never changes. */
234
+ export const restTeamPatchSchema = z.object({
235
+ name: line(MAX_TEAM_NAME_LENGTH),
236
+ });
237
+ export type RestTeamPatch = z.infer<typeof restTeamPatchSchema>;
238
+
239
+ /** What `POST /api/v1/teams/<key>/members` is sent. */
240
+ export const restTeamMemberInputSchema = z.object({ personId: uuid });
241
+ export type RestTeamMemberInput = z.infer<typeof restTeamMemberInputSchema>;
242
+
191
243
  /** An identity provider people may sign in through, as the pages offer it. */
192
244
  export const restProviderSchema = z.object({
193
245
  key: z.enum(PROVIDER_KEYS),
@@ -388,12 +440,17 @@ export const restPersonPatchSchema = z
388
440
  status: z.optional(z.enum(PERSON_STATUSES)),
389
441
  /** Whom the person reports to: another active person of the workspace, or `null` for nobody. */
390
442
  managerId: z.optional(z.nullable(uuid)),
443
+ /** The person's work email, one per workspace, or `null` for none; an administrator's to write. */
444
+ email: z.optional(z.nullable(z.email().check(z.maxLength(254)))),
391
445
  })
392
446
  .check(
393
447
  z.refine(
394
448
  (patch) =>
395
- patch.role !== undefined || patch.status !== undefined || patch.managerId !== undefined,
396
- "must name a role, a status or a manager",
449
+ patch.role !== undefined ||
450
+ patch.status !== undefined ||
451
+ patch.managerId !== undefined ||
452
+ patch.email !== undefined,
453
+ "must name a role, a status, a manager or an email",
397
454
  ),
398
455
  );
399
456
  export type RestPersonPatch = z.infer<typeof restPersonPatchSchema>;
@@ -886,6 +943,18 @@ export const restMyDecisionsSchema = z.object({
886
943
  });
887
944
  export type RestMyDecisions = z.infer<typeof restMyDecisionsSchema>;
888
945
 
946
+ /**
947
+ * `GET /api/v1/decisions/<id>`: one decision wherever it is (ADR-0068), in a project the asker
948
+ * may see or one that reaches them from elsewhere, with its project and whether they may answer
949
+ * it (`mayAnswer`, read as optional).
950
+ */
951
+ export const restReachedDecisionSchema = z.object({
952
+ project: projectRef,
953
+ decision: restDecisionSchema,
954
+ mayAnswer: z.optional(z.boolean()),
955
+ });
956
+ export type RestReachedDecision = z.infer<typeof restReachedDecisionSchema>;
957
+
889
958
  /**
890
959
  * What `GET /api/v1/people/<id>/work` answers: another person's open tasks, and their closed
891
960
  * ones for the week, within the projects the asker may see, for whoever oversees them. No
@@ -981,6 +1050,10 @@ export const restEventDataSchema = z.object({
981
1050
  role: z.optional(z.string()),
982
1051
  /** For `person.manager_changed`: the login of whom the person reports to now, or `null` for nobody. */
983
1052
  manager: z.optional(z.nullable(z.string())),
1053
+ /** For the events about a team (ADR-0073), and for a role granted to one: its name; `team.renamed` holds the name before in `name`, and each event of a team its `key` (ADR-0074). */
1054
+ team: z.optional(z.string()),
1055
+ /** For `person.email_changed`: whether the person has an address now; never the address itself. */
1056
+ set: z.optional(z.boolean()),
984
1057
  /** For the events about a project: its key and its name. */
985
1058
  key: z.optional(z.string()),
986
1059
  name: z.optional(z.string()),
@@ -1003,6 +1076,10 @@ export const restEventDataSchema = z.object({
1003
1076
  args: z.optional(z.record(z.string(), z.unknown())),
1004
1077
  /** For `plugin.enabled` and `plugin.configured`: the capabilities consented to. */
1005
1078
  capabilities: z.optional(z.array(z.string())),
1079
+ /** For `plugin.configured` of a secret: `administrator`, or the host of the answer it was sealed from (ADR-0070). */
1080
+ source: z.optional(z.string()),
1081
+ /** For `plugin.granted` and `plugin.revoked`: the plugin the role went to, by key, where one did (ADR-0072). */
1082
+ grantee: z.optional(z.string()),
1006
1083
  /** For `plugin.granted` and `plugin.revoked`: whether the role went to everyone. */
1007
1084
  everyone: z.optional(z.boolean()),
1008
1085
  /** For `plugin.failed`: what went wrong, in words for a person. */
@@ -1029,6 +1106,8 @@ export const restEventSchema = z.object({
1029
1106
  goalId: z.nullable(uuid),
1030
1107
  /** The plugin that caused it, by key, or `null` (ADR-0052). */
1031
1108
  plugin: z.nullable(z.string()),
1109
+ /** The plugin the person acted through (ADR-0072), by key, when one acted for them; answered from 0.4.5 and read as optional. */
1110
+ via: z.optional(z.nullable(z.string())),
1032
1111
  data: restEventDataSchema,
1033
1112
  createdAt: instant,
1034
1113
  });