@jxsuite/protocol 2.6.1 → 3.0.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/protocol",
3
- "version": "2.6.1",
3
+ "version": "3.0.0",
4
4
  "description": "The Studio Backend Protocol — wire types and the canonical route table every Jx Studio backend implements",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,6 @@
27
27
  "test:coverage": "bun test --isolate --coverage"
28
28
  },
29
29
  "dependencies": {
30
- "@jxsuite/schema": "^3.0.0"
30
+ "@jxsuite/schema": "^4.0.0"
31
31
  }
32
32
  }
package/src/index.ts CHANGED
@@ -10,4 +10,4 @@ export * from "./problems";
10
10
  export * from "./routes";
11
11
  export type * from "./types";
12
12
  // The one runtime value types.ts carries: the batch-read limit a backend and a client both size to.
13
- export { READ_FILES_MAX_PATHS } from "./types";
13
+ export { IMPORT_MAX_BREAKPOINTS, IMPORT_MAX_PAGES, READ_FILES_MAX_PATHS } from "./types";
package/src/problem.ts CHANGED
@@ -10,10 +10,10 @@
10
10
  * decisions follow from that and are worth stating, because both look arbitrary until they are
11
11
  * explained:
12
12
  *
13
- * - **`type` is absolute.** RFC 9457 permits a relative reference, resolved against the request
14
- * URL — which on a dev server is `http://127.0.0.1:3000/problems/…` and serves nothing. An
15
- * absolute `https://jxsuite.com/problems/…` means the same thing everywhere, which is the whole
16
- * point of identifying a type by URI.
13
+ * - **`type` is absolute.** RFC 9457 permits a relative reference, resolved against the request URL —
14
+ * which on a dev server is `http://127.0.0.1:3000/problems/…` and serves nothing. An absolute
15
+ * `https://jxsuite.com/problems/…` means the same thing everywhere, which is the whole point of
16
+ * identifying a type by URI.
17
17
  * - **`instance` is never emitted.** It identifies the specific occurrence, and Jx has no
18
18
  * per-occurrence resource to point at. A field whose only possible value is a fabricated URI is
19
19
  * noise that looks like information.
@@ -21,6 +21,8 @@
21
21
  * @license MIT
22
22
  */
23
23
 
24
+ import type { JsonValue } from "@jxsuite/schema/types";
25
+
24
26
  /** The media type a problem response carries (RFC 9457 §3). */
25
27
  export const PROBLEM_MEDIA_TYPE = "application/problem+json";
26
28
 
@@ -54,13 +56,58 @@ export interface ProblemDetails {
54
56
  * @deprecated Read `detail`. This field is removed once every client has migrated.
55
57
  */
56
58
  error?: string;
57
- /** Extension members (RFC 9457 §3.2) — anything the type documents. */
58
- [extension: string]: unknown;
59
+ /**
60
+ * Extension members (RFC 9457 §3.2) — anything the type documents. A problem is a JSON document,
61
+ * so every member is a JSON value.
62
+ */
63
+ [extension: string]: JsonValue | undefined;
59
64
  }
60
65
 
61
- /** The slug identifying a Jx problem type, i.e. the last segment of its `type` URI. */
62
- export function problemSlug(type: unknown): string | null {
63
- if (typeof type !== "string" || !type.startsWith(PROBLEM_TYPE_BASE)) {
66
+ /**
67
+ * The members of a failure body {@link problemDetail} consults, in the order it consults them.
68
+ *
69
+ * Each is untrusted: the body arrived from a backend of unknown vintage, so a member may hold any
70
+ * JSON value, and only a non-blank string counts as an answer.
71
+ */
72
+ export interface FailureBodyText {
73
+ detail?: JsonValue;
74
+ error?: JsonValue;
75
+ title?: JsonValue;
76
+ }
77
+
78
+ /**
79
+ * A parsed failure body, whatever shape it turned out to be: a bare text body, any other JSON
80
+ * value, or a typed body carrying (or lacking) the {@link FailureBodyText} members.
81
+ */
82
+ export type FailureBody = JsonValue | FailureBodyText | undefined;
83
+
84
+ /*
85
+ * The two narrowings the readers below make on wire data. They are local rather than imported from
86
+ * `@jxsuite/schema/guards` because this module is part of the AI gateway's worker graph, which
87
+ * `packages/ai/tests/worker-safety.test.ts` freezes to the protocol's two problem modules.
88
+ */
89
+
90
+ /** True when a wire value is a string — blank or not; the caller decides what blank means. */
91
+ function isText(value: FailureBody): value is string {
92
+ return typeof value === "string";
93
+ }
94
+
95
+ /**
96
+ * True when a failure body is an object, whose text members may or may not be present; false for
97
+ * null and every primitive. An array passes and simply carries none of the members.
98
+ */
99
+ function isFailureBodyText(body: FailureBody): body is FailureBodyText {
100
+ return typeof body === "object" && body !== null;
101
+ }
102
+
103
+ /**
104
+ * The slug identifying a Jx problem type, i.e. the last segment of its `type` URI.
105
+ *
106
+ * @param {JsonValue | undefined} type - A body's `type` member, as parsed off the wire
107
+ * @returns {string | null}
108
+ */
109
+ export function problemSlug(type: JsonValue | undefined): string | null {
110
+ if (!isText(type) || !type.startsWith(PROBLEM_TYPE_BASE)) {
64
111
  return null;
65
112
  }
66
113
  const slug = type.slice(PROBLEM_TYPE_BASE.length);
@@ -72,8 +119,12 @@ export function isProblemDetails(value: unknown): value is ProblemDetails {
72
119
  if (!value || typeof value !== "object") {
73
120
  return false;
74
121
  }
75
- const candidate = value as Partial<ProblemDetails>;
76
- return typeof candidate.type === "string" && typeof candidate.status === "number";
122
+ return (
123
+ "type" in value &&
124
+ typeof value.type === "string" &&
125
+ "status" in value &&
126
+ typeof value.status === "number"
127
+ );
77
128
  }
78
129
 
79
130
  /**
@@ -87,19 +138,18 @@ export function isProblemDetails(value: unknown): value is ProblemDetails {
87
138
  * generic one: "Failed to list connections" beats "Request failed (500)". Use {@link problemMessage}
88
139
  * when there is no such context.
89
140
  *
90
- * @param {unknown} body - A parsed response body, whatever shape it turned out to be
141
+ * @param {FailureBody} body - A parsed response body, whatever shape it turned out to be
91
142
  * @returns {string | null}
92
143
  */
93
- export function problemDetail(body: unknown): string | null {
94
- if (typeof body === "string") {
144
+ export function problemDetail(body: FailureBody): string | null {
145
+ if (isText(body)) {
95
146
  return body.trim() === "" ? null : body;
96
147
  }
97
- if (!body || typeof body !== "object") {
148
+ if (!isFailureBodyText(body)) {
98
149
  return null;
99
150
  }
100
- const candidate = body as { detail?: unknown; error?: unknown; title?: unknown };
101
- for (const value of [candidate.detail, candidate.error, candidate.title]) {
102
- if (typeof value === "string" && value.trim() !== "") {
151
+ for (const value of [body.detail, body.error, body.title]) {
152
+ if (isText(value) && value.trim() !== "") {
103
153
  return value;
104
154
  }
105
155
  }
@@ -114,11 +164,11 @@ export function problemDetail(body: unknown): string | null {
114
164
  * is most-specific-first, because a `title` describes the type rather than the occurrence and is
115
165
  * the less useful of the two whenever a `detail` exists.
116
166
  *
117
- * @param {unknown} body - A parsed response body, whatever shape it turned out to be
167
+ * @param {FailureBody} body - A parsed response body, whatever shape it turned out to be
118
168
  * @param {number} [status] - The response status, used only when the body says nothing
119
169
  * @returns {string}
120
170
  */
121
- export function problemMessage(body: unknown, status?: number): string {
171
+ export function problemMessage(body: FailureBody, status?: number): string {
122
172
  return (
123
173
  problemDetail(body) ?? (status === undefined ? "Request failed" : `Request failed (${status})`)
124
174
  );
package/src/problems.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  * @license MIT
14
14
  */
15
15
 
16
+ import type { JsonObject, JsonValue } from "@jxsuite/schema/types";
16
17
  import { PROBLEM_TYPE_BASE } from "./problem.ts";
17
18
  import type { ProblemDetails } from "./problem.ts";
18
19
 
@@ -33,12 +34,13 @@ const problem = (
33
34
  status: number,
34
35
  title: string,
35
36
  extensions?: readonly string[],
36
- ): ProblemType => ({
37
- status,
38
- title,
39
- type: `${PROBLEM_TYPE_BASE}${slug}`,
40
- ...(extensions === undefined ? {} : { extensions }),
41
- });
37
+ ): ProblemType => {
38
+ const declared: ProblemType = { status, title, type: `${PROBLEM_TYPE_BASE}${slug}` };
39
+ if (extensions !== undefined) {
40
+ declared.extensions = extensions;
41
+ }
42
+ return declared;
43
+ };
42
44
 
43
45
  /**
44
46
  * Every problem type a Jx backend may answer with, keyed by a stable name.
@@ -112,28 +114,39 @@ export type ProblemTypeName = keyof typeof PROBLEM_TYPES;
112
114
  *
113
115
  * @param {ProblemTypeName} name - A declared type
114
116
  * @param {string} [detail] - What happened THIS time
115
- * @param {Record<string, unknown>} [extensions] - Extension members this type documents
117
+ * @param {JsonObject} [extensions] - Extension members this type documents
116
118
  * @returns {ProblemDetails}
117
119
  */
118
120
  export function problemDetails(
119
121
  name: ProblemTypeName,
120
122
  detail?: string,
121
- extensions: Record<string, unknown> = {},
123
+ extensions: JsonObject = {},
122
124
  ): ProblemDetails {
123
125
  const declared = PROBLEM_TYPES[name];
124
- return {
126
+ const body: ProblemDetails = {
125
127
  ...extensions,
126
128
  status: declared.status,
127
129
  title: declared.title,
128
130
  type: declared.type,
129
- ...(detail === undefined || detail === "" ? {} : { detail, error: detail }),
130
131
  };
132
+ if (detail !== undefined && detail !== "") {
133
+ /*
134
+ * The one place the deprecated `error` alias is written (see ProblemDetails.error). Its
135
+ * deprecation addresses readers; the writer owes it to every unmigrated client for one release,
136
+ * and deleting the alias is deleting `error: detail` here.
137
+ */
138
+ Object.assign(body, { detail, error: detail });
139
+ }
140
+ return body;
131
141
  }
132
142
 
133
- /** The declared type a `type` URI names, or null when it is not one of ours. */
134
- export function problemTypeByUri(type: unknown): ProblemType | null {
135
- if (typeof type !== "string") {
136
- return null;
137
- }
143
+ /**
144
+ * The declared type a `type` URI names, or null when it is not one of ours.
145
+ *
146
+ * @param {JsonValue | undefined} type - A body's `type` member, as parsed off the wire
147
+ * @returns {ProblemType | null}
148
+ */
149
+ export function problemTypeByUri(type: JsonValue | undefined): ProblemType | null {
150
+ // Every declared `type` is a string, so a non-string member matches nothing and answers null.
138
151
  return Object.values(PROBLEM_TYPES).find((declared) => declared.type === type) ?? null;
139
152
  }
package/src/routes.ts CHANGED
@@ -44,13 +44,18 @@ const route = (
44
44
  path: string,
45
45
  summary: string,
46
46
  degradation?: string,
47
- ): StudioRoute => ({
48
- method,
49
- path,
50
- optional: degradation !== undefined,
51
- summary,
52
- ...(degradation === undefined ? {} : { degradation }),
53
- });
47
+ ): StudioRoute => {
48
+ const declared: StudioRoute = {
49
+ method,
50
+ path,
51
+ optional: degradation !== undefined,
52
+ summary,
53
+ };
54
+ if (degradation !== undefined) {
55
+ declared.degradation = degradation;
56
+ }
57
+ return declared;
58
+ };
54
59
 
55
60
  /**
56
61
  * Every protocol route, keyed by a stable name. Paths are the dev server's literal `/__studio/*`
@@ -496,11 +501,19 @@ export const STUDIO_ROUTES = {
496
501
  /** A stable route name in {@link STUDIO_ROUTES}. */
497
502
  export type StudioRouteName = keyof typeof STUDIO_ROUTES;
498
503
 
504
+ /** Every route name, in declaration order. */
505
+ function routeNames(): StudioRouteName[] {
506
+ /*
507
+ * SAFETY: StudioRouteName is derived from this very literal (`keyof typeof STUDIO_ROUTES`), its
508
+ * members are readonly under `as const`, and nothing in the protocol adds keys at runtime, so its
509
+ * own enumerable string keys are exactly the StudioRouteName members.
510
+ */
511
+ return Object.keys(STUDIO_ROUTES) as StudioRouteName[];
512
+ }
513
+
499
514
  /** Names of every route a minimal (core) backend must implement. */
500
515
  export function coreRouteNames(): StudioRouteName[] {
501
- return (Object.keys(STUDIO_ROUTES) as StudioRouteName[]).filter(
502
- (name) => !STUDIO_ROUTES[name].optional,
503
- );
516
+ return routeNames().filter((name) => !STUDIO_ROUTES[name].optional);
504
517
  }
505
518
 
506
519
  /**
@@ -508,7 +521,5 @@ export function coreRouteNames(): StudioRouteName[] {
508
521
  * optimisation with a fallback).
509
522
  */
510
523
  export function optionalRouteNames(): StudioRouteName[] {
511
- return (Object.keys(STUDIO_ROUTES) as StudioRouteName[]).filter(
512
- (name) => STUDIO_ROUTES[name].optional,
513
- );
524
+ return routeNames().filter((name) => STUDIO_ROUTES[name].optional);
514
525
  }
package/src/types.ts CHANGED
@@ -7,7 +7,12 @@
7
7
  * @license MIT
8
8
  */
9
9
 
10
- import type { JsonValue as SchemaJsonValue, JxMutableNode } from "@jxsuite/schema/types";
10
+ import type { ProjectBlock, StudioHints } from "@jxsuite/schema/format-registry";
11
+ import type {
12
+ JsonObject,
13
+ JsonValue as SchemaJsonValue,
14
+ JxMutableNode,
15
+ } from "@jxsuite/schema/types";
11
16
 
12
17
  /**
13
18
  * A JSON document value, or `undefined` to signal property removal in the mutators. Re-uses the
@@ -30,7 +35,7 @@ export interface ErrorBody {
30
35
  error: string;
31
36
  /** Machine-readable discriminator (e.g. "remote_moved", "cf_not_connected"). */
32
37
  code?: string;
33
- detail?: unknown;
38
+ detail?: SchemaJsonValue;
34
39
  }
35
40
 
36
41
  // ─── Filesystem ──────────────────────────────────────────────────────────────
@@ -95,11 +100,12 @@ export interface UploadResult {
95
100
 
96
101
  /**
97
102
  * The most paths one `filesRead` request may name. A backend answers at most this many and lists
98
- * the rest under {@link ReadFilesResult.omitted}, so a client that sends more is not refused — it is
99
- * answered in part, and asks again.
103
+ * the rest under {@link ReadFilesResult.omitted}, so a client that sends more is not refused — it
104
+ * is answered in part, and asks again.
100
105
  *
101
- * The one runtime value in this file: a client sizes its batches to it, so it has to exist at
102
- * runtime rather than only in a summary string.
106
+ * One of this file's three runtime values (with {@link IMPORT_MAX_PAGES} and
107
+ * {@link IMPORT_MAX_BREAKPOINTS}): a client sizes its batches to it, so it has to exist at runtime
108
+ * rather than only in a summary string.
103
109
  */
104
110
  export const READ_FILES_MAX_PATHS = 200;
105
111
 
@@ -154,7 +160,7 @@ export interface AssetCapabilities {
154
160
  export interface ReferenceHit {
155
161
  /**
156
162
  * Which key carried it: `$ref`, `$layout`, `$src`, `attr`, `imports`, `$elements`, `url`,
157
- * `tagName`.
163
+ * `source`, `copy`, `tagName`.
158
164
  */
159
165
  refType: string;
160
166
  /** The reference exactly as authored (`"./Card.json"`), or `"<my-card>"` for a tag instance. */
@@ -195,8 +201,13 @@ export interface ReferencesResult {
195
201
  // ─── Git ─────────────────────────────────────────────────────────────────────
196
202
 
197
203
  export interface GitFileStatus {
204
+ /**
205
+ * One porcelain letter — `M`, `A`, `D`, `R`, `U` — never a word and never the raw two-column
206
+ * code.
207
+ */
198
208
  status: string;
199
209
  path: string;
210
+ /** Whether this change is in the index. Absent reads as unstaged. */
200
211
  staged?: boolean;
201
212
  }
202
213
 
@@ -239,34 +250,32 @@ export interface ComponentMeta {
239
250
  tagName: string;
240
251
  $id?: string | null;
241
252
  path: string;
242
- props?: { name: string; type?: string; default?: JsonValue; [k: string]: unknown }[];
253
+ props?: { name: string; type?: string; default?: JsonValue; [k: string]: JsonValue }[];
243
254
  slots?: ComponentSlotMeta[];
244
255
  hasElements?: boolean;
256
+ /** The custom-element tags the component's own tree renders (`componentMetaFrom`'s `uses`). */
257
+ uses?: string[];
245
258
  }
246
259
 
247
260
  // ─── Extensions (specs/extensions.md §9/§9.1) ────────────────────────────────
248
261
 
249
- /** The `project` admission block of a section-owning extension class. */
250
- export interface ExtensionProjectBlock {
251
- /** The project.json top-level property the class owns. */
252
- key: string;
253
- title?: string;
254
- description?: string;
255
- referenceable?: boolean;
256
- }
262
+ /* The `project` admission block of a section-owning extension class: the descriptor's own block,
263
+ declared once in @jxsuite/schema and served here unchanged. */
264
+ export type { ProjectBlock as ExtensionProjectBlock } from "@jxsuite/schema/format-registry";
257
265
 
258
266
  /** One project-section contribution of an extension class, as served on the formats route. */
259
267
  export interface ExtensionContributionInfo {
260
268
  /** The $prototype-visible class name declaring the `project` block. */
261
269
  className: string;
262
- project: ExtensionProjectBlock;
270
+ project: ProjectBlock;
263
271
  /** The class descriptor's `$studio` block (settings-section vocabulary), when declared. */
264
- studio?: Record<string, unknown> | null;
272
+ studio?: StudioHints | null;
265
273
  /**
266
- * The section's value schema — `properties[<key>]` of the extension's shipped project fragment —
267
- * resolved backend-side; null when the extension ships no fragment (or it lacks the key).
274
+ * The section's value schema — `properties[<key>]` of the extension's shipped project fragment,
275
+ * which JSON Schema lets be a boolean as well as an object — resolved backend-side; null when the
276
+ * extension ships no fragment (or it lacks the key).
268
277
  */
269
- entrySchema?: Record<string, unknown> | null;
278
+ entrySchema?: SchemaJsonValue;
270
279
  }
271
280
 
272
281
  /**
@@ -292,7 +301,7 @@ export interface ExtensionsInfo {
292
301
  name: string;
293
302
  path: string;
294
303
  state?: boolean;
295
- stateDefaults?: Record<string, unknown>;
304
+ stateDefaults?: JsonObject;
296
305
  }[];
297
306
  }
298
307
 
@@ -369,8 +378,8 @@ export interface ExtensionCatalogEntry {
369
378
  * editors can register them without resolving relative `$ref`s.
370
379
  */
371
380
  export interface ProjectSchemasResponse {
372
- project?: Record<string, unknown> | null;
373
- document?: Record<string, unknown> | null;
381
+ project?: JsonObject | null;
382
+ document?: JsonObject | null;
374
383
  }
375
384
 
376
385
  // ─── Data surface (connector domain; specs/extensions.md §13) ───────────────
@@ -397,7 +406,7 @@ export interface DataConnectionInfo {
397
406
  name: string;
398
407
  provider: string;
399
408
  /** The connection's project.json value — identifiers and env-var names, never secrets. */
400
- settings: Record<string, unknown>;
409
+ settings: JsonObject;
401
410
  /** True when every env-var name the connection references resolves in the backend env. */
402
411
  configured: boolean;
403
412
  /** Referenced env-var NAMES with no value in the backend environment. */
@@ -419,11 +428,9 @@ export interface DataConnectionsResponse {
419
428
  connections: DataConnectionInfo[];
420
429
  }
421
430
 
422
- /** Result of the connection-test route (mirrors the connector's testConnection capability). */
423
- export interface DataConnectionTestResult {
424
- ok: boolean;
425
- error?: string;
426
- }
431
+ /* Result of the connection-test route: the connector `testConnection` capability's own result,
432
+ declared once in @jxsuite/schema and relayed unchanged. */
433
+ export type { TestConnectionResult as DataConnectionTestResult } from "@jxsuite/schema/format-registry";
427
434
 
428
435
  /**
429
436
  * A push-plan step kind. Open string union: connector DDL kinds today, and future push-step
@@ -450,13 +457,9 @@ export interface DataPushStep {
450
457
  connection?: string;
451
458
  }
452
459
 
453
- /** Request body of the data-push route. */
454
- export interface DataPushRequest {
455
- /** Restrict the push to one connection; omitted = every declared connection. */
456
- connection?: string;
457
- /** Compile the plan without executing it. */
458
- dryRun?: boolean;
459
- }
460
+ /* Request body of the data-push route: the same request `jx db push` takes, declared once in
461
+ @jxsuite/schema. */
462
+ export type { DataPushRequest } from "@jxsuite/schema/format-registry";
460
463
 
461
464
  /** Result of the data-push route. */
462
465
  export interface DataPushResult {
@@ -491,7 +494,7 @@ export interface DataColumnMeta {
491
494
 
492
495
  /** Result of the data-rows route. */
493
496
  export interface DataRowsResult {
494
- rows: Record<string, unknown>[];
497
+ rows: JsonObject[];
495
498
  /** Total row count of the table (for pagination). */
496
499
  total: number;
497
500
  columns: DataColumnMeta[];
@@ -501,7 +504,7 @@ export interface DataRowsResult {
501
504
  export interface DataRowInsert {
502
505
  table: string;
503
506
  connection?: string;
504
- values: Record<string, unknown>;
507
+ values: JsonObject;
505
508
  }
506
509
 
507
510
  /** Body of the row-update route (keyed on the introspected primary key). */
@@ -509,7 +512,7 @@ export interface DataRowUpdate {
509
512
  table: string;
510
513
  connection?: string;
511
514
  pk: string | number;
512
- set: Record<string, unknown>;
515
+ set: JsonObject;
513
516
  }
514
517
 
515
518
  /** Parameters of the row-delete route. */
@@ -526,36 +529,35 @@ export interface SecretsListResponse {
526
529
  names: string[];
527
530
  }
528
531
 
529
- /** Request body of the secrets-set route (values flow in, never back out). */
530
- export interface SecretsSetRequest {
531
- set?: Record<string, string>;
532
- remove?: string[];
533
- }
534
-
535
532
  /** Response of the secrets-set route — the resulting NAMES, never values. */
536
533
  export interface SecretsSetResponse {
537
534
  ok: boolean;
538
535
  names: string[];
539
536
  }
540
537
 
541
- // ─── User settings (desktop launchers; specs/desktop.md §3.1) ───────────────
538
+ // ─── Key-value patches (secrets and user settings) ──────────────────────────
542
539
 
543
540
  /**
544
- * A set of changes to the user-level settings store.
541
+ * A set of changes to a string-valued key store: the request body of the secrets-set route (values
542
+ * flow in, never back out; specs/extensions.md §13), and the patch the desktop launchers apply to
543
+ * the user-level settings store (specs/desktop.md §3.1).
545
544
  *
546
- * A patch rather than the whole map, and for the same reason {@link SecretsSetRequest} is one: a key
547
- * named by neither `set` nor `remove` must be left exactly as it was found. Writing the whole map
548
- * let any window overwrite the shared store with its own view of it — and on the chromium launcher
549
- * every window is a separate process with a separate browser profile, so a window holding no
550
- * settings could clear the credentials another had just stored.
545
+ * A patch rather than the whole map: a key named by neither `set` nor `remove` must be left exactly
546
+ * as it was found. Writing the whole settings map let any window overwrite the shared store with
547
+ * its own view of it — and on the chromium launcher every window is a separate process with a
548
+ * separate browser profile, so a window holding no settings could clear the credentials another had
549
+ * just stored.
551
550
  */
552
- export interface SettingsPatch {
551
+ export interface KeyValuePatch {
553
552
  /** Keys to store, with their new values. */
554
553
  set?: Record<string, string>;
555
554
  /** Keys to forget. */
556
555
  remove?: string[];
557
556
  }
558
557
 
558
+ /** The former published names of {@link KeyValuePatch}, kept for existing importers. */
559
+ export type { KeyValuePatch as SecretsSetRequest, KeyValuePatch as SettingsPatch };
560
+
559
561
  // ─── Packages ────────────────────────────────────────────────────────────────
560
562
 
561
563
  export interface PackageInfo {
@@ -602,22 +604,32 @@ export interface AppInfo {
602
604
 
603
605
  export interface CodeServiceResult {
604
606
  code?: string;
605
- diagnostics?: unknown[];
606
- [key: string]: unknown;
607
+ diagnostics?: SchemaJsonValue[];
608
+ [key: string]: JsonValue;
607
609
  }
608
610
 
609
611
  // ─── Projects & starters ─────────────────────────────────────────────────────
610
612
 
611
- /** A starter template surfaced in the New Project picker (mirrors @jxsuite/starters StarterMeta). */
613
+ /**
614
+ * A starter template surfaced in the New Project picker: one entry of `@jxsuite/starters`'
615
+ * `registry.json`, everything the picker needs to render a template card.
616
+ */
612
617
  export interface StarterInfo {
618
+ /** Stable id; also the directory name under `sites/` and the value threaded as `starter`. */
613
619
  id: string;
620
+ /** Human-facing display name, e.g. "Bistro & Café". */
614
621
  name: string;
622
+ /** Industry archetype label, e.g. "Restaurant & Food". */
615
623
  industry: string;
624
+ /** One-line summary shown under the name. */
616
625
  tagline: string;
626
+ /** Longer description for the showcase / tooltip. */
617
627
  description: string;
628
+ /** Short bullets naming the Jx features the starter demonstrates. */
618
629
  features: string[];
630
+ /** Accent colour (hex), mirroring the site's primary token; used for card theming. */
619
631
  accent: string;
620
- /** Preview image as a self-contained `data:` URI. */
632
+ /** Small preview image as a self-contained `data:` URI so the picker needs no extra plumbing. */
621
633
  thumbnail: string;
622
634
  }
623
635
 
@@ -682,6 +694,18 @@ export interface ImportReadyEvent {
682
694
  root: string;
683
695
  }
684
696
 
697
+ /**
698
+ * The most pages one site import may capture. A backend clamps {@link ImportSiteOptions.maxPages} to
699
+ * it, and a client that builds the options clamps to the same bound before it asks.
700
+ */
701
+ export const IMPORT_MAX_PAGES = 100;
702
+
703
+ /**
704
+ * The most breakpoints one site import may be asked to keep; past it, a "limit" policy is not
705
+ * limiting. `@jxsuite/import`'s planner clamps to it, and a client offers no larger count.
706
+ */
707
+ export const IMPORT_MAX_BREAKPOINTS = 12;
708
+
685
709
  /** Options for the import-site pipeline (StudioPlatform.importSite). */
686
710
  export interface ImportSiteOptions {
687
711
  /** The live site to clone; must be http(s). */