@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,561 @@
|
|
|
1
|
+
// The manifest of a plugin, `console-plugin.json` (docs/specs/plugins.md, "The manifest"): what the
|
|
2
|
+
// plugin is and asks, as data the console reads without running anything. One schema, in the mini
|
|
3
|
+
// build of zod so that the settings pages read it too, and the words every key is held to.
|
|
4
|
+
import * as z from "zod/mini";
|
|
5
|
+
import {
|
|
6
|
+
MAX_PROJECT_DESCRIPTION_LENGTH,
|
|
7
|
+
MAX_PROJECT_KEY_LENGTH,
|
|
8
|
+
MAX_PROJECT_NAME_LENGTH,
|
|
9
|
+
} from "../limits.js";
|
|
10
|
+
import { hasForbiddenCodePoint } from "../text.js";
|
|
11
|
+
import { isPluginCapability } from "./capabilities.js";
|
|
12
|
+
import {
|
|
13
|
+
MAX_PLUGIN_AGENT_NOTE_LENGTH,
|
|
14
|
+
MAX_PLUGIN_DESCRIPTION_LENGTH,
|
|
15
|
+
MAX_PLUGIN_NAME_LENGTH,
|
|
16
|
+
MAX_PLUGIN_OPTIONS,
|
|
17
|
+
MAX_PLUGIN_PAGES,
|
|
18
|
+
MAX_PLUGIN_ROLES,
|
|
19
|
+
MAX_PLUGIN_SETTINGS,
|
|
20
|
+
MAX_PLUGIN_WIDGETS,
|
|
21
|
+
} from "./limits.js";
|
|
22
|
+
|
|
23
|
+
/** A plugin's key, and every key within a plugin: a role's, a page's, a widget's, a setting's, a collection's, a command's. */
|
|
24
|
+
export const PLUGIN_KEY_PATTERN = /^[a-z][a-z0-9-]{1,31}$/;
|
|
25
|
+
|
|
26
|
+
export function isPluginKey(value: string): boolean {
|
|
27
|
+
return PLUGIN_KEY_PATTERN.test(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The majors of the plugin API this package speaks, and the one a new plugin is written against. */
|
|
31
|
+
export const PLUGIN_API_MAJORS = [1] as const;
|
|
32
|
+
export const CURRENT_PLUGIN_API = 1;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Words no plugin's key may be: the console's own paths and the command's own words, so that
|
|
36
|
+
* `/plugins/<key>` and `console plugin <key>` never read as something else.
|
|
37
|
+
*/
|
|
38
|
+
export const RESERVED_PLUGIN_KEYS: readonly string[] = [
|
|
39
|
+
"api",
|
|
40
|
+
"auth",
|
|
41
|
+
"plugins",
|
|
42
|
+
"plugin",
|
|
43
|
+
"settings",
|
|
44
|
+
"people",
|
|
45
|
+
"person",
|
|
46
|
+
"projects",
|
|
47
|
+
"project",
|
|
48
|
+
"work",
|
|
49
|
+
"decisions",
|
|
50
|
+
"decision",
|
|
51
|
+
"agents",
|
|
52
|
+
"categories",
|
|
53
|
+
"category",
|
|
54
|
+
"pages",
|
|
55
|
+
"connect",
|
|
56
|
+
"docs",
|
|
57
|
+
"doc",
|
|
58
|
+
"tasks",
|
|
59
|
+
"task",
|
|
60
|
+
"runs",
|
|
61
|
+
"run",
|
|
62
|
+
"skills",
|
|
63
|
+
"skill",
|
|
64
|
+
"members",
|
|
65
|
+
"member",
|
|
66
|
+
"rules",
|
|
67
|
+
"goals",
|
|
68
|
+
"goal",
|
|
69
|
+
"feed",
|
|
70
|
+
"files",
|
|
71
|
+
"file",
|
|
72
|
+
"events",
|
|
73
|
+
"frame",
|
|
74
|
+
"web",
|
|
75
|
+
"console",
|
|
76
|
+
"skillcdn",
|
|
77
|
+
"runtime",
|
|
78
|
+
"new",
|
|
79
|
+
"me",
|
|
80
|
+
"help",
|
|
81
|
+
"records",
|
|
82
|
+
"enable",
|
|
83
|
+
"disable",
|
|
84
|
+
"grant",
|
|
85
|
+
"revoke",
|
|
86
|
+
"secret",
|
|
87
|
+
"export",
|
|
88
|
+
"purge",
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
/** Words a page's key may not be: the routes beside the pages under a plugin's path. */
|
|
92
|
+
export const RESERVED_PAGE_KEYS: readonly string[] = ["frame", "web", "x"];
|
|
93
|
+
|
|
94
|
+
/** Words an argument may not be named: the flags `console plugin <key> <command>` keeps for itself. */
|
|
95
|
+
export const RESERVED_ARG_NAMES: readonly string[] = [
|
|
96
|
+
"json",
|
|
97
|
+
"url",
|
|
98
|
+
"account",
|
|
99
|
+
"project",
|
|
100
|
+
"file",
|
|
101
|
+
"run",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/** Words a command's or a query's name may not be: the subcommands of `console plugin <key>`. */
|
|
105
|
+
export const RESERVED_COMMAND_NAMES: readonly string[] = [
|
|
106
|
+
"help",
|
|
107
|
+
"records",
|
|
108
|
+
"skills",
|
|
109
|
+
"skill",
|
|
110
|
+
"enable",
|
|
111
|
+
"disable",
|
|
112
|
+
"settings",
|
|
113
|
+
"secret",
|
|
114
|
+
"grant",
|
|
115
|
+
"revoke",
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
/** The groups of tabs the console knows; a plugin may declare another, with a name. */
|
|
119
|
+
export const PLUGIN_MENU_GROUPS = [
|
|
120
|
+
"people",
|
|
121
|
+
"customers",
|
|
122
|
+
"money",
|
|
123
|
+
"operations",
|
|
124
|
+
"marketing",
|
|
125
|
+
"engineering",
|
|
126
|
+
"reports",
|
|
127
|
+
] as const;
|
|
128
|
+
|
|
129
|
+
export type PluginMenuGroup = (typeof PLUGIN_MENU_GROUPS)[number];
|
|
130
|
+
|
|
131
|
+
export const PLUGIN_SETTING_TYPES = [
|
|
132
|
+
"text",
|
|
133
|
+
"markdown",
|
|
134
|
+
"number",
|
|
135
|
+
"boolean",
|
|
136
|
+
"enum",
|
|
137
|
+
"day",
|
|
138
|
+
"person",
|
|
139
|
+
"project",
|
|
140
|
+
"secret",
|
|
141
|
+
"list",
|
|
142
|
+
] as const;
|
|
143
|
+
|
|
144
|
+
export type PluginSettingType = (typeof PLUGIN_SETTING_TYPES)[number];
|
|
145
|
+
|
|
146
|
+
export const PLUGIN_PAGE_LEVELS = ["workspace", "project"] as const;
|
|
147
|
+
export type PluginPageLevel = (typeof PLUGIN_PAGE_LEVELS)[number];
|
|
148
|
+
|
|
149
|
+
export const PLUGIN_WIDGET_PLACES = ["home:top", "home:bottom"] as const;
|
|
150
|
+
export type PluginWidgetPlace = (typeof PLUGIN_WIDGET_PLACES)[number];
|
|
151
|
+
|
|
152
|
+
export const PLUGIN_WIDGET_SIZES = ["third", "half", "full"] as const;
|
|
153
|
+
export type PluginWidgetSize = (typeof PLUGIN_WIDGET_SIZES)[number];
|
|
154
|
+
|
|
155
|
+
export const PLUGIN_BOARD_NEEDS = ["required", "optional", "none"] as const;
|
|
156
|
+
export type PluginBoardNeeds = (typeof PLUGIN_BOARD_NEEDS)[number];
|
|
157
|
+
|
|
158
|
+
const LANGUAGE_TAG = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
159
|
+
const DAY = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
|
|
160
|
+
/** A path within the package: relative, forward slashes, never climbing out. */
|
|
161
|
+
const PACKAGE_PATH = /^(?!\/)(?!\.\.(?:\/|$))(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/;
|
|
162
|
+
/** A semver range as npm spells one, loosely: the console compares versions, the registry resolves. */
|
|
163
|
+
const SEMVER_RANGE = /^[\sA-Za-z0-9.^~<>=|*x-]{1,100}$/;
|
|
164
|
+
|
|
165
|
+
const key = z
|
|
166
|
+
.string()
|
|
167
|
+
.check(
|
|
168
|
+
z.regex(
|
|
169
|
+
PLUGIN_KEY_PATTERN,
|
|
170
|
+
"is lowercase letters, digits and hyphens, 2 to 32 long, starting with a letter",
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const line = (max: number) =>
|
|
175
|
+
z.string().check(
|
|
176
|
+
z.minLength(1),
|
|
177
|
+
z.maxLength(max),
|
|
178
|
+
z.refine((value) => !hasForbiddenCodePoint(value), "must not contain control characters"),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const packagePath = z
|
|
182
|
+
.string()
|
|
183
|
+
.check(
|
|
184
|
+
z.maxLength(200),
|
|
185
|
+
z.regex(PACKAGE_PATH, "is a path within the package, with forward slashes"),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
/** A text per language, `en` always among them. */
|
|
189
|
+
export type Localized = Readonly<Record<string, string>>;
|
|
190
|
+
|
|
191
|
+
const localized = (max: number) =>
|
|
192
|
+
z
|
|
193
|
+
.record(z.string().check(z.regex(LANGUAGE_TAG, "is a language tag")), line(max))
|
|
194
|
+
.check(z.refine((texts) => typeof texts.en === "string", "needs an English text under en"));
|
|
195
|
+
|
|
196
|
+
const roleList = z.array(key).check(z.maxLength(MAX_PLUGIN_ROLES));
|
|
197
|
+
|
|
198
|
+
const settingOption = z.strictObject({
|
|
199
|
+
value: line(64),
|
|
200
|
+
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const settingItemType = z.enum(
|
|
204
|
+
PLUGIN_SETTING_TYPES.filter((type) => type !== "secret" && type !== "list"),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
/** A setting's key, as code and `--set` name it: a lowercase letter, then letters and digits. */
|
|
208
|
+
export const SETTING_KEY_PATTERN = /^[a-z][a-zA-Z0-9]{0,31}$/;
|
|
209
|
+
|
|
210
|
+
const settingKey = z
|
|
211
|
+
.string()
|
|
212
|
+
.check(
|
|
213
|
+
z.regex(
|
|
214
|
+
SETTING_KEY_PATTERN,
|
|
215
|
+
"is a lowercase letter followed by letters and digits, up to 32 long",
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const pluginSettingSchema = z
|
|
220
|
+
.strictObject({
|
|
221
|
+
key: settingKey,
|
|
222
|
+
type: z.enum(PLUGIN_SETTING_TYPES),
|
|
223
|
+
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
224
|
+
description: z.optional(localized(MAX_PLUGIN_DESCRIPTION_LENGTH)),
|
|
225
|
+
required: z.optional(z.boolean()),
|
|
226
|
+
default: z.optional(z.unknown()),
|
|
227
|
+
min: z.optional(z.number()),
|
|
228
|
+
max: z.optional(z.number()),
|
|
229
|
+
options: z.optional(
|
|
230
|
+
z.array(settingOption).check(z.minLength(1), z.maxLength(MAX_PLUGIN_OPTIONS)),
|
|
231
|
+
),
|
|
232
|
+
of: z.optional(settingItemType),
|
|
233
|
+
})
|
|
234
|
+
.check(
|
|
235
|
+
z.superRefine((setting, ctx) => {
|
|
236
|
+
for (const problem of settingProblems(setting)) {
|
|
237
|
+
ctx.addIssue({ code: "custom", message: problem, input: setting });
|
|
238
|
+
}
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
export type PluginSetting = z.infer<typeof pluginSettingSchema>;
|
|
243
|
+
|
|
244
|
+
/** True when `value` is what a setting of `type` holds, `options` being the values of an `enum`. */
|
|
245
|
+
function fitsSettingType(
|
|
246
|
+
type: Exclude<PluginSettingType, "secret" | "list">,
|
|
247
|
+
value: unknown,
|
|
248
|
+
options: readonly string[] | undefined,
|
|
249
|
+
): boolean {
|
|
250
|
+
switch (type) {
|
|
251
|
+
case "text":
|
|
252
|
+
case "markdown":
|
|
253
|
+
return typeof value === "string";
|
|
254
|
+
case "number":
|
|
255
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
256
|
+
case "boolean":
|
|
257
|
+
return typeof value === "boolean";
|
|
258
|
+
case "enum":
|
|
259
|
+
return typeof value === "string" && (options ?? []).includes(value);
|
|
260
|
+
case "day":
|
|
261
|
+
return typeof value === "string" && DAY.test(value);
|
|
262
|
+
case "person":
|
|
263
|
+
case "project":
|
|
264
|
+
return false;
|
|
265
|
+
default:
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function settingProblems(setting: {
|
|
271
|
+
type: PluginSettingType;
|
|
272
|
+
default?: unknown;
|
|
273
|
+
min?: number;
|
|
274
|
+
max?: number;
|
|
275
|
+
options?: readonly { value: string }[];
|
|
276
|
+
of?: Exclude<PluginSettingType, "secret" | "list">;
|
|
277
|
+
}): string[] {
|
|
278
|
+
const problems: string[] = [];
|
|
279
|
+
const bounded =
|
|
280
|
+
setting.type === "number" ||
|
|
281
|
+
setting.type === "text" ||
|
|
282
|
+
setting.type === "markdown" ||
|
|
283
|
+
setting.type === "list";
|
|
284
|
+
if (!bounded && (setting.min !== undefined || setting.max !== undefined)) {
|
|
285
|
+
problems.push(`a ${setting.type} setting takes no min or max`);
|
|
286
|
+
}
|
|
287
|
+
if (setting.min !== undefined && setting.max !== undefined && setting.min > setting.max) {
|
|
288
|
+
problems.push("min is above max");
|
|
289
|
+
}
|
|
290
|
+
const enumerated = setting.type === "enum" || (setting.type === "list" && setting.of === "enum");
|
|
291
|
+
if (enumerated && setting.options === undefined) {
|
|
292
|
+
problems.push("an enum needs options");
|
|
293
|
+
}
|
|
294
|
+
if (!enumerated && setting.options !== undefined) {
|
|
295
|
+
problems.push("only an enum takes options");
|
|
296
|
+
}
|
|
297
|
+
if (setting.type === "list" && setting.of === undefined) {
|
|
298
|
+
problems.push("a list needs of, the type of its items");
|
|
299
|
+
}
|
|
300
|
+
if (setting.type !== "list" && setting.of !== undefined) {
|
|
301
|
+
problems.push("only a list takes of");
|
|
302
|
+
}
|
|
303
|
+
if (setting.default !== undefined) {
|
|
304
|
+
const values = setting.options?.map((option) => option.value);
|
|
305
|
+
if (setting.type === "secret" || setting.type === "person" || setting.type === "project") {
|
|
306
|
+
problems.push(`a ${setting.type} setting takes no default`);
|
|
307
|
+
} else if (setting.type === "list") {
|
|
308
|
+
const of = setting.of;
|
|
309
|
+
if (
|
|
310
|
+
of !== undefined &&
|
|
311
|
+
!(
|
|
312
|
+
Array.isArray(setting.default) &&
|
|
313
|
+
setting.default.every((item) => fitsSettingType(of, item, values))
|
|
314
|
+
)
|
|
315
|
+
) {
|
|
316
|
+
problems.push(`the default is not a list of ${of}`);
|
|
317
|
+
}
|
|
318
|
+
} else if (!fitsSettingType(setting.type, setting.default, values)) {
|
|
319
|
+
problems.push(`the default is not a ${setting.type}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return problems;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const pluginPageSchema = z.strictObject({
|
|
326
|
+
key: key.check(
|
|
327
|
+
z.refine((value) => !RESERVED_PAGE_KEYS.includes(value), "is a word the routes reserve"),
|
|
328
|
+
),
|
|
329
|
+
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
330
|
+
level: z.enum(PLUGIN_PAGE_LEVELS),
|
|
331
|
+
roles: z.optional(roleList),
|
|
332
|
+
wide: z.optional(z.boolean()),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
export type PluginPage = z.infer<typeof pluginPageSchema>;
|
|
336
|
+
|
|
337
|
+
const pluginWidgetSchema = z.strictObject({
|
|
338
|
+
key,
|
|
339
|
+
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
340
|
+
place: z.enum(PLUGIN_WIDGET_PLACES),
|
|
341
|
+
size: z.enum(PLUGIN_WIDGET_SIZES),
|
|
342
|
+
roles: z.optional(roleList),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
export type PluginWidget = z.infer<typeof pluginWidgetSchema>;
|
|
346
|
+
|
|
347
|
+
const pluginMenuSchema = z
|
|
348
|
+
.strictObject({
|
|
349
|
+
group: key,
|
|
350
|
+
order: z.optional(z.int().check(z.gte(0), z.lte(1000))),
|
|
351
|
+
name: z.optional(localized(MAX_PLUGIN_NAME_LENGTH)),
|
|
352
|
+
})
|
|
353
|
+
.check(
|
|
354
|
+
z.refine(
|
|
355
|
+
(menu) =>
|
|
356
|
+
(PLUGIN_MENU_GROUPS as readonly string[]).includes(menu.group) || menu.name !== undefined,
|
|
357
|
+
"a group the console does not know needs a name",
|
|
358
|
+
),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
export type PluginMenu = z.infer<typeof pluginMenuSchema>;
|
|
362
|
+
|
|
363
|
+
const pluginBoardSchema = z
|
|
364
|
+
.strictObject({
|
|
365
|
+
needs: z.enum(PLUGIN_BOARD_NEEDS),
|
|
366
|
+
defaults: z.optional(
|
|
367
|
+
z.strictObject({
|
|
368
|
+
key: z
|
|
369
|
+
.string()
|
|
370
|
+
.check(
|
|
371
|
+
z.maxLength(MAX_PROJECT_KEY_LENGTH),
|
|
372
|
+
z.regex(/^[a-z][a-z0-9-]*$/, "is lowercase letters, digits and hyphens"),
|
|
373
|
+
),
|
|
374
|
+
name: localized(MAX_PROJECT_NAME_LENGTH),
|
|
375
|
+
description: localized(MAX_PROJECT_DESCRIPTION_LENGTH),
|
|
376
|
+
}),
|
|
377
|
+
),
|
|
378
|
+
})
|
|
379
|
+
.check(
|
|
380
|
+
z.refine(
|
|
381
|
+
(board) => board.needs !== "none" || board.defaults === undefined,
|
|
382
|
+
"a plugin that needs no board has no defaults for one",
|
|
383
|
+
),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
export type PluginBoard = z.infer<typeof pluginBoardSchema>;
|
|
387
|
+
|
|
388
|
+
const pluginRoleSchema = z.strictObject({
|
|
389
|
+
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
390
|
+
description: localized(MAX_PLUGIN_DESCRIPTION_LENGTH),
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
export type PluginRole = z.infer<typeof pluginRoleSchema>;
|
|
394
|
+
|
|
395
|
+
const pluginStoreSchema = z.strictObject({
|
|
396
|
+
homepage: z.optional(z.string().check(z.maxLength(2048), z.url({ protocol: /^https$/ }))),
|
|
397
|
+
repository: z.optional(z.string().check(z.maxLength(2048), z.url({ protocol: /^https$/ }))),
|
|
398
|
+
keywords: z.optional(z.array(line(40)).check(z.maxLength(10))),
|
|
399
|
+
icon: z.optional(packagePath),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
/** The manifest as a plugin writes it; `parsePluginManifest` fills what it leaves out. */
|
|
403
|
+
export const pluginManifestSchema = z
|
|
404
|
+
.strictObject({
|
|
405
|
+
key: key.check(
|
|
406
|
+
z.refine((value) => !RESERVED_PLUGIN_KEYS.includes(value), "is a word the console reserves"),
|
|
407
|
+
),
|
|
408
|
+
name: localized(MAX_PLUGIN_NAME_LENGTH),
|
|
409
|
+
description: localized(MAX_PLUGIN_DESCRIPTION_LENGTH),
|
|
410
|
+
agentNote: z.optional(localized(MAX_PLUGIN_AGENT_NOTE_LENGTH)),
|
|
411
|
+
pluginApi: z.int().check(z.positive()),
|
|
412
|
+
server: packagePath,
|
|
413
|
+
web: z.optional(packagePath),
|
|
414
|
+
skills: z.optional(packagePath),
|
|
415
|
+
migrations: z.optional(packagePath),
|
|
416
|
+
capabilities: z._default(
|
|
417
|
+
z.array(z.string().check(z.maxLength(300))).check(z.maxLength(50)),
|
|
418
|
+
[],
|
|
419
|
+
),
|
|
420
|
+
roles: z._default(z.record(key, pluginRoleSchema), {}),
|
|
421
|
+
defaultRole: z.optional(key),
|
|
422
|
+
requires: z._default(
|
|
423
|
+
z.record(key, z.string().check(z.regex(SEMVER_RANGE, "is a semver range"))),
|
|
424
|
+
{},
|
|
425
|
+
),
|
|
426
|
+
board: z._default(pluginBoardSchema, { needs: "none" }),
|
|
427
|
+
settings: z._default(z.array(pluginSettingSchema).check(z.maxLength(MAX_PLUGIN_SETTINGS)), []),
|
|
428
|
+
menu: z.optional(pluginMenuSchema),
|
|
429
|
+
pages: z._default(z.array(pluginPageSchema).check(z.maxLength(MAX_PLUGIN_PAGES)), []),
|
|
430
|
+
widgets: z._default(z.array(pluginWidgetSchema).check(z.maxLength(MAX_PLUGIN_WIDGETS)), []),
|
|
431
|
+
dataVersion: z._default(z.int().check(z.positive()), 1),
|
|
432
|
+
store: z.optional(pluginStoreSchema),
|
|
433
|
+
})
|
|
434
|
+
.check(
|
|
435
|
+
z.superRefine((manifest, ctx) => {
|
|
436
|
+
for (const problem of manifestProblems(manifest)) {
|
|
437
|
+
ctx.addIssue({ code: "custom", message: problem, input: manifest });
|
|
438
|
+
}
|
|
439
|
+
}),
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
export type PluginManifest = z.infer<typeof pluginManifestSchema>;
|
|
443
|
+
|
|
444
|
+
function duplicates(keys: readonly string[]): string[] {
|
|
445
|
+
const seen = new Set<string>();
|
|
446
|
+
const twice = new Set<string>();
|
|
447
|
+
for (const value of keys) {
|
|
448
|
+
if (seen.has(value)) {
|
|
449
|
+
twice.add(value);
|
|
450
|
+
}
|
|
451
|
+
seen.add(value);
|
|
452
|
+
}
|
|
453
|
+
return [...twice];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** What one field alone cannot say: the manifest's parts against each other. */
|
|
457
|
+
function manifestProblems(
|
|
458
|
+
manifest: Omit<PluginManifest, "key" | "name" | "description">,
|
|
459
|
+
): string[] {
|
|
460
|
+
const problems: string[] = [];
|
|
461
|
+
const roles = Object.keys(manifest.roles);
|
|
462
|
+
if (roles.length > MAX_PLUGIN_ROLES) {
|
|
463
|
+
problems.push(`declares ${roles.length} roles; ${MAX_PLUGIN_ROLES} at most`);
|
|
464
|
+
}
|
|
465
|
+
if (manifest.defaultRole !== undefined && !roles.includes(manifest.defaultRole)) {
|
|
466
|
+
problems.push(`defaultRole ${manifest.defaultRole} is not a declared role`);
|
|
467
|
+
}
|
|
468
|
+
for (const capability of manifest.capabilities) {
|
|
469
|
+
if (!isPluginCapability(capability)) {
|
|
470
|
+
problems.push(`capabilities: ${capability} is not one the console knows`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const capability of duplicates(manifest.capabilities)) {
|
|
474
|
+
problems.push(`capabilities: ${capability} is declared twice`);
|
|
475
|
+
}
|
|
476
|
+
const undeclared = (where: string, named: readonly string[] | undefined) => {
|
|
477
|
+
for (const role of named ?? []) {
|
|
478
|
+
if (!roles.includes(role)) {
|
|
479
|
+
problems.push(`${where} names the role ${role}, which is not declared`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
for (const page of manifest.pages) {
|
|
484
|
+
undeclared(`pages.${page.key}`, page.roles);
|
|
485
|
+
}
|
|
486
|
+
for (const widget of manifest.widgets) {
|
|
487
|
+
undeclared(`widgets.${widget.key}`, widget.roles);
|
|
488
|
+
}
|
|
489
|
+
for (const [what, keys] of [
|
|
490
|
+
["pages", manifest.pages.map((page) => page.key)],
|
|
491
|
+
["widgets", manifest.widgets.map((widget) => widget.key)],
|
|
492
|
+
["settings", manifest.settings.map((setting) => setting.key)],
|
|
493
|
+
] as const) {
|
|
494
|
+
for (const twice of duplicates(keys)) {
|
|
495
|
+
problems.push(`${what}: ${twice} is declared twice`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if ((manifest.pages.length > 0 || manifest.widgets.length > 0) && manifest.web === undefined) {
|
|
499
|
+
problems.push("pages and widgets need web, the directory of the bundle");
|
|
500
|
+
}
|
|
501
|
+
if (manifest.migrations !== undefined && !manifest.capabilities.includes("storage:sql")) {
|
|
502
|
+
problems.push("migrations need the capability storage:sql");
|
|
503
|
+
}
|
|
504
|
+
if (manifest.pages.length > 0 && manifest.menu === undefined) {
|
|
505
|
+
problems.push("pages need menu, the tab they are under");
|
|
506
|
+
}
|
|
507
|
+
return problems;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** The issue in words: the mini build of zod says "Invalid input" for its own checks, so the kind of issue is spelled out here. */
|
|
511
|
+
function wordsOf(issue: z.core.$ZodIssue): string {
|
|
512
|
+
switch (issue.code) {
|
|
513
|
+
case "unrecognized_keys":
|
|
514
|
+
return `${issue.keys.join(", ")}: not a field of the manifest`;
|
|
515
|
+
case "invalid_type":
|
|
516
|
+
return issue.input === undefined
|
|
517
|
+
? "is missing"
|
|
518
|
+
: `is not ${issue.expected === "array" ? "a list" : issue.expected === "object" ? "an object" : `a ${issue.expected}`}`;
|
|
519
|
+
case "too_big":
|
|
520
|
+
return `is over ${issue.maximum}`;
|
|
521
|
+
case "too_small":
|
|
522
|
+
return `is under ${issue.minimum}`;
|
|
523
|
+
case "invalid_value":
|
|
524
|
+
return `is not one of ${issue.values.map(String).join(", ")}`;
|
|
525
|
+
case "invalid_format":
|
|
526
|
+
return issue.message === "Invalid input" ? `is not a valid ${issue.format}` : issue.message;
|
|
527
|
+
default:
|
|
528
|
+
return issue.message;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export type ManifestParse =
|
|
533
|
+
| { readonly ok: true; readonly manifest: PluginManifest }
|
|
534
|
+
| { readonly ok: false; readonly problems: readonly string[] };
|
|
535
|
+
|
|
536
|
+
/** The manifest from what `console-plugin.json` held, filled in, or every problem with it in words. */
|
|
537
|
+
export function parsePluginManifest(value: unknown): ManifestParse {
|
|
538
|
+
const result = pluginManifestSchema.safeParse(value);
|
|
539
|
+
if (result.success) {
|
|
540
|
+
return { ok: true, manifest: result.data };
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
ok: false,
|
|
544
|
+
problems: result.error.issues.map((issue) => {
|
|
545
|
+
const path = issue.path.map(String).join(".");
|
|
546
|
+
const message = wordsOf(issue);
|
|
547
|
+
return path.length === 0 ? message : `${path}: ${message}`;
|
|
548
|
+
}),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** The text in the person's language, else in English, else the first there is. */
|
|
553
|
+
export function localizedText(texts: Localized, language: string): string {
|
|
554
|
+
return (
|
|
555
|
+
texts[language] ??
|
|
556
|
+
texts[language.split("-")[0] ?? language] ??
|
|
557
|
+
texts.en ??
|
|
558
|
+
Object.values(texts)[0] ??
|
|
559
|
+
""
|
|
560
|
+
);
|
|
561
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// What a console refuses a plugin, or a call on one, with (docs/specs/plugins.md, "Limits and
|
|
2
|
+
// refusals"): the codes, the status each answers with, and the error a plugin's own code throws to
|
|
3
|
+
// refuse in its own words, which the console answers as `plugin.<key>.<code>`.
|
|
4
|
+
|
|
5
|
+
/** The console's refusals about plugins, by code, with the HTTP status each answers with. */
|
|
6
|
+
export const PLUGIN_REFUSALS = {
|
|
7
|
+
"plugin.not_found": 404,
|
|
8
|
+
"plugin.disabled": 409,
|
|
9
|
+
"plugin.unavailable": 503,
|
|
10
|
+
"plugin.forbidden": 403,
|
|
11
|
+
"plugin.invalid_args": 400,
|
|
12
|
+
"plugin.timeout": 504,
|
|
13
|
+
"plugin.too_many": 409,
|
|
14
|
+
"plugin.conflict": 409,
|
|
15
|
+
"plugin.needs_consent": 409,
|
|
16
|
+
"plugin.requires": 409,
|
|
17
|
+
"plugin.required_by": 409,
|
|
18
|
+
"plugin.rate_limited": 429,
|
|
19
|
+
/** The plugin's code threw something that is not its own refusal, or answered what cannot be carried; what it said is in the log. */
|
|
20
|
+
"plugin.failed": 500,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export type PluginRefusalCode = keyof typeof PLUGIN_REFUSALS;
|
|
24
|
+
|
|
25
|
+
/** The status a plugin's own refusal answers with. */
|
|
26
|
+
export const PLUGIN_OWN_REFUSAL_STATUS = 400;
|
|
27
|
+
|
|
28
|
+
/** A plugin's own code: lowercase words joined by underscores, as the console's own codes are. */
|
|
29
|
+
const OWN_CODE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What a plugin throws to refuse in its own words: `throw new PluginError("over_limit", "Ten
|
|
33
|
+
* days a year at most.")`. The console answers `plugin.<key>.over_limit` with the message, and
|
|
34
|
+
* the command prints it as any refusal. Anything else thrown is `plugin.failed`, logged, and
|
|
35
|
+
* answered without its message.
|
|
36
|
+
*/
|
|
37
|
+
export class PluginError extends Error {
|
|
38
|
+
readonly code: string;
|
|
39
|
+
/** What the caller may do with the refusal, as JSON; optional. */
|
|
40
|
+
readonly data: unknown;
|
|
41
|
+
|
|
42
|
+
constructor(code: string, message: string, data?: unknown) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "PluginError";
|
|
45
|
+
this.code = OWN_CODE.test(code) ? code : "refused";
|
|
46
|
+
this.data = data;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isPluginError(value: unknown): value is PluginError {
|
|
51
|
+
return (
|
|
52
|
+
value instanceof PluginError ||
|
|
53
|
+
(typeof value === "object" &&
|
|
54
|
+
value !== null &&
|
|
55
|
+
(value as { name?: unknown }).name === "PluginError" &&
|
|
56
|
+
typeof (value as { code?: unknown }).code === "string" &&
|
|
57
|
+
typeof (value as { message?: unknown }).message === "string")
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The code the console answers a plugin's own refusal with. */
|
|
62
|
+
export function ownRefusalCode(pluginKey: string, code: string): string {
|
|
63
|
+
return `plugin.${pluginKey}.${code}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What a call on `ctx` throws when the console refuses it: the console's code (`plugin.forbidden`
|
|
68
|
+
* for a capability or a role the plugin lacks, `plugin.conflict` for a record that moved on,
|
|
69
|
+
* `plugin.too_many` for a quota reached, ...) and a message a person can read. Plugin code may
|
|
70
|
+
* let it through, and the console answers the caller with it as it is.
|
|
71
|
+
*/
|
|
72
|
+
export class PluginRefusal extends Error {
|
|
73
|
+
readonly code: string;
|
|
74
|
+
readonly status: number;
|
|
75
|
+
readonly data: unknown;
|
|
76
|
+
|
|
77
|
+
constructor(code: PluginRefusalCode | string, message: string, data?: unknown) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "PluginRefusal";
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.status = (PLUGIN_REFUSALS as Record<string, number>)[code] ?? PLUGIN_OWN_REFUSAL_STATUS;
|
|
82
|
+
this.data = data;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isPluginRefusal(value: unknown): value is PluginRefusal {
|
|
87
|
+
return (
|
|
88
|
+
value instanceof PluginRefusal ||
|
|
89
|
+
(typeof value === "object" &&
|
|
90
|
+
value !== null &&
|
|
91
|
+
(value as { name?: unknown }).name === "PluginRefusal" &&
|
|
92
|
+
typeof (value as { code?: unknown }).code === "string")
|
|
93
|
+
);
|
|
94
|
+
}
|