@jxsuite/protocol 2.6.1 → 4.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/README.md CHANGED
@@ -7,11 +7,11 @@ The **Studio Backend Protocol**: the wire types and canonical route table that e
7
7
 
8
8
  ## Who implements this
9
9
 
10
- | Backend | Transport |
11
- | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
12
- | `@jxsuite/server` (dev server) | The literal `/__studio/*` HTTP routes (the reference implementation) |
13
- | Desktop (Electrobun / Chromium) | Typed RPC / WebSocket JSON-RPC dispatching into the same handlers |
14
- | Cloud platforms | HTTP behind a session gateway (e.g. `/api/v1/p/:owner/:repo/:branch/studio/*`), same sub-paths and shapes |
10
+ | Backend | Transport |
11
+ | ------------------------------ | --------------------------------------------------------------------------------------------------------- |
12
+ | `@jxsuite/server` (dev server) | The literal `/__studio/*` HTTP routes (the reference implementation) |
13
+ | Desktop (Electron / Chromium) | Typed RPC / WebSocket JSON-RPC dispatching into the same handlers |
14
+ | Cloud platforms | HTTP behind a session gateway (e.g. `/api/v1/p/:owner/:repo/:branch/studio/*`), same sub-paths and shapes |
15
15
 
16
16
  A backend is free to change the transport and path prefix; the **sub-path, method, and body shapes are the contract**. `STUDIO_PROTOCOL_VERSION` bumps when any shape changes incompatibly.
17
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/protocol",
3
- "version": "2.6.1",
3
+ "version": "4.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
 
@@ -212,6 +223,12 @@ export interface GitStatusResult {
212
223
  export interface GitBranchesResult {
213
224
  current: string;
214
225
  branches: string[];
226
+ /**
227
+ * The repository's default branch (the one a pull request merges into), when the host knows it. A
228
+ * host whose branches are sessions reports it so a review is never committed to the live branch;
229
+ * a local repository may omit it.
230
+ */
231
+ defaultBranch?: string;
215
232
  }
216
233
 
217
234
  export interface GitLogEntry {
@@ -239,34 +256,32 @@ export interface ComponentMeta {
239
256
  tagName: string;
240
257
  $id?: string | null;
241
258
  path: string;
242
- props?: { name: string; type?: string; default?: JsonValue; [k: string]: unknown }[];
259
+ props?: { name: string; type?: string; default?: JsonValue; [k: string]: JsonValue }[];
243
260
  slots?: ComponentSlotMeta[];
244
261
  hasElements?: boolean;
262
+ /** The custom-element tags the component's own tree renders (`componentMetaFrom`'s `uses`). */
263
+ uses?: string[];
245
264
  }
246
265
 
247
266
  // ─── Extensions (specs/extensions.md §9/§9.1) ────────────────────────────────
248
267
 
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
- }
268
+ /* The `project` admission block of a section-owning extension class: the descriptor's own block,
269
+ declared once in @jxsuite/schema and served here unchanged. */
270
+ export type { ProjectBlock as ExtensionProjectBlock } from "@jxsuite/schema/format-registry";
257
271
 
258
272
  /** One project-section contribution of an extension class, as served on the formats route. */
259
273
  export interface ExtensionContributionInfo {
260
274
  /** The $prototype-visible class name declaring the `project` block. */
261
275
  className: string;
262
- project: ExtensionProjectBlock;
276
+ project: ProjectBlock;
263
277
  /** The class descriptor's `$studio` block (settings-section vocabulary), when declared. */
264
- studio?: Record<string, unknown> | null;
278
+ studio?: StudioHints | null;
265
279
  /**
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).
280
+ * The section's value schema — `properties[<key>]` of the extension's shipped project fragment,
281
+ * which JSON Schema lets be a boolean as well as an object — resolved backend-side; null when the
282
+ * extension ships no fragment (or it lacks the key).
268
283
  */
269
- entrySchema?: Record<string, unknown> | null;
284
+ entrySchema?: SchemaJsonValue;
270
285
  }
271
286
 
272
287
  /**
@@ -292,7 +307,7 @@ export interface ExtensionsInfo {
292
307
  name: string;
293
308
  path: string;
294
309
  state?: boolean;
295
- stateDefaults?: Record<string, unknown>;
310
+ stateDefaults?: JsonObject;
296
311
  }[];
297
312
  }
298
313
 
@@ -369,8 +384,8 @@ export interface ExtensionCatalogEntry {
369
384
  * editors can register them without resolving relative `$ref`s.
370
385
  */
371
386
  export interface ProjectSchemasResponse {
372
- project?: Record<string, unknown> | null;
373
- document?: Record<string, unknown> | null;
387
+ project?: JsonObject | null;
388
+ document?: JsonObject | null;
374
389
  }
375
390
 
376
391
  // ─── Data surface (connector domain; specs/extensions.md §13) ───────────────
@@ -397,7 +412,7 @@ export interface DataConnectionInfo {
397
412
  name: string;
398
413
  provider: string;
399
414
  /** The connection's project.json value — identifiers and env-var names, never secrets. */
400
- settings: Record<string, unknown>;
415
+ settings: JsonObject;
401
416
  /** True when every env-var name the connection references resolves in the backend env. */
402
417
  configured: boolean;
403
418
  /** Referenced env-var NAMES with no value in the backend environment. */
@@ -419,11 +434,9 @@ export interface DataConnectionsResponse {
419
434
  connections: DataConnectionInfo[];
420
435
  }
421
436
 
422
- /** Result of the connection-test route (mirrors the connector's testConnection capability). */
423
- export interface DataConnectionTestResult {
424
- ok: boolean;
425
- error?: string;
426
- }
437
+ /* Result of the connection-test route: the connector `testConnection` capability's own result,
438
+ declared once in @jxsuite/schema and relayed unchanged. */
439
+ export type { TestConnectionResult as DataConnectionTestResult } from "@jxsuite/schema/format-registry";
427
440
 
428
441
  /**
429
442
  * A push-plan step kind. Open string union: connector DDL kinds today, and future push-step
@@ -450,13 +463,9 @@ export interface DataPushStep {
450
463
  connection?: string;
451
464
  }
452
465
 
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
- }
466
+ /* Request body of the data-push route: the same request `jx db push` takes, declared once in
467
+ @jxsuite/schema. */
468
+ export type { DataPushRequest } from "@jxsuite/schema/format-registry";
460
469
 
461
470
  /** Result of the data-push route. */
462
471
  export interface DataPushResult {
@@ -491,7 +500,7 @@ export interface DataColumnMeta {
491
500
 
492
501
  /** Result of the data-rows route. */
493
502
  export interface DataRowsResult {
494
- rows: Record<string, unknown>[];
503
+ rows: JsonObject[];
495
504
  /** Total row count of the table (for pagination). */
496
505
  total: number;
497
506
  columns: DataColumnMeta[];
@@ -501,7 +510,7 @@ export interface DataRowsResult {
501
510
  export interface DataRowInsert {
502
511
  table: string;
503
512
  connection?: string;
504
- values: Record<string, unknown>;
513
+ values: JsonObject;
505
514
  }
506
515
 
507
516
  /** Body of the row-update route (keyed on the introspected primary key). */
@@ -509,7 +518,7 @@ export interface DataRowUpdate {
509
518
  table: string;
510
519
  connection?: string;
511
520
  pk: string | number;
512
- set: Record<string, unknown>;
521
+ set: JsonObject;
513
522
  }
514
523
 
515
524
  /** Parameters of the row-delete route. */
@@ -526,36 +535,35 @@ export interface SecretsListResponse {
526
535
  names: string[];
527
536
  }
528
537
 
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
538
  /** Response of the secrets-set route — the resulting NAMES, never values. */
536
539
  export interface SecretsSetResponse {
537
540
  ok: boolean;
538
541
  names: string[];
539
542
  }
540
543
 
541
- // ─── User settings (desktop launchers; specs/desktop.md §3.1) ───────────────
544
+ // ─── Key-value patches (secrets and user settings) ──────────────────────────
542
545
 
543
546
  /**
544
- * A set of changes to the user-level settings store.
547
+ * A set of changes to a string-valued key store: the request body of the secrets-set route (values
548
+ * flow in, never back out; specs/extensions.md §13), and the patch the desktop launchers apply to
549
+ * the user-level settings store (specs/desktop.md §3.1).
545
550
  *
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.
551
+ * A patch rather than the whole map: a key named by neither `set` nor `remove` must be left exactly
552
+ * as it was found. Writing the whole settings map let any window overwrite the shared store with
553
+ * its own view of it — and on the chromium launcher every window is a separate process with a
554
+ * separate browser profile, so a window holding no settings could clear the credentials another had
555
+ * just stored.
551
556
  */
552
- export interface SettingsPatch {
557
+ export interface KeyValuePatch {
553
558
  /** Keys to store, with their new values. */
554
559
  set?: Record<string, string>;
555
560
  /** Keys to forget. */
556
561
  remove?: string[];
557
562
  }
558
563
 
564
+ /** The former published names of {@link KeyValuePatch}, kept for existing importers. */
565
+ export type { KeyValuePatch as SecretsSetRequest, KeyValuePatch as SettingsPatch };
566
+
559
567
  // ─── Packages ────────────────────────────────────────────────────────────────
560
568
 
561
569
  export interface PackageInfo {
@@ -602,22 +610,32 @@ export interface AppInfo {
602
610
 
603
611
  export interface CodeServiceResult {
604
612
  code?: string;
605
- diagnostics?: unknown[];
606
- [key: string]: unknown;
613
+ diagnostics?: SchemaJsonValue[];
614
+ [key: string]: JsonValue;
607
615
  }
608
616
 
609
617
  // ─── Projects & starters ─────────────────────────────────────────────────────
610
618
 
611
- /** A starter template surfaced in the New Project picker (mirrors @jxsuite/starters StarterMeta). */
619
+ /**
620
+ * A starter template surfaced in the New Project picker: one entry of `@jxsuite/starters`'
621
+ * `registry.json`, everything the picker needs to render a template card.
622
+ */
612
623
  export interface StarterInfo {
624
+ /** Stable id; also the directory name under `sites/` and the value threaded as `starter`. */
613
625
  id: string;
626
+ /** Human-facing display name, e.g. "Bistro & Café". */
614
627
  name: string;
628
+ /** Industry archetype label, e.g. "Restaurant & Food". */
615
629
  industry: string;
630
+ /** One-line summary shown under the name. */
616
631
  tagline: string;
632
+ /** Longer description for the showcase / tooltip. */
617
633
  description: string;
634
+ /** Short bullets naming the Jx features the starter demonstrates. */
618
635
  features: string[];
636
+ /** Accent colour (hex), mirroring the site's primary token; used for card theming. */
619
637
  accent: string;
620
- /** Preview image as a self-contained `data:` URI. */
638
+ /** Small preview image as a self-contained `data:` URI so the picker needs no extra plumbing. */
621
639
  thumbnail: string;
622
640
  }
623
641
 
@@ -682,6 +700,18 @@ export interface ImportReadyEvent {
682
700
  root: string;
683
701
  }
684
702
 
703
+ /**
704
+ * The most pages one site import may capture. A backend clamps {@link ImportSiteOptions.maxPages} to
705
+ * it, and a client that builds the options clamps to the same bound before it asks.
706
+ */
707
+ export const IMPORT_MAX_PAGES = 100;
708
+
709
+ /**
710
+ * The most breakpoints one site import may be asked to keep; past it, a "limit" policy is not
711
+ * limiting. `@jxsuite/import`'s planner clamps to it, and a client offers no larger count.
712
+ */
713
+ export const IMPORT_MAX_BREAKPOINTS = 12;
714
+
685
715
  /** Options for the import-site pipeline (StudioPlatform.importSite). */
686
716
  export interface ImportSiteOptions {
687
717
  /** The live site to clone; must be http(s). */