@capybara-hq/plugin-sdk 0.1.0-rc.6 → 0.1.0-rc.8

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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Plugin setup guides (spec `2026-11-08-plugin-setup-guides-design.md`): setup material a plugin
3
+ * ships (steps and template files such as Slack's app manifest) that the kernel renders with a
4
+ * fixed set of safe, deterministic variables and the web shows where the person needs it. The
5
+ * format lives in `plugin.json` `setupGuides` ({@link PluginSetupGuide}); this module holds the
6
+ * places, their modes and variables, the limits and the pure renderer.
7
+ *
8
+ * Placeholders are `{{name}}` (spaces inside the braces allowed) with `name` matching
9
+ * `[A-Za-z][A-Za-z0-9]*`. There is no other syntax: no conditionals, loops, filters or escapes. A
10
+ * placeholder the place does not define, or one without a value, stays as written.
11
+ */
12
+ /** Where a guide is shown. `channel-connection`: 新建渠道 and the channel detail page. */
13
+ export declare const SETUP_GUIDE_PLACES: readonly ["channel-connection"];
14
+ export type SetupGuidePlace = (typeof SETUP_GUIDE_PLACES)[number];
15
+ /**
16
+ * The modes of each place: a step or file with `mode` applies to that mode only.
17
+ * `channel-connection`: the connection's `inboundMode`.
18
+ */
19
+ export declare const SETUP_GUIDE_MODES: Readonly<Record<SetupGuidePlace, readonly string[]>>;
20
+ /**
21
+ * The variables of each place. None is a secret: the kernel fills them from public URLs, names and
22
+ * versions only (`publicUrl`, `webhookUrl`, `signInCallbackUrl`, `connectionName`, `orgName`,
23
+ * `pluginName`, `pluginVersion`, `mode`).
24
+ */
25
+ export declare const SETUP_GUIDE_VARIABLES: Readonly<Record<SetupGuidePlace, readonly string[]>>;
26
+ /** Languages of a template file: the code block's label and how values are escaped. */
27
+ export declare const SETUP_GUIDE_LANGUAGES: readonly ["yaml", "json", "text"];
28
+ export type SetupGuideLanguage = (typeof SETUP_GUIDE_LANGUAGES)[number];
29
+ /** Limits of setup guides. */
30
+ export declare const SETUP_GUIDE_LIMITS: {
31
+ /** Guides per plugin. */
32
+ readonly guides: 8;
33
+ /** Steps per guide. */
34
+ readonly steps: 20;
35
+ /** Files per guide. */
36
+ readonly files: 8;
37
+ /** Characters of a step (each language). */
38
+ readonly stepChars: 2000;
39
+ /** Characters of a title (each language). */
40
+ readonly titleChars: 100;
41
+ /** Bytes of a template file. */
42
+ readonly fileBytes: number;
43
+ };
44
+ /** File names a template file may have (checked on the path). */
45
+ export declare const SETUP_GUIDE_FILE_RE: RegExp;
46
+ /** Localized text: English, optional Chinese (falls back to English). */
47
+ export interface SetupGuideText {
48
+ en: string;
49
+ zh?: string;
50
+ }
51
+ /** A step: plain text (a backtick pair is shown as inline code), optionally for one mode. */
52
+ export interface SetupGuideStep extends SetupGuideText {
53
+ mode?: string;
54
+ }
55
+ /** A template file of a guide. */
56
+ export interface SetupGuideFile {
57
+ /** `./…` inside the plugin, `.tmpl` / `.yaml` / `.yml` / `.json` / `.txt`. */
58
+ path: string;
59
+ title: SetupGuideText;
60
+ language: SetupGuideLanguage;
61
+ mode?: string;
62
+ }
63
+ /** A setup guide in `plugin.json` `setupGuides`. */
64
+ export interface PluginSetupGuide {
65
+ /** `^[a-z0-9][a-z0-9-]{0,63}$`, unique in the plugin. */
66
+ id: string;
67
+ place: SetupGuidePlace;
68
+ title: SetupGuideText;
69
+ steps?: SetupGuideStep[];
70
+ files?: SetupGuideFile[];
71
+ }
72
+ /** The placeholder names a text uses, each once, in order of first use. */
73
+ export declare function setupGuidePlaceholders(text: string): string[];
74
+ /** The placeholders of a text that the place does not define (the toolchain refuses them). */
75
+ export declare function unknownSetupGuidePlaceholders(text: string, place: SetupGuidePlace): string[];
76
+ /** The modes a guide's steps and files use, in the place's order (empty: no mode switch). */
77
+ export declare function setupGuideModes(guide: PluginSetupGuide): string[];
78
+ /**
79
+ * Renders a guide text: each `{{name}}` with a value in `values` is replaced by it, escaped for
80
+ * `language` (`yaml` / `json`: JSON-string-escaped without quotes, so a placeholder inside double
81
+ * quotes stays one string; `text`: control characters become spaces). A placeholder whose value is
82
+ * null or undefined stays as written and is listed in `unresolved` when `known` holds it; one not
83
+ * in `known` stays as written and is not listed. Never runs code; deterministic.
84
+ */
85
+ export declare function renderSetupGuideText(text: string, values: Readonly<Record<string, string | null | undefined>>, language: SetupGuideLanguage, known?: readonly string[]): {
86
+ text: string;
87
+ unresolved: string[];
88
+ };
89
+ /**
90
+ * Whether a guide's template file is usable: present, at most {@link SETUP_GUIDE_LIMITS}`.fileBytes`,
91
+ * valid UTF-8 text without NUL. The problem (English, naming the path), or undefined when it is.
92
+ * Shared by the packer and the kernel's package reader.
93
+ */
94
+ export declare function setupGuideFileProblem(path: string, data: Uint8Array | undefined): string | undefined;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Plugin setup guides (spec `2026-11-08-plugin-setup-guides-design.md`): setup material a plugin
3
+ * ships (steps and template files such as Slack's app manifest) that the kernel renders with a
4
+ * fixed set of safe, deterministic variables and the web shows where the person needs it. The
5
+ * format lives in `plugin.json` `setupGuides` ({@link PluginSetupGuide}); this module holds the
6
+ * places, their modes and variables, the limits and the pure renderer.
7
+ *
8
+ * Placeholders are `{{name}}` (spaces inside the braces allowed) with `name` matching
9
+ * `[A-Za-z][A-Za-z0-9]*`. There is no other syntax: no conditionals, loops, filters or escapes. A
10
+ * placeholder the place does not define, or one without a value, stays as written.
11
+ */
12
+ /** Where a guide is shown. `channel-connection`: 新建渠道 and the channel detail page. */
13
+ export const SETUP_GUIDE_PLACES = ["channel-connection"];
14
+ /**
15
+ * The modes of each place: a step or file with `mode` applies to that mode only.
16
+ * `channel-connection`: the connection's `inboundMode`.
17
+ */
18
+ export const SETUP_GUIDE_MODES = {
19
+ "channel-connection": ["long_connection", "webhook"],
20
+ };
21
+ /**
22
+ * The variables of each place. None is a secret: the kernel fills them from public URLs, names and
23
+ * versions only (`publicUrl`, `webhookUrl`, `signInCallbackUrl`, `connectionName`, `orgName`,
24
+ * `pluginName`, `pluginVersion`, `mode`).
25
+ */
26
+ export const SETUP_GUIDE_VARIABLES = {
27
+ "channel-connection": [
28
+ "publicUrl",
29
+ "webhookUrl",
30
+ "signInCallbackUrl",
31
+ "connectionName",
32
+ "orgName",
33
+ "pluginName",
34
+ "pluginVersion",
35
+ "mode",
36
+ ],
37
+ };
38
+ /** Languages of a template file: the code block's label and how values are escaped. */
39
+ export const SETUP_GUIDE_LANGUAGES = ["yaml", "json", "text"];
40
+ /** Limits of setup guides. */
41
+ export const SETUP_GUIDE_LIMITS = {
42
+ /** Guides per plugin. */
43
+ guides: 8,
44
+ /** Steps per guide. */
45
+ steps: 20,
46
+ /** Files per guide. */
47
+ files: 8,
48
+ /** Characters of a step (each language). */
49
+ stepChars: 2000,
50
+ /** Characters of a title (each language). */
51
+ titleChars: 100,
52
+ /** Bytes of a template file. */
53
+ fileBytes: 64 * 1024,
54
+ };
55
+ /** File names a template file may have (checked on the path). */
56
+ export const SETUP_GUIDE_FILE_RE = /\.(?:tmpl|ya?ml|json|txt)$/i;
57
+ const PLACEHOLDER_RE = /\{\{\s*([A-Za-z][A-Za-z0-9]*)\s*\}\}/g;
58
+ /** The placeholder names a text uses, each once, in order of first use. */
59
+ export function setupGuidePlaceholders(text) {
60
+ const out = [];
61
+ for (const m of text.matchAll(PLACEHOLDER_RE)) {
62
+ const name = m[1];
63
+ if (!out.includes(name))
64
+ out.push(name);
65
+ }
66
+ return out;
67
+ }
68
+ /** The placeholders of a text that the place does not define (the toolchain refuses them). */
69
+ export function unknownSetupGuidePlaceholders(text, place) {
70
+ const known = SETUP_GUIDE_VARIABLES[place];
71
+ return setupGuidePlaceholders(text).filter((n) => !known.includes(n));
72
+ }
73
+ /** The modes a guide's steps and files use, in the place's order (empty: no mode switch). */
74
+ export function setupGuideModes(guide) {
75
+ const used = new Set();
76
+ for (const s of guide.steps ?? [])
77
+ if (s.mode !== undefined)
78
+ used.add(s.mode);
79
+ for (const f of guide.files ?? [])
80
+ if (f.mode !== undefined)
81
+ used.add(f.mode);
82
+ return SETUP_GUIDE_MODES[guide.place].filter((m) => used.has(m));
83
+ }
84
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what is replaced
85
+ const CONTROL_RE = /[\u0000-\u001f\u007f]/g;
86
+ /** A value as inserted into a text of the language (see {@link renderSetupGuideText}). */
87
+ function escapeValue(value, language) {
88
+ if (language === "text")
89
+ return value.replace(CONTROL_RE, " ");
90
+ // JSON string escaping without the quotes: valid inside a JSON string and a YAML double-quoted
91
+ // scalar, whatever the value holds
92
+ return JSON.stringify(value).slice(1, -1);
93
+ }
94
+ /**
95
+ * Renders a guide text: each `{{name}}` with a value in `values` is replaced by it, escaped for
96
+ * `language` (`yaml` / `json`: JSON-string-escaped without quotes, so a placeholder inside double
97
+ * quotes stays one string; `text`: control characters become spaces). A placeholder whose value is
98
+ * null or undefined stays as written and is listed in `unresolved` when `known` holds it; one not
99
+ * in `known` stays as written and is not listed. Never runs code; deterministic.
100
+ */
101
+ export function renderSetupGuideText(text, values, language, known = Object.keys(values)) {
102
+ const unresolved = [];
103
+ const out = text.replace(PLACEHOLDER_RE, (whole, name) => {
104
+ if (!known.includes(name))
105
+ return whole;
106
+ const v = Object.hasOwn(values, name) ? values[name] : undefined;
107
+ if (v === null || v === undefined) {
108
+ if (!unresolved.includes(name))
109
+ unresolved.push(name);
110
+ return whole;
111
+ }
112
+ return escapeValue(v, language);
113
+ });
114
+ return { text: out, unresolved };
115
+ }
116
+ /**
117
+ * Whether a guide's template file is usable: present, at most {@link SETUP_GUIDE_LIMITS}`.fileBytes`,
118
+ * valid UTF-8 text without NUL. The problem (English, naming the path), or undefined when it is.
119
+ * Shared by the packer and the kernel's package reader.
120
+ */
121
+ export function setupGuideFileProblem(path, data) {
122
+ if (data === undefined)
123
+ return `${path} not found in the package`;
124
+ if (data.byteLength > SETUP_GUIDE_LIMITS.fileBytes) {
125
+ return `${path} is larger than ${SETUP_GUIDE_LIMITS.fileBytes} bytes`;
126
+ }
127
+ let text;
128
+ try {
129
+ text = new TextDecoder("utf-8", { fatal: true }).decode(data);
130
+ }
131
+ catch {
132
+ return `${path} is not valid UTF-8 text`;
133
+ }
134
+ return text.includes("\u0000") ? `${path} is not a text file (it contains NUL)` : undefined;
135
+ }
@@ -0,0 +1,39 @@
1
+ import { type ParsedTheme } from "./theme.js";
2
+ /**
3
+ * Parses a logo SVG (UTF-8, ≤ {@link THEME_LIMITS}`.svgBytes`) with a strict, namespace-aware
4
+ * XML parser and re-serializes it. The root must be an `svg` element in the SVG namespace with a
5
+ * numeric `viewBox`; its `width`, `height` and `class` are dropped. Rejected: a DOCTYPE (and so
6
+ * any entity declaration), processing instructions, a non-UTF-8 encoding, elements outside the
7
+ * SVG allowlist (`script`, `foreignObject`, `style`, …) or in the XHTML namespace, `style` and
8
+ * `on*` attributes, `href` / `xlink:href` not starting with `#`, `javascript:` / `vbscript:` in
9
+ * any value, quotes or a backslash in attribute values, any function other than `url(#id)`, colors
10
+ * (`rgb`, `rgba`, `hsl`, `hsla`) and transforms (`translate`, `rotate`, `scale`, `matrix`,
11
+ * `skewX`, `skewY`), a viewBox longer than 128 characters, and animation of `href`, `style` or event
12
+ * handlers. Comments, the XML declaration and elements / attributes in other namespaces (editor
13
+ * metadata) are dropped; CDATA becomes escaped text. Never throws.
14
+ */
15
+ export declare function sanitizeThemeSvg(bytes: Uint8Array): {
16
+ svg: string;
17
+ } | {
18
+ error: string;
19
+ };
20
+ /** A woff2 font: the `wOF2` signature and at most {@link THEME_LIMITS}`.fontBytes`. */
21
+ export declare function checkWoff2(bytes: Uint8Array): string | undefined;
22
+ /**
23
+ * A PNG logo: the PNG signature, an `IHDR` first chunk with a height of at least
24
+ * {@link THEME_LIMITS}`.pngMinHeight` pixels, and at most {@link THEME_LIMITS}`.pngBytes`.
25
+ */
26
+ export declare function checkPng(bytes: Uint8Array): string | undefined;
27
+ /**
28
+ * Resolves and checks the asset files of a parsed theme: each `fonts[].file` ({@link checkWoff2})
29
+ * and `logo.mark` / `logo.favicon` (`.svg` through {@link sanitizeThemeSvg}, `.png` through
30
+ * {@link checkPng}); the favicon must be square (SVG viewBox or PNG width = height). `files` holds the plugin's files keyed by their path without a leading
31
+ * `./`; the returned `assets` (the sanitized SVG bytes, the other files unchanged) use the same
32
+ * keys. Errors are `<field>: <reason>`, English; the total input size of the theme's distinct
33
+ * assets must stay within {@link THEME_LIMITS}`.assetsBytes`. That limit is per plugin, so a
34
+ * caller reading several themes also checks the sum over all of them (distinct paths).
35
+ */
36
+ export declare function validateThemeAssets(theme: ParsedTheme, files: Map<string, Uint8Array>): {
37
+ assets: Map<string, Uint8Array>;
38
+ errors: string[];
39
+ };