@norskvideo/ctl-sdk 0.1.56 → 0.1.58

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,7 @@
1
+ import type { ReactNode } from "react";
2
+ import { type VariableRow } from "./VariableRowsEditor.js";
3
+ export declare function LabelsEditor({ rows, onChange, addLabel, }: {
4
+ rows: VariableRow[];
5
+ onChange: (rows: VariableRow[]) => void;
6
+ addLabel?: string;
7
+ }): ReactNode;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { labelNameError } from "./label-rows.js";
3
+ import { VariableRowsEditor } from "./VariableRowsEditor.js";
4
+ export function LabelsEditor({ rows, onChange, addLabel = "Add label", }) {
5
+ const seen = new Set();
6
+ return (_jsx(VariableRowsEditor, { rows: rows, onChange: onChange, nameCase: "as-typed", noun: "label", addLabel: addLabel, namePlaceholder: "env", valuePlaceholder: "qa", nameError: (name) => {
7
+ const error = labelNameError(name.trim(), seen);
8
+ if (name.trim() !== "")
9
+ seen.add(name.trim());
10
+ return error;
11
+ } }));
12
+ }
@@ -3,7 +3,8 @@ import { ChevronDown, ChevronRight, X } from "lucide-react";
3
3
  import { useRef, useState } from "react";
4
4
  import toast from "react-hot-toast";
5
5
  import { validateInstanceId } from "../validate.js";
6
- import { parseLabelsText } from "./labels-text.js";
6
+ import { LabelsEditor } from "./LabelsEditor.js";
7
+ import { readLabelRows } from "./label-rows.js";
7
8
  import { ProductIframe } from "./ProductIframe.js";
8
9
  import { useUIPrimitives } from "./ui-primitives.js";
9
10
  const emptyAdvancedDefaults = {
@@ -38,8 +39,8 @@ function assembleAdvancedDefaults(d) {
38
39
  export function ProductTemplateBuildForm({ productName, configScreenUrl, onClose, sections, onSubmit, onSaved, onSavedAndLaunch, onRequirements, withLabels = false, }) {
39
40
  const { Button, Input, Label } = useUIPrimitives();
40
41
  const [productTemplateName, setProductTemplateName] = useState("");
41
- const [labelsDraft, setLabelsDraft] = useState("");
42
- const labels = parseLabelsText(labelsDraft);
42
+ const [labelRowsDraft, setLabelRowsDraft] = useState([]);
43
+ const labels = readLabelRows(labelRowsDraft);
43
44
  // The iframe can suggest a name derived from its current choices (#317).
44
45
  // We prefill the field with it while the operator hasn't typed anything,
45
46
  // so changing template/release keeps the suggestion live; once they edit
@@ -130,5 +131,5 @@ export function ProductTemplateBuildForm({ productName, configScreenUrl, onClose
130
131
  return (_jsxs("div", { className: "mb-6 rounded-lg border border-zinc-700 bg-zinc-900 p-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsxs("div", { children: [_jsxs("h4", { className: "text-sm font-semibold text-zinc-100", children: ["Create ", productName, " product template"] }), _jsx("p", { className: "text-xs text-zinc-500 mt-0.5", children: "Saves a reusable product template. Launch instances from it later." })] }), _jsx("button", { type: "button", onClick: onClose, className: "text-zinc-500 hover:text-zinc-300", children: _jsx(X, { size: 14 }) })] }), _jsxs("form", { onSubmit: handleSave, className: "space-y-4", children: [_jsxs("div", { children: [_jsx(Label, { children: "Product template name *" }), _jsx(Input, { value: productTemplateName, onChange: (e) => {
131
132
  setNameTouched(true);
132
133
  setProductTemplateName(e.target.value);
133
- }, placeholder: suggestedName || `e.g. ${productName}-prod`, required: true }), productTemplateNameError ? (_jsx("p", { className: "text-xs text-red-400 mt-1", children: productTemplateNameError })) : (_jsx("p", { className: "text-xs text-zinc-500 mt-1", children: "Lowercase letters, numbers, and hyphens only. Used to launch instances later." }))] }), withLabels && (_jsxs("div", { children: [_jsx(Label, { children: "Labels" }), _jsx(Input, { "aria-label": "Labels", value: labelsDraft, onChange: (e) => setLabelsDraft(e.target.value), placeholder: "e.g. env=qa,team=news" }), labels.ok ? (_jsx("p", { className: "text-xs text-zinc-500 mt-1", children: "name=value pairs joined by commas. Instances launched from it start with them; they decide who may see it, and what walls show it." })) : (_jsx("p", { className: "text-xs text-red-400 mt-1", children: labels.error }))] })), configScreenUrl ? (_jsx(ProductIframe, { ref: iframeRef, productName: productName, configScreenUrl: configScreenUrl, onValidityChange: (v) => setIframeValid(v), onSuggestName: handleSuggestName, ...(onRequirements ? { onRequirements } : {}) })) : (_jsxs("p", { className: "text-xs text-zinc-500", children: ["Product manifest does not declare ", _jsx("code", { children: "ui.configScreenUrl" }), " \u2014 nothing to configure."] })), sections, _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setDefaultsOpen((v) => !v), className: "flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-widest text-zinc-500 hover:text-zinc-300", children: [defaultsOpen ? _jsx(ChevronDown, { size: 12 }) : _jsx(ChevronRight, { size: 12 }), "Custom defaults"] }), defaultsOpen && (_jsxs("div", { className: "mt-2", children: [_jsx("p", { className: "text-xs text-zinc-500 mb-2", children: "Launch-form defaults baked into this template's manifest. Operators can still override them per instance." }), _jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsxs("div", { children: [_jsx(Label, { children: "Network mode" }), _jsxs("select", { "aria-label": "Network mode", value: advancedDraft.networkMode, onChange: (e) => patchDraft({ networkMode: e.target.value }), className: "block rounded border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm text-zinc-100", children: [_jsx("option", { value: "", children: "product default" }), _jsx("option", { value: "docker", children: "docker" }), _jsx("option", { value: "hybrid", children: "hybrid" })] })] }), _jsxs("div", { className: "min-w-[14rem] flex-1", children: [_jsx(Label, { children: "Host ports" }), _jsx(Input, { "aria-label": "Host ports", placeholder: "e.g. 1935, 5001/udp, 9000:80@svc", value: advancedDraft.hostPorts, onChange: (e) => patchDraft({ hostPorts: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Container user" }), _jsx(Input, { "aria-label": "Container user", placeholder: "uid:gid", value: advancedDraft.containerUser, onChange: (e) => patchDraft({ containerUser: e.target.value }), className: "w-28" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Shared memory size" }), _jsx(Input, { "aria-label": "Shared memory size", placeholder: "e.g. 8gb", value: advancedDraft.shmSize, onChange: (e) => patchDraft({ shmSize: e.target.value }), className: "w-24" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Open file limit" }), _jsx(Input, { "aria-label": "Open file limit", type: "number", min: 1024, max: 1048576, placeholder: "runner default", value: advancedDraft.openFileLimit, onChange: (e) => patchDraft({ openFileLimit: e.target.value }), className: "w-32" })] })] })] }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "primary", disabled: !canSubmit, onClick: handleSaveAndLaunch, children: submitting ? "Working…" : "Save & Launch…" }), _jsx(Button, { type: "submit", variant: "default", disabled: !canSubmit, children: "Save" }), _jsx(Button, { type: "button", variant: "ghost", onClick: onClose, children: "Cancel" })] })] })] }));
134
+ }, placeholder: suggestedName || `e.g. ${productName}-prod`, required: true }), productTemplateNameError ? (_jsx("p", { className: "text-xs text-red-400 mt-1", children: productTemplateNameError })) : (_jsx("p", { className: "text-xs text-zinc-500 mt-1", children: "Lowercase letters, numbers, and hyphens only. Used to launch instances later." }))] }), withLabels && (_jsxs("div", { children: [_jsx(Label, { children: "Labels" }), _jsx(LabelsEditor, { rows: labelRowsDraft, onChange: setLabelRowsDraft }), labels.ok ? (_jsx("p", { className: "text-xs text-zinc-500 mt-1", children: "name=value pairs joined by commas. Instances launched from it start with them; they decide who may see it, and what walls show it." })) : (_jsx("p", { className: "text-xs text-red-400 mt-1", children: labels.error }))] })), configScreenUrl ? (_jsx(ProductIframe, { ref: iframeRef, productName: productName, configScreenUrl: configScreenUrl, onValidityChange: (v) => setIframeValid(v), onSuggestName: handleSuggestName, ...(onRequirements ? { onRequirements } : {}) })) : (_jsxs("p", { className: "text-xs text-zinc-500", children: ["Product manifest does not declare ", _jsx("code", { children: "ui.configScreenUrl" }), " \u2014 nothing to configure."] })), sections, _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setDefaultsOpen((v) => !v), className: "flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-widest text-zinc-500 hover:text-zinc-300", children: [defaultsOpen ? _jsx(ChevronDown, { size: 12 }) : _jsx(ChevronRight, { size: 12 }), "Custom defaults"] }), defaultsOpen && (_jsxs("div", { className: "mt-2", children: [_jsx("p", { className: "text-xs text-zinc-500 mb-2", children: "Launch-form defaults baked into this template's manifest. Operators can still override them per instance." }), _jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsxs("div", { children: [_jsx(Label, { children: "Network mode" }), _jsxs("select", { "aria-label": "Network mode", value: advancedDraft.networkMode, onChange: (e) => patchDraft({ networkMode: e.target.value }), className: "block rounded border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm text-zinc-100", children: [_jsx("option", { value: "", children: "product default" }), _jsx("option", { value: "docker", children: "docker" }), _jsx("option", { value: "hybrid", children: "hybrid" })] })] }), _jsxs("div", { className: "min-w-[14rem] flex-1", children: [_jsx(Label, { children: "Host ports" }), _jsx(Input, { "aria-label": "Host ports", placeholder: "e.g. 1935, 5001/udp, 9000:80@svc", value: advancedDraft.hostPorts, onChange: (e) => patchDraft({ hostPorts: e.target.value }) })] }), _jsxs("div", { children: [_jsx(Label, { children: "Container user" }), _jsx(Input, { "aria-label": "Container user", placeholder: "uid:gid", value: advancedDraft.containerUser, onChange: (e) => patchDraft({ containerUser: e.target.value }), className: "w-28" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Shared memory size" }), _jsx(Input, { "aria-label": "Shared memory size", placeholder: "e.g. 8gb", value: advancedDraft.shmSize, onChange: (e) => patchDraft({ shmSize: e.target.value }), className: "w-24" })] }), _jsxs("div", { children: [_jsx(Label, { children: "Open file limit" }), _jsx(Input, { "aria-label": "Open file limit", type: "number", min: 1024, max: 1048576, placeholder: "runner default", value: advancedDraft.openFileLimit, onChange: (e) => patchDraft({ openFileLimit: e.target.value }), className: "w-32" })] })] })] }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "primary", disabled: !canSubmit, onClick: handleSaveAndLaunch, children: submitting ? "Working…" : "Save & Launch…" }), _jsx(Button, { type: "submit", variant: "default", disabled: !canSubmit, children: "Save" }), _jsx(Button, { type: "button", variant: "ghost", onClick: onClose, children: "Cancel" })] })] })] }));
134
135
  }
@@ -20,5 +20,10 @@ export interface VariableRowsEditorProps {
20
20
  showBackup?: boolean;
21
21
  addLabel?: string;
22
22
  namePlaceholder?: string;
23
+ valuePlaceholder?: string;
24
+ /** Env-var names are upper-cased as typed; a label's are kept as typed. */
25
+ nameCase?: "upper" | "as-typed";
26
+ /** What a row is, for the controls' accessible names. */
27
+ noun?: string;
23
28
  }
24
- export declare function VariableRowsEditor({ rows, onChange, nameError, showBackup, addLabel, namePlaceholder, }: VariableRowsEditorProps): ReactNode;
29
+ export declare function VariableRowsEditor({ rows, onChange, nameError, showBackup, addLabel, namePlaceholder, valuePlaceholder, nameCase, noun, }: VariableRowsEditorProps): ReactNode;
@@ -2,13 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Free-form environment-variable rows: NAME, a value, and (optionally) the
3
3
  // value the backup replica should use instead.
4
4
  //
5
- // Shared because this is the same editor in two places that otherwise cannot
6
- // share a form. The production composer uses it for variables the operator adds
7
- // beyond what the product template declares; the product-template create panel
8
- // uses it for the whole template tier, because at CREATION TIME there is no
9
- // declared parameter list to tabulate — the tar has not been built yet. That
10
- // asymmetry is why the composer's parameter TABLE cannot be reused here, and
11
- // why this row editor is the part that can.
5
+ // The one name/value editor (RFC 0008 §2): variables here, and labels through
6
+ // LabelsEditor, which keeps names as typed and checks them as labels. The
7
+ // product-template create panel uses it for the whole template tier, because
8
+ // at CREATION TIME there is no declared parameter list to tabulate. The
9
+ // production composer's added variables are rows of its parameter table
10
+ // instead (they share its columns with the declared parameters, and carry the
11
+ // ask-at-launch box), so they are not this editor.
12
12
  //
13
13
  // Controlled, and name validation is injected: the reserved-prefix and
14
14
  // uniqueness rules are the manager's vocabulary, not the SDK's.
@@ -19,12 +19,12 @@ export function newVariableRow() {
19
19
  seq += 1;
20
20
  return { id: `var-${seq}`, name: "", value: "" };
21
21
  }
22
- export function VariableRowsEditor({ rows, onChange, nameError, showBackup = false, addLabel = "Add variable", namePlaceholder = "MY_VAR", }) {
22
+ export function VariableRowsEditor({ rows, onChange, nameError, showBackup = false, addLabel = "Add variable", namePlaceholder = "MY_VAR", valuePlaceholder = "value", nameCase = "upper", noun = "variable", }) {
23
23
  const { Button, Input } = useUIPrimitives();
24
24
  const errors = rows.map((r, i) => nameError?.(r.name, i));
25
25
  const update = (id, patch) => onChange(rows.map((r) => (r.id === id ? { ...r, ...patch } : r)));
26
- return (_jsxs("div", { className: "space-y-1.5", children: [rows.map((row, i) => (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "w-48 shrink-0", children: _jsx(Input, { "aria-label": "variable name", className: "font-mono", placeholder: namePlaceholder, value: row.name,
26
+ return (_jsxs("div", { className: "space-y-1.5", children: [rows.map((row, i) => (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "w-48 shrink-0", children: _jsx(Input, { "aria-label": `${noun} name`, className: "font-mono", placeholder: namePlaceholder, value: row.name,
27
27
  // Env-var names are conventionally upper-case; doing it here
28
28
  // saves the operator shouting and keeps the two hosts identical.
29
- onChange: (e) => update(row.id, { name: e.target.value.toUpperCase() }) }) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(Input, { "aria-label": `value for ${row.name || "new variable"}`, placeholder: "value", value: row.value, onChange: (e) => update(row.id, { value: e.target.value }) }) }), showBackup && (_jsx("div", { className: "min-w-0 flex-1", children: _jsx(Input, { "aria-label": `backup value for ${row.name || "new variable"}`, placeholder: "same as primary", value: row.backupValue ?? "", onChange: (e) => update(row.id, { backupValue: e.target.value }) }) })), _jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": `remove variable ${row.name || "row"}`, onClick: () => onChange(rows.filter((r) => r.id !== row.id)), children: _jsx(Trash2, { size: 13 }) })] }), errors[i] && _jsx("p", { className: "mt-0.5 text-xs text-amber-400", children: errors[i] })] }, row.id))), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onChange([...rows, newVariableRow()]), children: [_jsx(Plus, { size: 12 }), " ", addLabel] })] }));
29
+ onChange: (e) => update(row.id, { name: nameCase === "upper" ? e.target.value.toUpperCase() : e.target.value }) }) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(Input, { "aria-label": `value for ${row.name || `new ${noun}`}`, placeholder: valuePlaceholder, value: row.value, onChange: (e) => update(row.id, { value: e.target.value }) }) }), showBackup && (_jsx("div", { className: "min-w-0 flex-1", children: _jsx(Input, { "aria-label": `backup value for ${row.name || `new ${noun}`}`, placeholder: "same as primary", value: row.backupValue ?? "", onChange: (e) => update(row.id, { backupValue: e.target.value }) }) })), _jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": `remove ${noun} ${row.name || "row"}`, onClick: () => onChange(rows.filter((r) => r.id !== row.id)), children: _jsx(Trash2, { size: 13 }) })] }), errors[i] && _jsx("p", { className: "mt-0.5 text-xs text-amber-400", children: errors[i] })] }, row.id))), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onChange([...rows, newVariableRow()]), children: [_jsx(Plus, { size: 12 }), " ", addLabel] })] }));
30
30
  }
@@ -1,4 +1,6 @@
1
1
  export { type ComposeLayer, ComposeLayerPreview, type ComposeLayerPreviewProps } from "./ComposeLayerPreview.js";
2
+ export { LabelsEditor } from "./LabelsEditor.js";
3
+ export { labelNameError, labelRows, readLabelRows } from "./label-rows.js";
2
4
  export { type LabelsTextResult, labelsText, parseLabelsText } from "./labels-text.js";
3
5
  export { ProductIframe, type ProductIframeHandle, type SubmitResult } from "./ProductIframe.js";
4
6
  export { ProductTemplateBuildForm, type ProductTemplateBuildOutcome, type ProductTemplateBuildRequest, } from "./ProductTemplateBuildForm.js";
@@ -1,4 +1,6 @@
1
1
  export { ComposeLayerPreview } from "./ComposeLayerPreview.js";
2
+ export { LabelsEditor } from "./LabelsEditor.js";
3
+ export { labelNameError, labelRows, readLabelRows } from "./label-rows.js";
2
4
  export { labelsText, parseLabelsText } from "./labels-text.js";
3
5
  export { ProductIframe } from "./ProductIframe.js";
4
6
  export { ProductTemplateBuildForm, } from "./ProductTemplateBuildForm.js";
@@ -0,0 +1,7 @@
1
+ import type { LabelsTextResult } from "./labels-text.js";
2
+ import { type VariableRow } from "./VariableRowsEditor.js";
3
+ /** Why `name` cannot be a label here, or undefined. `seen` holds the names
4
+ * of the rows above it. An empty name is a row not yet filled in. */
5
+ export declare function labelNameError(name: string, seen: ReadonlySet<string>): string | undefined;
6
+ export declare function labelRows(labels: Readonly<Record<string, string>>): VariableRow[];
7
+ export declare function readLabelRows(rows: readonly VariableRow[]): LabelsTextResult;
@@ -0,0 +1,47 @@
1
+ // Labels as rows of the name/value editor (RFC 0008 §2: one editor for
2
+ // labels and variables alike). ADR-0015's rules, mirrored so the editor can
3
+ // say what is wrong as it is typed; the platform still checks every set.
4
+ import { newVariableRow } from "./VariableRowsEditor.js";
5
+ const NAME = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
6
+ const MAX = 64;
7
+ const VALUE_FORBIDDEN = /[,=[\]]/;
8
+ /** Why `name` cannot be a label here, or undefined. `seen` holds the names
9
+ * of the rows above it. An empty name is a row not yet filled in. */
10
+ export function labelNameError(name, seen) {
11
+ if (name === "")
12
+ return undefined;
13
+ if (name.startsWith("norsk."))
14
+ return "names under norsk. are the platform's";
15
+ if (!NAME.test(name) || name.length > MAX)
16
+ return "letters, digits and . _ / -, at most 64";
17
+ if (seen.has(name))
18
+ return `'${name}' is named twice`;
19
+ return undefined;
20
+ }
21
+ export function labelRows(labels) {
22
+ return Object.entries(labels)
23
+ .sort(([a], [b]) => a.localeCompare(b))
24
+ .map(([name, value]) => ({ ...newVariableRow(), name, value }));
25
+ }
26
+ export function readLabelRows(rows) {
27
+ const labels = {};
28
+ const seen = new Set();
29
+ for (const { name, value } of rows) {
30
+ const n = name.trim();
31
+ const v = value.trim();
32
+ if (n === "" && v === "")
33
+ continue;
34
+ if (n === "")
35
+ return { ok: false, error: `the value '${v}' has no name` };
36
+ const problem = labelNameError(n, seen);
37
+ if (problem)
38
+ return { ok: false, error: `${n}: ${problem}` };
39
+ if (v === "")
40
+ return { ok: false, error: `${n} has no value` };
41
+ if (VALUE_FORBIDDEN.test(v) || v.length > MAX)
42
+ return { ok: false, error: `${n}: a value has no , = [ or ]` };
43
+ seen.add(n);
44
+ labels[n] = v;
45
+ }
46
+ return { ok: true, labels };
47
+ }
package/fleet.d.ts CHANGED
@@ -109,6 +109,16 @@ export interface FleetOptions {
109
109
  WebSocket?: new (url: string) => WebSocketLike;
110
110
  /** The page's origin, for a live operation's ws(s) URL; the page's own by default. */
111
111
  origin?: string;
112
+ /** The page's path, which says which instance it is (RFC 0008); its own by default. */
113
+ path?: string;
114
+ }
115
+ /** The dashboard instance a page is, and how it is configured (RFC 0008). */
116
+ export interface FleetInstance {
117
+ name: string;
118
+ displayName: string;
119
+ /** `<product>/<name>`, for an instance of a product's definition. */
120
+ definition?: string;
121
+ parameters: Record<string, string>;
112
122
  }
113
123
  export interface Fleet {
114
124
  query(selection: FleetSelection): Promise<FleetDirectory>;
@@ -117,6 +127,8 @@ export interface Fleet {
117
127
  call<T = unknown>(runtimeId: string, operationId: string, input?: FleetCallInput): Promise<T>;
118
128
  /** Open a runtime's declared live operation. */
119
129
  live(runtimeId: string, operationId: string, handlers: FleetLiveHandlers, input?: FleetCallInput): Promise<FleetLive>;
130
+ /** Which instance this page is, and its parameters. */
131
+ instance(): Promise<FleetInstance>;
120
132
  state: {
121
133
  get<T = unknown>(key: string): Promise<FleetStateDocument<T> | undefined>;
122
134
  /** Write at `revision` (omit for a new key); a FleetStateConflict when
package/fleet.js CHANGED
@@ -94,6 +94,13 @@ export function fleetClient(options = {}) {
94
94
  socket.addEventListener("close", () => handlers.onClose?.());
95
95
  return { send: (data) => socket.send(data), close: () => socket.close() };
96
96
  },
97
+ async instance() {
98
+ const path = options.path ?? globalThis.location?.pathname ?? "/";
99
+ const res = await doFetch(`${base}/instance?path=${encodeURIComponent(path)}`, { credentials: "same-origin" });
100
+ if (!res.ok)
101
+ throw new Error(`the dashboard instance answered ${res.status}`);
102
+ return (await res.json());
103
+ },
97
104
  state: {
98
105
  async get(key) {
99
106
  const res = await doFetch(`${base}/state/${encodeURIComponent(key)}`, { credentials: "same-origin" });
@@ -9,7 +9,7 @@ declare const DefaultProductTemplateSchema: z.ZodObject<{
9
9
  url: z.ZodString;
10
10
  }, z.core.$strip>;
11
11
  export type DefaultProductTemplate = z.infer<typeof DefaultProductTemplateSchema>;
12
- declare const DefaultDashboardSchema: z.ZodObject<{
12
+ export declare const DefaultDashboardSchema: z.ZodObject<{
13
13
  name: z.ZodString;
14
14
  displayName: z.ZodOptional<z.ZodString>;
15
15
  url: z.ZodString;
@@ -30,6 +30,16 @@ declare const DefaultDashboardSchema: z.ZodObject<{
30
30
  label: z.ZodString;
31
31
  }, z.core.$strip>], "kind">;
32
32
  }, z.core.$strip>], "mode">>;
33
+ instances: z.ZodOptional<z.ZodEnum<{
34
+ single: "single";
35
+ many: "many";
36
+ }>>;
37
+ parameters: z.ZodOptional<z.ZodArray<z.ZodObject<{
38
+ name: z.ZodString;
39
+ purpose: z.ZodString;
40
+ default: z.ZodOptional<z.ZodString>;
41
+ required: z.ZodOptional<z.ZodBoolean>;
42
+ }, z.core.$strip>>>;
33
43
  }, z.core.$strip>;
34
44
  export type DefaultDashboard = z.infer<typeof DefaultDashboardSchema>;
35
45
  /** One product role (RFC 0003 D5a). The name rule is foundation's, so the
@@ -246,6 +256,16 @@ export declare const ManifestSchema: z.ZodObject<{
246
256
  label: z.ZodString;
247
257
  }, z.core.$strip>], "kind">;
248
258
  }, z.core.$strip>], "mode">>;
259
+ instances: z.ZodOptional<z.ZodEnum<{
260
+ single: "single";
261
+ many: "many";
262
+ }>>;
263
+ parameters: z.ZodOptional<z.ZodArray<z.ZodObject<{
264
+ name: z.ZodString;
265
+ purpose: z.ZodString;
266
+ default: z.ZodOptional<z.ZodString>;
267
+ required: z.ZodOptional<z.ZodBoolean>;
268
+ }, z.core.$strip>>>;
249
269
  }, z.core.$strip>>>;
250
270
  export: z.ZodOptional<z.ZodObject<{
251
271
  include: z.ZodDefault<z.ZodArray<z.ZodString>>;
@@ -90,7 +90,7 @@ const DefaultProductTemplateSchema = z.object({
90
90
  const DashboardLabel = z
91
91
  .string()
92
92
  .regex(/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/, "one DNS label: lowercase letters, digits and hyphens, 1-63 chars");
93
- const DefaultDashboardSchema = z.object({
93
+ export const DefaultDashboardSchema = z.object({
94
94
  name: DashboardLabel.meta({ description: "The dashboard's name, and its host's label when it has one of its own." }),
95
95
  displayName: z.string().min(1).optional(),
96
96
  url: z
@@ -113,6 +113,19 @@ const DefaultDashboardSchema = z.object({
113
113
  ])
114
114
  .optional()
115
115
  .meta({ description: "The hosting proposed; absent is a host of its own." }),
116
+ // RFC 0008: the entry is a definition, of one instance or many.
117
+ instances: z.enum(["single", "many"]).optional().meta({
118
+ description: "single (the default): one instance, made when the product is added. many: none until an operator makes them, each with its own parameters and host.",
119
+ }),
120
+ parameters: z
121
+ .array(z.object({
122
+ name: z.string().regex(/^[A-Z][A-Z0-9_]*$/, "upper-case letters, digits and underscores"),
123
+ purpose: z.string().min(1),
124
+ default: z.string().optional(),
125
+ required: z.boolean().optional(),
126
+ }))
127
+ .optional()
128
+ .meta({ description: "What each instance is asked for when it is made; the page reads its answers." }),
116
129
  });
117
130
  /** Save-as-product-template file selection. When the runner exports a running
118
131
  * instance's working directory into a new product template, it packs only files
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-sdk",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {