@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.
- package/README.md +3 -3
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +162 -15
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +16 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +68 -19
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +26 -5
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +28 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +46 -15
- package/dist/components/board.js.map +1 -1
- package/dist/components/decision-form.d.ts +0 -3
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +3 -15
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +30 -10
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/my-decisions.d.ts +20 -3
- package/dist/components/my-decisions.d.ts.map +1 -1
- package/dist/components/my-decisions.js +14 -1
- package/dist/components/my-decisions.js.map +1 -1
- package/dist/components/people.d.ts +43 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +117 -9
- package/dist/components/people.js.map +1 -1
- package/dist/components/picker.d.ts +8 -0
- package/dist/components/picker.d.ts.map +1 -1
- package/dist/components/picker.js +2 -2
- package/dist/components/picker.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +0 -1
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +22 -1
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts +5 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +143 -30
- package/dist/components/plugin-settings.js.map +1 -1
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -1
- package/dist/components/projects.js.map +1 -1
- package/dist/components/searches.d.ts +11 -0
- package/dist/components/searches.d.ts.map +1 -0
- package/dist/components/searches.js +30 -0
- package/dist/components/searches.js.map +1 -0
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +6 -5
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/ui.d.ts +16 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +10 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/console.css +362 -67
- package/dist/console.d.ts +2 -1
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +90 -14
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +14 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +36 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +89 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +87 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +87 -3
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/args.d.ts +6 -1
- package/dist/plugin/args.d.ts.map +1 -1
- package/dist/plugin/args.js +16 -11
- package/dist/plugin/args.js.map +1 -1
- package/dist/plugin/bridge.d.ts +20 -1
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +28 -0
- package/dist/plugin/bridge.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +1 -1
- package/dist/plugin/capabilities.d.ts.map +1 -1
- package/dist/plugin/capabilities.js +10 -0
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +158 -13
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +13 -1
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +17 -1
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +4 -0
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +4 -0
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +15 -0
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +61 -4
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +44 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +55 -3
- package/dist/plugin/rest-schemas.js.map +1 -1
- package/dist/plugin/testing.d.ts +15 -1
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +40 -4
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +6 -0
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +10 -1
- package/dist/plugin/web.js.map +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/router.d.ts +19 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +18 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +11 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +14 -1
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +367 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +62 -2
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +1 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +6 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +7 -4
- package/docs/specs/plugins.md +47 -39
- package/docs/specs/rest.md +22 -5
- package/package.json +1 -1
- package/skills/building-a-plugin/SKILL.md +8 -6
- package/skills/working-the-board/SKILL.md +1 -1
- package/src/api.ts +17 -0
- package/src/cli/cli.ts +200 -12
- package/src/cli/format.ts +82 -19
- package/src/cli/help.ts +30 -5
- package/src/client.ts +70 -0
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +83 -42
- package/src/components/decision-form.tsx +13 -32
- package/src/components/event-feed.tsx +36 -9
- package/src/components/my-decisions.tsx +50 -5
- package/src/components/people.tsx +394 -37
- package/src/components/picker.tsx +11 -2
- package/src/components/plugin-frame.tsx +23 -0
- package/src/components/plugin-settings.tsx +346 -137
- package/src/components/projects.tsx +14 -14
- package/src/components/searches.ts +47 -0
- package/src/components/task-form.tsx +23 -28
- package/src/components/ui.tsx +36 -0
- package/src/console.tsx +229 -40
- package/src/data.ts +53 -1
- package/src/i18n/en.ts +98 -2
- package/src/i18n/ko.ts +95 -3
- package/src/index.ts +11 -0
- package/src/limits.ts +6 -0
- package/src/plugin/args.ts +16 -11
- package/src/plugin/bridge.ts +45 -0
- package/src/plugin/capabilities.ts +11 -0
- package/src/plugin/context.ts +170 -10
- package/src/plugin/define.ts +34 -0
- package/src/plugin/limits.ts +4 -0
- package/src/plugin/manifest.ts +74 -3
- package/src/plugin/rest-schemas.ts +62 -5
- package/src/plugin/testing.ts +61 -5
- package/src/plugin/web.ts +17 -0
- package/src/plugin.ts +1 -0
- package/src/router.ts +25 -0
- package/src/routes.ts +17 -2
- package/src/schemas.ts +81 -2
- package/src/styles/console.css +362 -67
- 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(
|
|
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
|
-
[
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
package/src/plugin/testing.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { PluginRefusal } from "./refusals.js";
|
|
|
38
38
|
export interface TestConsoleOptions {
|
|
39
39
|
readonly workspace?: PluginConsole["workspace"];
|
|
40
40
|
readonly people?: Partial<PluginConsole["people"]>;
|
|
41
|
+
readonly teams?: Partial<PluginConsole["teams"]>;
|
|
41
42
|
readonly projects?: Partial<PluginConsole["projects"]>;
|
|
42
43
|
readonly tasks?: Partial<PluginConsole["tasks"]>;
|
|
43
44
|
readonly decisions?: Partial<PluginConsole["decisions"]>;
|
|
@@ -45,6 +46,16 @@ export interface TestConsoleOptions {
|
|
|
45
46
|
readonly runs?: Partial<PluginConsole["runs"]>;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Another plugin as a test gives it: its version and its api; and, where `for` is given, what it
|
|
51
|
+
* answers when called for a person. Without `for`, it answers the same for anyone.
|
|
52
|
+
*/
|
|
53
|
+
export interface TestOtherPlugin {
|
|
54
|
+
readonly version: string;
|
|
55
|
+
readonly api: OtherPlugin["api"];
|
|
56
|
+
readonly for?: ((personId: string) => TestOtherPlugin) | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
export interface TestContextOptions {
|
|
49
60
|
readonly plugin?: Partial<PluginInfo>;
|
|
50
61
|
readonly caller?: PluginCaller;
|
|
@@ -56,7 +67,11 @@ export interface TestContextOptions {
|
|
|
56
67
|
readonly fetch?: (url: string, init?: PluginFetchInit) => Promise<PluginFetchResponse>;
|
|
57
68
|
/** The secrets by name; `sign` and `verify` use them, and never hand them over. */
|
|
58
69
|
readonly secrets?: Readonly<Record<string, string>>;
|
|
59
|
-
readonly plugins?: Readonly<Record<string,
|
|
70
|
+
readonly plugins?: Readonly<Record<string, TestOtherPlugin>>;
|
|
71
|
+
/** The console's public origin, from which `ctx.endpoints.url` is answered; left out, it is `plugin.unavailable`. */
|
|
72
|
+
readonly publicUrl?: string;
|
|
73
|
+
/** The endpoints the plugin defines, by name, which `ctx.endpoints.url` answers for; any name when left out. */
|
|
74
|
+
readonly endpoints?: readonly string[];
|
|
60
75
|
readonly now?: () => Date;
|
|
61
76
|
readonly language?: string;
|
|
62
77
|
}
|
|
@@ -226,7 +241,7 @@ function memoryRecords<T extends Record<string, unknown>>(
|
|
|
226
241
|
}
|
|
227
242
|
return found;
|
|
228
243
|
};
|
|
229
|
-
const by = caller
|
|
244
|
+
const by = actingPersonOf(caller)?.id ?? null;
|
|
230
245
|
const indexed = (field: string) => {
|
|
231
246
|
if (
|
|
232
247
|
collection !== undefined &&
|
|
@@ -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
|
|
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
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
|
|
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 ||
|
|
396
|
-
|
|
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
|
});
|