@skillcdn/console 0.4.1 → 0.4.2
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 +19 -2
- package/SKILLCDN.md +7 -5
- package/dist/api.d.ts +4 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +543 -34
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +34 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +277 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +35 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +21 -0
- package/dist/cli/plugin-check.d.ts.map +1 -0
- package/dist/cli/plugin-check.js +374 -0
- package/dist/cli/plugin-check.js.map +1 -0
- package/dist/cli/plugin-cli.d.ts +20 -0
- package/dist/cli/plugin-cli.d.ts.map +1 -0
- package/dist/cli/plugin-cli.js +123 -0
- package/dist/cli/plugin-cli.js.map +1 -0
- package/dist/cli/plugin-init.d.ts +20 -0
- package/dist/cli/plugin-init.d.ts.map +1 -0
- package/dist/cli/plugin-init.js +74 -0
- package/dist/cli/plugin-init.js.map +1 -0
- package/dist/cli/plugin-main.d.ts +3 -0
- package/dist/cli/plugin-main.d.ts.map +1 -0
- package/dist/cli/plugin-main.js +28 -0
- package/dist/cli/plugin-main.js.map +1 -0
- package/dist/client.d.ts +97 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +63 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +2 -2
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +59 -15
- package/dist/components/board.js.map +1 -1
- package/dist/components/event-feed.d.ts +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +33 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +52 -0
- package/dist/components/plugin-frame.d.ts.map +1 -0
- package/dist/components/plugin-frame.js +314 -0
- package/dist/components/plugin-frame.js.map +1 -0
- package/dist/components/plugin-settings.d.ts +38 -0
- package/dist/components/plugin-settings.d.ts.map +1 -0
- package/dist/components/plugin-settings.js +348 -0
- package/dist/components/plugin-settings.js.map +1 -0
- package/dist/components/shell.d.ts +2 -0
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +26 -1
- package/dist/components/shell.js.map +1 -1
- package/dist/components/workspace.d.ts +7 -1
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +1 -1
- package/dist/components/workspace.js.map +1 -1
- package/dist/console.css +327 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +136 -26
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +7 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +19 -2
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +149 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +160 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +144 -0
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin/args.d.ts +148 -0
- package/dist/plugin/args.d.ts.map +1 -0
- package/dist/plugin/args.js +369 -0
- package/dist/plugin/args.js.map +1 -0
- package/dist/plugin/bridge.d.ts +146 -0
- package/dist/plugin/bridge.d.ts.map +1 -0
- package/dist/plugin/bridge.js +125 -0
- package/dist/plugin/bridge.js.map +1 -0
- package/dist/plugin/capabilities.d.ts +20 -0
- package/dist/plugin/capabilities.d.ts.map +1 -0
- package/dist/plugin/capabilities.js +133 -0
- package/dist/plugin/capabilities.js.map +1 -0
- package/dist/plugin/context.d.ts +374 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +2 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +208 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +482 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/definition-error.d.ts +11 -0
- package/dist/plugin/definition-error.d.ts.map +1 -0
- package/dist/plugin/definition-error.js +17 -0
- package/dist/plugin/definition-error.js.map +1 -0
- package/dist/plugin/limits.d.ts +67 -0
- package/dist/plugin/limits.d.ts.map +1 -0
- package/dist/plugin/limits.js +70 -0
- package/dist/plugin/limits.js.map +1 -0
- package/dist/plugin/load-worker.d.ts +7 -0
- package/dist/plugin/load-worker.d.ts.map +1 -0
- package/dist/plugin/load-worker.js +41 -0
- package/dist/plugin/load-worker.js.map +1 -0
- package/dist/plugin/load.d.ts +18 -0
- package/dist/plugin/load.d.ts.map +1 -0
- package/dist/plugin/load.js +62 -0
- package/dist/plugin/load.js.map +1 -0
- package/dist/plugin/manifest.d.ts +237 -0
- package/dist/plugin/manifest.d.ts.map +1 -0
- package/dist/plugin/manifest.js +411 -0
- package/dist/plugin/manifest.js.map +1 -0
- package/dist/plugin/refusals.d.ts +49 -0
- package/dist/plugin/refusals.d.ts.map +1 -0
- package/dist/plugin/refusals.js +79 -0
- package/dist/plugin/refusals.js.map +1 -0
- package/dist/plugin/rest-schemas.d.ts +556 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -0
- package/dist/plugin/rest-schemas.js +262 -0
- package/dist/plugin/rest-schemas.js.map +1 -0
- package/dist/plugin/sandbox.d.ts +23 -0
- package/dist/plugin/sandbox.d.ts.map +1 -0
- package/dist/plugin/sandbox.js +99 -0
- package/dist/plugin/sandbox.js.map +1 -0
- package/dist/plugin/testing.d.ts +45 -0
- package/dist/plugin/testing.d.ts.map +1 -0
- package/dist/plugin/testing.js +420 -0
- package/dist/plugin/testing.js.map +1 -0
- package/dist/plugin/web-react.d.ts +11 -0
- package/dist/plugin/web-react.d.ts.map +1 -0
- package/dist/plugin/web-react.js +34 -0
- package/dist/plugin/web-react.js.map +1 -0
- package/dist/plugin/web.d.ts +95 -0
- package/dist/plugin/web.d.ts.map +1 -0
- package/dist/plugin/web.js +245 -0
- package/dist/plugin/web.js.map +1 -0
- package/dist/plugin-nav.d.ts +29 -0
- package/dist/plugin-nav.d.ts.map +1 -0
- package/dist/plugin-nav.js +92 -0
- package/dist/plugin-nav.js.map +1 -0
- package/dist/plugin-testing.d.ts +2 -0
- package/dist/plugin-testing.d.ts.map +1 -0
- package/dist/plugin-testing.js +4 -0
- package/dist/plugin-testing.js.map +1 -0
- package/dist/plugin-web-react.d.ts +3 -0
- package/dist/plugin-web-react.d.ts.map +1 -0
- package/dist/plugin-web-react.js +3 -0
- package/dist/plugin-web-react.js.map +1 -0
- package/dist/plugin-web.d.ts +4 -0
- package/dist/plugin-web.d.ts.map +1 -0
- package/dist/plugin-web.js +5 -0
- package/dist/plugin-web.js.map +1 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +15 -0
- package/dist/plugin.js.map +1 -0
- package/dist/router.d.ts +28 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +47 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +4 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +6 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +117 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +40 -1
- package/dist/schemas.js.map +1 -1
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +101 -0
- package/dist/tokens.js.map +1 -0
- package/dist/vocabulary.d.ts +11 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +24 -0
- package/dist/vocabulary.js.map +1 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +2 -0
- package/dist/web.js.map +1 -1
- package/docs/specs/cli.md +11 -4
- package/docs/specs/plugins.md +487 -0
- package/docs/specs/rest.md +3 -2
- package/package.json +20 -2
- package/skills/building-a-console/SKILL.md +25 -26
- package/skills/building-a-console/references/configuring.md +40 -0
- package/skills/building-a-console/references/the-default-console.md +1 -1
- package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
- package/skills/building-a-plugin/SKILL.md +137 -0
- package/skills/deploying-a-console/SKILL.md +24 -26
- package/skills/deploying-a-console/references/access.md +13 -9
- package/skills/deploying-a-console/references/resources.md +25 -10
- package/skills/deploying-a-console/references/running-it-again.md +15 -0
- package/skills/deploying-a-console/references/sizing.md +2 -0
- package/skills/planning-the-board/SKILL.md +96 -0
- package/skills/planning-the-board/references/a-task.md +36 -0
- package/skills/planning-the-board/references/setting-up.md +44 -0
- package/skills/working-the-board/SKILL.md +43 -34
- package/skills/working-the-board/references/the-pack.md +19 -0
- package/src/api.ts +8 -0
- package/src/cli/cli.ts +697 -33
- package/src/cli/format.ts +356 -9
- package/src/cli/help.ts +41 -2
- package/src/cli/plugin-check.ts +470 -0
- package/src/cli/plugin-cli.ts +148 -0
- package/src/cli/plugin-init.ts +90 -0
- package/src/cli/plugin-main.ts +30 -0
- package/src/client.ts +294 -0
- package/src/compat.ts +2 -2
- package/src/components/board.tsx +62 -14
- package/src/components/event-feed.tsx +42 -1
- package/src/components/plugin-frame.tsx +437 -0
- package/src/components/plugin-settings.tsx +1111 -0
- package/src/components/shell.tsx +82 -14
- package/src/components/workspace.tsx +18 -0
- package/src/console.tsx +286 -19
- package/src/data.ts +27 -2
- package/src/i18n/en.ts +168 -0
- package/src/i18n/ko.ts +148 -0
- package/src/index.ts +12 -0
- package/src/plugin/args.ts +526 -0
- package/src/plugin/bridge.ts +314 -0
- package/src/plugin/capabilities.ts +150 -0
- package/src/plugin/context.ts +416 -0
- package/src/plugin/define.ts +792 -0
- package/src/plugin/definition-error.ts +19 -0
- package/src/plugin/limits.ts +70 -0
- package/src/plugin/load-worker.ts +50 -0
- package/src/plugin/load.ts +82 -0
- package/src/plugin/manifest.ts +561 -0
- package/src/plugin/refusals.ts +94 -0
- package/src/plugin/rest-schemas.ts +354 -0
- package/src/plugin/sandbox.ts +123 -0
- package/src/plugin/testing.ts +612 -0
- package/src/plugin/web-react.ts +45 -0
- package/src/plugin/web.ts +403 -0
- package/src/plugin-nav.ts +132 -0
- package/src/plugin-testing.ts +10 -0
- package/src/plugin-web-react.ts +4 -0
- package/src/plugin-web.ts +35 -0
- package/src/plugin.ts +126 -0
- package/src/router.ts +73 -0
- package/src/routes.ts +7 -0
- package/src/schemas.ts +43 -0
- package/src/styles/console.css +327 -0
- package/src/tokens.ts +101 -0
- package/src/vocabulary.ts +30 -0
- package/src/web.ts +2 -0
- package/template/plugin/.gitignore +2 -0
- package/template/plugin/README.md +26 -0
- package/template/plugin/SKILLCDN.md +13 -0
- package/template/plugin/build.mjs +40 -0
- package/template/plugin/console-plugin.json +50 -0
- package/template/plugin/package.json +27 -0
- package/template/plugin/skills/__key__/SKILL.md +23 -0
- package/template/plugin/src/check.test.ts +34 -0
- package/template/plugin/src/server/index.test.ts +41 -0
- package/template/plugin/src/server/index.ts +72 -0
- package/template/plugin/src/web/index.css +51 -0
- package/template/plugin/src/web/index.ts +115 -0
- package/template/plugin/tsconfig.json +17 -0
- package/skills/working-the-board/references/setting-up.md +0 -3
- /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// The REST shapes of the plugins' routes (docs/specs/plugins.md, "REST"): what the console
|
|
2
|
+
// answers about its plugins and what an administrator sends to enable, configure and grant. In
|
|
3
|
+
// the mini build of zod, as every REST schema is, and part of the snapshot the contract is held
|
|
4
|
+
// to (ADR-0043). What a plugin's own command or query carries is the plugin's, and not here.
|
|
5
|
+
import * as z from "zod/mini";
|
|
6
|
+
import {
|
|
7
|
+
MAX_PROJECT_DESCRIPTION_LENGTH,
|
|
8
|
+
MAX_PROJECT_KEY_LENGTH,
|
|
9
|
+
MAX_PROJECT_NAME_LENGTH,
|
|
10
|
+
} from "../limits.js";
|
|
11
|
+
import { PLUGIN_STATES } from "../vocabulary.js";
|
|
12
|
+
import { ARG_TYPES } from "./args.js";
|
|
13
|
+
import {
|
|
14
|
+
MAX_PLUGIN_ARGV_WORD_LENGTH,
|
|
15
|
+
MAX_PLUGIN_ARGV_WORDS,
|
|
16
|
+
MAX_PLUGIN_COMMAND_FILES,
|
|
17
|
+
} from "./limits.js";
|
|
18
|
+
import {
|
|
19
|
+
PLUGIN_BOARD_NEEDS,
|
|
20
|
+
PLUGIN_KEY_PATTERN,
|
|
21
|
+
PLUGIN_PAGE_LEVELS,
|
|
22
|
+
PLUGIN_SETTING_TYPES,
|
|
23
|
+
PLUGIN_WIDGET_PLACES,
|
|
24
|
+
PLUGIN_WIDGET_SIZES,
|
|
25
|
+
SETTING_KEY_PATTERN,
|
|
26
|
+
} from "./manifest.js";
|
|
27
|
+
|
|
28
|
+
/** How long a secret an administrator sets for a plugin may be. */
|
|
29
|
+
export const MAX_PLUGIN_SECRET_LENGTH = 4096;
|
|
30
|
+
/** How many grants one plugin holds. */
|
|
31
|
+
export const MAX_PLUGIN_GRANTS = 1000;
|
|
32
|
+
|
|
33
|
+
const uuid = z.uuid();
|
|
34
|
+
const instant = z.iso.datetime();
|
|
35
|
+
const pluginKey = z.string().check(z.regex(PLUGIN_KEY_PATTERN));
|
|
36
|
+
const settingKey = z.string().check(z.regex(SETTING_KEY_PATTERN));
|
|
37
|
+
const projectKey = z.string().check(z.minLength(1), z.maxLength(MAX_PROJECT_KEY_LENGTH));
|
|
38
|
+
/** A text per language, as the manifest has it. */
|
|
39
|
+
const localized = z.record(z.string(), z.string());
|
|
40
|
+
|
|
41
|
+
/** An argument of a command or a query, as the console describes it for help and forms. */
|
|
42
|
+
export const restPluginArgSchema: z.ZodMiniType<RestPluginArg> = z.object({
|
|
43
|
+
type: z.enum(ARG_TYPES),
|
|
44
|
+
about: z.optional(z.string()),
|
|
45
|
+
optional: z.boolean(),
|
|
46
|
+
positional: z.boolean(),
|
|
47
|
+
secret: z.optional(z.boolean()),
|
|
48
|
+
max: z.optional(z.number()),
|
|
49
|
+
min: z.optional(z.number()),
|
|
50
|
+
integer: z.optional(z.boolean()),
|
|
51
|
+
options: z.optional(z.array(z.string())),
|
|
52
|
+
collection: z.optional(z.string()),
|
|
53
|
+
get of() {
|
|
54
|
+
return z.optional(restPluginArgSchema);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
export interface RestPluginArg {
|
|
58
|
+
readonly type: (typeof ARG_TYPES)[number];
|
|
59
|
+
readonly about?: string | undefined;
|
|
60
|
+
readonly optional: boolean;
|
|
61
|
+
readonly positional: boolean;
|
|
62
|
+
readonly secret?: boolean | undefined;
|
|
63
|
+
readonly max?: number | undefined;
|
|
64
|
+
readonly min?: number | undefined;
|
|
65
|
+
readonly integer?: boolean | undefined;
|
|
66
|
+
readonly options?: readonly string[] | undefined;
|
|
67
|
+
readonly collection?: string | undefined;
|
|
68
|
+
readonly of?: RestPluginArg | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A command or a query of a plugin, as `GET /api/v1/plugins/<key>` lists them. */
|
|
72
|
+
export const restPluginOperationSchema = z.object({
|
|
73
|
+
name: z.string(),
|
|
74
|
+
kind: z.enum(["command", "query"]),
|
|
75
|
+
about: z.string(),
|
|
76
|
+
args: z.record(z.string(), restPluginArgSchema),
|
|
77
|
+
/** The roles that may run it, or `null` for every member. */
|
|
78
|
+
roles: z.nullable(z.array(z.string())),
|
|
79
|
+
});
|
|
80
|
+
export type RestPluginOperation = z.infer<typeof restPluginOperationSchema>;
|
|
81
|
+
|
|
82
|
+
export const restPluginPageSchema = z.object({
|
|
83
|
+
key: z.string(),
|
|
84
|
+
name: localized,
|
|
85
|
+
level: z.enum(PLUGIN_PAGE_LEVELS),
|
|
86
|
+
wide: z.boolean(),
|
|
87
|
+
});
|
|
88
|
+
export type RestPluginPage = z.infer<typeof restPluginPageSchema>;
|
|
89
|
+
|
|
90
|
+
export const restPluginWidgetSchema = z.object({
|
|
91
|
+
key: z.string(),
|
|
92
|
+
name: localized,
|
|
93
|
+
place: z.enum(PLUGIN_WIDGET_PLACES),
|
|
94
|
+
size: z.enum(PLUGIN_WIDGET_SIZES),
|
|
95
|
+
});
|
|
96
|
+
export type RestPluginWidget = z.infer<typeof restPluginWidgetSchema>;
|
|
97
|
+
|
|
98
|
+
/** A plugin as a member may know of it: what it is, how it stands, and what the member may do with it. */
|
|
99
|
+
export const restPluginSchema = z.object({
|
|
100
|
+
key: z.string(),
|
|
101
|
+
name: localized,
|
|
102
|
+
description: localized,
|
|
103
|
+
/** The sentence agents read with the rules, or `null` for the description. */
|
|
104
|
+
agentNote: z.nullable(localized),
|
|
105
|
+
version: z.string(),
|
|
106
|
+
state: z.enum(PLUGIN_STATES),
|
|
107
|
+
/** Why the plugin failed, or waits for consent; empty otherwise. */
|
|
108
|
+
problems: z.array(z.string()),
|
|
109
|
+
/** The asker's roles in the plugin; every role for an administrator. */
|
|
110
|
+
roles: z.array(z.string()),
|
|
111
|
+
/** The roles the plugin declares, by key, with their names. */
|
|
112
|
+
declaredRoles: z.array(z.object({ key: z.string(), name: localized, description: localized })),
|
|
113
|
+
/** The role every member holds without a grant, or `null` where the manifest names none. */
|
|
114
|
+
defaultRole: z.nullable(z.string()),
|
|
115
|
+
menu: z.nullable(z.object({ group: z.string(), order: z.int(), name: z.nullable(localized) })),
|
|
116
|
+
/** The pages and widgets the asker may see. */
|
|
117
|
+
pages: z.array(restPluginPageSchema),
|
|
118
|
+
widgets: z.array(restPluginWidgetSchema),
|
|
119
|
+
/** The board project, or `null` for none. */
|
|
120
|
+
board: z.nullable(z.object({ key: z.string(), name: z.string() })),
|
|
121
|
+
boardNeeds: z.enum(PLUGIN_BOARD_NEEDS),
|
|
122
|
+
/** What the plugin asks, and what was consented to; `consent` is `null` to a member. */
|
|
123
|
+
capabilities: z.array(z.string()),
|
|
124
|
+
consent: z.nullable(z.array(z.string())),
|
|
125
|
+
/** The projects the plugin is on: all, their keys, or `null` to a member. */
|
|
126
|
+
projects: z.nullable(z.union([z.literal("all"), z.array(z.string())])),
|
|
127
|
+
requires: z.record(z.string(), z.string()),
|
|
128
|
+
dataVersion: z.int(),
|
|
129
|
+
enabledAt: z.nullable(instant),
|
|
130
|
+
});
|
|
131
|
+
export type RestPlugin = z.infer<typeof restPluginSchema>;
|
|
132
|
+
|
|
133
|
+
export const restPluginsSchema = z.object({
|
|
134
|
+
items: z.array(restPluginSchema),
|
|
135
|
+
/** Whether this deployment can give a plugin a schema of its own (`storage:sql`). */
|
|
136
|
+
sql: z.boolean(),
|
|
137
|
+
});
|
|
138
|
+
export type RestPlugins = z.infer<typeof restPluginsSchema>;
|
|
139
|
+
|
|
140
|
+
/** A setting as the pages draw its form: the manifest's declaration, and whether and what it holds. */
|
|
141
|
+
export const restPluginSettingSchema = z.object({
|
|
142
|
+
key: z.string(),
|
|
143
|
+
type: z.enum(PLUGIN_SETTING_TYPES),
|
|
144
|
+
name: localized,
|
|
145
|
+
description: z.nullable(localized),
|
|
146
|
+
required: z.boolean(),
|
|
147
|
+
default: z.nullable(z.unknown()),
|
|
148
|
+
min: z.nullable(z.number()),
|
|
149
|
+
max: z.nullable(z.number()),
|
|
150
|
+
options: z.nullable(z.array(z.object({ value: z.string(), name: localized }))),
|
|
151
|
+
of: z.nullable(z.string()),
|
|
152
|
+
/** Whether a value is set; a secret's value is never here. */
|
|
153
|
+
set: z.boolean(),
|
|
154
|
+
value: z.nullable(z.unknown()),
|
|
155
|
+
/** When a secret was set, or `null`. */
|
|
156
|
+
setAt: z.nullable(instant),
|
|
157
|
+
});
|
|
158
|
+
export type RestPluginSetting = z.infer<typeof restPluginSettingSchema>;
|
|
159
|
+
|
|
160
|
+
/** A grant of a role: to a person, to the members of a project, or to everyone. */
|
|
161
|
+
export const restPluginGrantSchema = z.object({
|
|
162
|
+
id: uuid,
|
|
163
|
+
role: z.string(),
|
|
164
|
+
person: z.nullable(z.object({ id: uuid, login: z.string(), name: z.nullable(z.string()) })),
|
|
165
|
+
project: z.nullable(z.object({ key: z.string(), name: z.string() })),
|
|
166
|
+
everyone: z.boolean(),
|
|
167
|
+
createdAt: instant,
|
|
168
|
+
});
|
|
169
|
+
export type RestPluginGrant = z.infer<typeof restPluginGrantSchema>;
|
|
170
|
+
|
|
171
|
+
export const restPluginGrantsSchema = z.object({ items: z.array(restPluginGrantSchema) });
|
|
172
|
+
export type RestPluginGrants = z.infer<typeof restPluginGrantsSchema>;
|
|
173
|
+
|
|
174
|
+
/** One plugin whole, for `GET /api/v1/plugins/<key>`: the listing's fields, and what agents and administrators need. */
|
|
175
|
+
export const restPluginDetailSchema = z.extend(restPluginSchema, {
|
|
176
|
+
operations: z.array(restPluginOperationSchema),
|
|
177
|
+
/** Where the plugin's skills are read, or `null` for none. */
|
|
178
|
+
skills: z.nullable(z.string()),
|
|
179
|
+
/** The board project the manifest proposes, for an administrator who makes it; `null` where it proposes none. */
|
|
180
|
+
boardDefaults: z.nullable(z.object({ key: z.string(), name: localized, description: localized })),
|
|
181
|
+
settings: z.array(restPluginSettingSchema),
|
|
182
|
+
/** The grants, to an administrator; `null` to a member. */
|
|
183
|
+
grants: z.nullable(z.array(restPluginGrantSchema)),
|
|
184
|
+
/** The plugin's own events are at this path. */
|
|
185
|
+
events: z.string(),
|
|
186
|
+
});
|
|
187
|
+
export type RestPluginDetail = z.infer<typeof restPluginDetailSchema>;
|
|
188
|
+
|
|
189
|
+
/** What `POST /api/v1/plugins/<key>/enable` is sent. */
|
|
190
|
+
export const restPluginEnableInputSchema = z.object({
|
|
191
|
+
/** Every capability the manifest asks, named again: consent. */
|
|
192
|
+
consent: z.array(z.string().check(z.maxLength(300))).check(z.maxLength(50)),
|
|
193
|
+
/** The board project: one to make, one that exists, or none. */
|
|
194
|
+
board: z.nullable(
|
|
195
|
+
z.union([
|
|
196
|
+
z.object({
|
|
197
|
+
create: z.object({
|
|
198
|
+
key: z.string().check(z.minLength(1), z.maxLength(MAX_PROJECT_KEY_LENGTH)),
|
|
199
|
+
name: z.string().check(z.minLength(1), z.maxLength(MAX_PROJECT_NAME_LENGTH)),
|
|
200
|
+
description: z.string().check(z.maxLength(MAX_PROJECT_DESCRIPTION_LENGTH)),
|
|
201
|
+
}),
|
|
202
|
+
}),
|
|
203
|
+
z.object({ project: projectKey }),
|
|
204
|
+
]),
|
|
205
|
+
),
|
|
206
|
+
/** The projects the plugin is on. */
|
|
207
|
+
projects: z.union([z.literal("all"), z.array(projectKey).check(z.maxLength(500))]),
|
|
208
|
+
/** The settings' values, without secrets. */
|
|
209
|
+
settings: z.optional(z.record(settingKey, z.unknown())),
|
|
210
|
+
});
|
|
211
|
+
export type RestPluginEnableInput = z.infer<typeof restPluginEnableInputSchema>;
|
|
212
|
+
|
|
213
|
+
/** What `PATCH /api/v1/plugins/<key>/settings` is sent: the values that change; `null` clears one. */
|
|
214
|
+
export const restPluginSettingsPatchSchema = z.object({
|
|
215
|
+
values: z.record(settingKey, z.unknown()),
|
|
216
|
+
});
|
|
217
|
+
export type RestPluginSettingsPatch = z.infer<typeof restPluginSettingsPatchSchema>;
|
|
218
|
+
|
|
219
|
+
/** What `PUT /api/v1/plugins/<key>/secrets/<name>` is sent. */
|
|
220
|
+
export const restPluginSecretInputSchema = z.object({
|
|
221
|
+
value: z.string().check(z.minLength(1), z.maxLength(MAX_PLUGIN_SECRET_LENGTH)),
|
|
222
|
+
});
|
|
223
|
+
export type RestPluginSecretInput = z.infer<typeof restPluginSecretInputSchema>;
|
|
224
|
+
|
|
225
|
+
/** What `POST /api/v1/plugins/<key>/grants` is sent: a role, and exactly one of whom. */
|
|
226
|
+
export const restPluginGrantInputSchema = z
|
|
227
|
+
.object({
|
|
228
|
+
role: pluginKey,
|
|
229
|
+
person: z.optional(uuid),
|
|
230
|
+
project: z.optional(projectKey),
|
|
231
|
+
everyone: z.optional(z.literal(true)),
|
|
232
|
+
})
|
|
233
|
+
.check(
|
|
234
|
+
z.refine(
|
|
235
|
+
(input) =>
|
|
236
|
+
[input.person !== undefined, input.project !== undefined, input.everyone === true].filter(
|
|
237
|
+
Boolean,
|
|
238
|
+
).length === 1,
|
|
239
|
+
"names a person, a project or everyone, one of the three",
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
export type RestPluginGrantInput = z.infer<typeof restPluginGrantInputSchema>;
|
|
243
|
+
|
|
244
|
+
/** A file a person uploaded for a command (`POST .../files`): the handle a `file` argument takes, good for an hour. */
|
|
245
|
+
export const restPluginFileHandleSchema = z.object({
|
|
246
|
+
handle: z.string(),
|
|
247
|
+
name: z.string(),
|
|
248
|
+
type: z.string(),
|
|
249
|
+
bytes: z.int(),
|
|
250
|
+
});
|
|
251
|
+
export type RestPluginFileHandle = z.infer<typeof restPluginFileHandleSchema>;
|
|
252
|
+
|
|
253
|
+
/** A handle as a command line names one: what `POST .../files` answered. */
|
|
254
|
+
const handle = z.string().check(z.minLength(1), z.maxLength(254));
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* What `POST /api/v1/plugins/<key>/invoke` is sent: the words as typed, the command's or the
|
|
258
|
+
* query's name first, the handles of the files uploaded for it, the project the call is on, and
|
|
259
|
+
* the run it is for where the caller has several open.
|
|
260
|
+
*/
|
|
261
|
+
export const restPluginInvokeInputSchema = z.object({
|
|
262
|
+
argv: z
|
|
263
|
+
.array(z.string().check(z.maxLength(MAX_PLUGIN_ARGV_WORD_LENGTH)))
|
|
264
|
+
.check(z.minLength(1), z.maxLength(MAX_PLUGIN_ARGV_WORDS)),
|
|
265
|
+
files: z.optional(z.array(handle).check(z.maxLength(MAX_PLUGIN_COMMAND_FILES))),
|
|
266
|
+
project: z.optional(projectKey),
|
|
267
|
+
run: z.optional(uuid),
|
|
268
|
+
});
|
|
269
|
+
export type RestPluginInvokeInput = z.infer<typeof restPluginInvokeInputSchema>;
|
|
270
|
+
|
|
271
|
+
/** What `POST /api/v1/plugins/<key>/commands/<name>` is sent: the arguments as JSON, by name. */
|
|
272
|
+
export const restPluginCommandInputSchema = z.object({
|
|
273
|
+
args: z.record(z.string(), z.unknown()),
|
|
274
|
+
project: z.optional(projectKey),
|
|
275
|
+
run: z.optional(uuid),
|
|
276
|
+
});
|
|
277
|
+
export type RestPluginCommandInput = z.infer<typeof restPluginCommandInputSchema>;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* What a command or a query answered: `text` is what the command prints, `data` what `--json`
|
|
281
|
+
* prints (a query's whole answer), `waiting` the id of a decision the caller now waits for.
|
|
282
|
+
*/
|
|
283
|
+
export const restPluginInvocationSchema = z.object({
|
|
284
|
+
kind: z.enum(["command", "query"]),
|
|
285
|
+
text: z.string(),
|
|
286
|
+
data: z.unknown(),
|
|
287
|
+
waiting: z.nullable(z.string()),
|
|
288
|
+
});
|
|
289
|
+
export type RestPluginInvocation = z.infer<typeof restPluginInvocationSchema>;
|
|
290
|
+
|
|
291
|
+
/** A record of a plugin's collection, as `GET .../records/<collection>` lists them. */
|
|
292
|
+
export const restPluginRecordSchema = z.object({
|
|
293
|
+
id: uuid,
|
|
294
|
+
collection: z.string(),
|
|
295
|
+
/** The id of the project the record is linked to, or `null`. */
|
|
296
|
+
project: z.nullable(z.string()),
|
|
297
|
+
/** The id of the person the record is about, where the collection has an owner. */
|
|
298
|
+
owner: z.nullable(z.string()),
|
|
299
|
+
data: z.record(z.string(), z.unknown()),
|
|
300
|
+
version: z.int(),
|
|
301
|
+
createdAt: instant,
|
|
302
|
+
updatedAt: instant,
|
|
303
|
+
createdBy: z.nullable(z.string()),
|
|
304
|
+
updatedBy: z.nullable(z.string()),
|
|
305
|
+
archivedAt: z.nullable(instant),
|
|
306
|
+
});
|
|
307
|
+
export type RestPluginRecord = z.infer<typeof restPluginRecordSchema>;
|
|
308
|
+
|
|
309
|
+
/** A page of records, and the cursor `after` continues with, or `null` on the last. */
|
|
310
|
+
export const restPluginRecordPageSchema = z.object({
|
|
311
|
+
items: z.array(restPluginRecordSchema),
|
|
312
|
+
after: z.nullable(z.string()),
|
|
313
|
+
});
|
|
314
|
+
export type RestPluginRecordPage = z.infer<typeof restPluginRecordPageSchema>;
|
|
315
|
+
|
|
316
|
+
/** One version of a record, as it was. */
|
|
317
|
+
export const restPluginRecordVersionSchema = z.object({
|
|
318
|
+
version: z.int(),
|
|
319
|
+
data: z.record(z.string(), z.unknown()),
|
|
320
|
+
at: instant,
|
|
321
|
+
by: z.nullable(z.string()),
|
|
322
|
+
});
|
|
323
|
+
export type RestPluginRecordVersion = z.infer<typeof restPluginRecordVersionSchema>;
|
|
324
|
+
|
|
325
|
+
/** What `GET .../records/<collection>/<id>[?version=]` answers: the record, or one version of it. */
|
|
326
|
+
export const restPluginRecordReadSchema = z.union([
|
|
327
|
+
restPluginRecordSchema,
|
|
328
|
+
restPluginRecordVersionSchema,
|
|
329
|
+
]);
|
|
330
|
+
export type RestPluginRecordRead = z.infer<typeof restPluginRecordReadSchema>;
|
|
331
|
+
|
|
332
|
+
export const restPluginRecordVersionsSchema = z.object({
|
|
333
|
+
items: z.array(restPluginRecordVersionSchema),
|
|
334
|
+
});
|
|
335
|
+
export type RestPluginRecordVersions = z.infer<typeof restPluginRecordVersionsSchema>;
|
|
336
|
+
|
|
337
|
+
/** An event a plugin emitted (`ctx.events.emit`), as `GET .../plugin-events?after=` lists them for its frames: its number in the stream, its kind, its data, when. */
|
|
338
|
+
export const restPluginEventSchema = z.object({
|
|
339
|
+
id: z.int(),
|
|
340
|
+
kind: z.string(),
|
|
341
|
+
data: z.unknown(),
|
|
342
|
+
at: instant,
|
|
343
|
+
});
|
|
344
|
+
export type RestPluginEvent = z.infer<typeof restPluginEventSchema>;
|
|
345
|
+
|
|
346
|
+
export const restPluginEventsSchema = z.object({
|
|
347
|
+
items: z.array(restPluginEventSchema),
|
|
348
|
+
more: z.boolean(),
|
|
349
|
+
});
|
|
350
|
+
export type RestPluginEvents = z.infer<typeof restPluginEventsSchema>;
|
|
351
|
+
|
|
352
|
+
/** What `POST /api/v1/plugins/<key>/purge` is sent: the key again. */
|
|
353
|
+
export const restPluginPurgeInputSchema = z.object({ confirm: pluginKey });
|
|
354
|
+
export type RestPluginPurgeInput = z.infer<typeof restPluginPurgeInputSchema>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// The hardening of a thread that runs plugin code (docs/specs/plugins.md, "The host"): the
|
|
2
|
+
// built-ins the host refuses, the globals it removes, and a scan of a bundle's text for the same,
|
|
3
|
+
// so that `console-plugin check` says before the host does. Defence in depth: the permission model
|
|
4
|
+
// of the host process is the first line; this is the second.
|
|
5
|
+
|
|
6
|
+
/** The built-in modules a plugin's thread may not load, with or without the `node:` prefix. */
|
|
7
|
+
export const FORBIDDEN_BUILTINS: readonly string[] = [
|
|
8
|
+
"net",
|
|
9
|
+
"http",
|
|
10
|
+
"https",
|
|
11
|
+
"http2",
|
|
12
|
+
"tls",
|
|
13
|
+
"dgram",
|
|
14
|
+
"dns",
|
|
15
|
+
"dns/promises",
|
|
16
|
+
"child_process",
|
|
17
|
+
"cluster",
|
|
18
|
+
"worker_threads",
|
|
19
|
+
"vm",
|
|
20
|
+
"inspector",
|
|
21
|
+
"inspector/promises",
|
|
22
|
+
"repl",
|
|
23
|
+
"wasi",
|
|
24
|
+
"v8",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** The globals a plugin's thread does without: the network, and the way under the module system. */
|
|
28
|
+
export const REMOVED_GLOBALS: readonly string[] = ["fetch", "WebSocket", "EventSource"];
|
|
29
|
+
|
|
30
|
+
/** True when a module specifier names a built-in the host refuses. */
|
|
31
|
+
export function isForbiddenBuiltin(specifier: string): boolean {
|
|
32
|
+
return FORBIDDEN_BUILTINS.includes(
|
|
33
|
+
specifier.startsWith("node:") ? specifier.slice(5) : specifier,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const IMPORT_OR_REQUIRE =
|
|
38
|
+
/(?:\bfrom\s*|\bimport\s*\(?\s*|\brequire\s*\(\s*|\bprocess\.getBuiltinModule\s*\(\s*)["'`](node:)?([a-z_/]+)["'`]/g;
|
|
39
|
+
|
|
40
|
+
/** The built-ins a bundle's text imports or requires that the host refuses, each once, as written. */
|
|
41
|
+
export function forbiddenImportsIn(source: string): string[] {
|
|
42
|
+
const found = new Set<string>();
|
|
43
|
+
for (const match of source.matchAll(IMPORT_OR_REQUIRE)) {
|
|
44
|
+
const specifier = `${match[1] ?? ""}${match[2] ?? ""}`;
|
|
45
|
+
if (isForbiddenBuiltin(specifier)) {
|
|
46
|
+
found.add(specifier);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return [...found];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const GLOBAL_USES = /\b(fetch|WebSocket|EventSource|process\.binding|process\.dlopen)\s*\(/g;
|
|
53
|
+
|
|
54
|
+
/** The removed globals a bundle's text calls, each once; a plugin that calls them fails at runtime. */
|
|
55
|
+
export function removedGlobalsIn(source: string): string[] {
|
|
56
|
+
const found = new Set<string>();
|
|
57
|
+
for (const match of source.matchAll(GLOBAL_USES)) {
|
|
58
|
+
if (match[1] !== undefined) {
|
|
59
|
+
found.add(match[1]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [...found];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** What the thread looks like after hardening, for a test to assert on. */
|
|
66
|
+
export interface HardenedThread {
|
|
67
|
+
readonly removed: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The module hook: refuses the forbidden built-ins and lets everything else resolve as it would. */
|
|
71
|
+
export function forbiddingResolve<C, R>(
|
|
72
|
+
specifier: string,
|
|
73
|
+
context: C,
|
|
74
|
+
nextResolve: (specifier: string, context: C) => R,
|
|
75
|
+
): R {
|
|
76
|
+
if (isForbiddenBuiltin(specifier)) {
|
|
77
|
+
const error = new Error(
|
|
78
|
+
`the host refuses ${specifier}: a plugin reaches the console through ctx alone`,
|
|
79
|
+
);
|
|
80
|
+
(error as { code?: string }).code = "ERR_PLUGIN_FORBIDDEN_BUILTIN";
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
return nextResolve(specifier, context);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Hardens the current thread: refuses the forbidden built-ins through the module hooks, removes
|
|
88
|
+
* the globals, empties the environment, and blocks `process.binding`. Call it in a worker thread
|
|
89
|
+
* before importing plugin code; it reaches nothing outside that thread.
|
|
90
|
+
*/
|
|
91
|
+
export function hardenThread(
|
|
92
|
+
moduleApi: Pick<typeof import("node:module"), "registerHooks">,
|
|
93
|
+
): HardenedThread {
|
|
94
|
+
moduleApi.registerHooks({ resolve: forbiddingResolve });
|
|
95
|
+
const removed: string[] = [];
|
|
96
|
+
const global = globalThis as Record<string, unknown>;
|
|
97
|
+
for (const name of REMOVED_GLOBALS) {
|
|
98
|
+
if (name in global) {
|
|
99
|
+
Object.defineProperty(global, name, {
|
|
100
|
+
value: undefined,
|
|
101
|
+
writable: false,
|
|
102
|
+
configurable: false,
|
|
103
|
+
});
|
|
104
|
+
removed.push(name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const proc = global.process as Record<string, unknown> | undefined;
|
|
108
|
+
if (proc !== undefined) {
|
|
109
|
+
const refuse = () => {
|
|
110
|
+
throw new Error("the host refuses process.binding");
|
|
111
|
+
};
|
|
112
|
+
for (const name of ["binding", "_linkedBinding", "dlopen"]) {
|
|
113
|
+
Object.defineProperty(proc, name, { value: refuse, writable: false, configurable: false });
|
|
114
|
+
}
|
|
115
|
+
const env = proc.env as Record<string, unknown> | undefined;
|
|
116
|
+
if (env !== undefined) {
|
|
117
|
+
for (const key of Object.keys(env)) {
|
|
118
|
+
delete env[key];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { removed };
|
|
123
|
+
}
|