@jxsuite/protocol 2.4.0 → 2.6.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/LICENSE +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -3
- package/src/problems.ts +13 -2
- package/src/routes.ts +41 -10
- package/src/types.ts +87 -5
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A backend is free to change the transport and path prefix; the **sub-path, metho
|
|
|
17
17
|
|
|
18
18
|
## Core vs optional
|
|
19
19
|
|
|
20
|
-
Optional routes back optional `StudioPlatform` members. Omitting one is not an error. Studio degrades exactly as each entry's `degradation` describes (the starter picker empties, the Projects catalogue hides, the Publish panel explains git-push publishing, and so on). `coreRouteNames()` / `optionalRouteNames()` split the table programmatically; a conformance test for a new backend can iterate `STUDIO_ROUTES` and assert every core route is served.
|
|
20
|
+
Optional routes back optional `StudioPlatform` members, or, like `filesRead`, back no member and let an adapter serve one it already has more cheaply (their `degradation` names the route they stand in for). Omitting one is not an error. Studio degrades exactly as each entry's `degradation` describes (the starter picker empties, the Projects catalogue hides, the Publish panel explains git-push publishing, and so on). `coreRouteNames()` / `optionalRouteNames()` split the table programmatically; a conformance test for a new backend can iterate `STUDIO_ROUTES` and assert every core route is served.
|
|
21
21
|
|
|
22
22
|
Highlights an implementer should know:
|
|
23
23
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Studio Backend Protocol package: wire types plus the canonical route
|
|
3
|
-
*
|
|
4
|
-
* guide.
|
|
2
|
+
* The Studio Backend Protocol package: wire types plus the canonical route table every Jx Studio
|
|
3
|
+
* backend implements. See README.md for the implementer guide.
|
|
5
4
|
*
|
|
6
5
|
* @license MIT
|
|
7
6
|
*/
|
|
@@ -10,3 +9,5 @@ export * from "./problem";
|
|
|
10
9
|
export * from "./problems";
|
|
11
10
|
export * from "./routes";
|
|
12
11
|
export type * from "./types";
|
|
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";
|
package/src/problems.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* declared once, exported as data, rendered into the docs by the same generator, and checked by the
|
|
7
7
|
* same drift machinery.
|
|
8
8
|
*
|
|
9
|
-
* **What belongs here.** A type is a
|
|
9
|
+
* **What belongs here.** A type is a _class_ of failure a client might handle differently — not a
|
|
10
10
|
* message. "The project root was refused" is a type; "root /x/y was refused" is a `detail`. If two
|
|
11
11
|
* entries would only ever differ in their wording, they are one type.
|
|
12
12
|
*
|
|
@@ -78,7 +78,18 @@ export const PROBLEM_TYPES = {
|
|
|
78
78
|
403,
|
|
79
79
|
"The path resolves outside the active project",
|
|
80
80
|
),
|
|
81
|
-
|
|
81
|
+
/*
|
|
82
|
+
* Refused for a reason the user can act on (desktop.md §10.4): a role, a policy, a plan, a quota.
|
|
83
|
+
* Its own type rather than a `forbidden` with a code, by the same test as
|
|
84
|
+
* `needsInstallationAccess`: the client does something different with it — it offers the
|
|
85
|
+
* `actions` the backend listed, under the backend's `heading`, and follows `retry` once one is
|
|
86
|
+
* done. Nothing here says WHY; that is the backend's, and so is every word the user reads.
|
|
87
|
+
*/
|
|
88
|
+
actionRequired: problem("action-required", 403, "An action is required first", [
|
|
89
|
+
"actions",
|
|
90
|
+
"heading",
|
|
91
|
+
"retry",
|
|
92
|
+
]),
|
|
82
93
|
// ─── Backend state ────────────────────────────────────────────────────────
|
|
83
94
|
noActiveProject: problem("no-active-project", 409, "No project is active on this backend"),
|
|
84
95
|
capabilityUnavailable: problem(
|
package/src/routes.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Studio Backend Protocol — the canonical route table. The dev server
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* back
|
|
7
|
-
*
|
|
2
|
+
* Studio Backend Protocol — the canonical route table. The dev server (@jxsuite/server) is the
|
|
3
|
+
* reference implementation; every other backend (desktop RPC bridge, cloud platforms) serves the
|
|
4
|
+
* same shapes, either at these literal paths or through an equivalent transport. Optional routes
|
|
5
|
+
* mostly back optional StudioPlatform members — Studio degrades without them, as described by each
|
|
6
|
+
* entry's `degradation`. A few back no member at all: an adapter uses them to serve members it
|
|
7
|
+
* already has more cheaply (`filesRead`), and their `degradation` is the route they stand in for.
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
@@ -28,7 +28,10 @@ export type StudioRouteMethod = "GET" | "POST" | "PUT" | "DELETE";
|
|
|
28
28
|
export interface StudioRoute {
|
|
29
29
|
path: string;
|
|
30
30
|
method: StudioRouteMethod;
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* True when a backend may omit the route: its PAL member is optional, or it has no member and an
|
|
33
|
+
* adapter falls back to another route.
|
|
34
|
+
*/
|
|
32
35
|
optional: boolean;
|
|
33
36
|
/** One-line contract summary. */
|
|
34
37
|
summary: string;
|
|
@@ -107,10 +110,35 @@ export const STUDIO_ROUTES = {
|
|
|
107
110
|
files: route(
|
|
108
111
|
"GET",
|
|
109
112
|
"/__studio/files",
|
|
110
|
-
"List a directory (DirEntry[]) in stable path order;
|
|
111
|
-
"files project-wide",
|
|
113
|
+
"List a directory (DirEntry[]) in stable path order; an entry may carry an opaque content " +
|
|
114
|
+
"`version` the backend vouches for; with ?glob=<pattern>, search matching files project-wide",
|
|
112
115
|
),
|
|
113
116
|
fileRead: route("GET", "/__studio/file", "Read a file's text content"),
|
|
117
|
+
/**
|
|
118
|
+
* Many reads in ONE round trip, for a backend whose every request is a network hop.
|
|
119
|
+
*
|
|
120
|
+
* The first optional route with no `StudioPlatform` member behind it: the PAL keeps `readFile`,
|
|
121
|
+
* and an adapter for such a backend coalesces the reads issued in one tick into this call itself
|
|
122
|
+
* (specs/desktop.md §3.1, §10.1). Callers never see it, so a backend without it costs latency and
|
|
123
|
+
* nothing else — which is why the dev server, whose reads are loopback, does not serve it.
|
|
124
|
+
*
|
|
125
|
+
* The answer is partial by design, never a 400: past `READ_FILES_MAX_PATHS` paths or the
|
|
126
|
+
* backend's byte budget, the rest come back in `omitted` for the client to ask again, and the
|
|
127
|
+
* first path is always answered so that asking again makes progress. Each answered path carries
|
|
128
|
+
* its own outcome, because one missing file is not a failed batch. It is a READ: a read-only
|
|
129
|
+
* session may call it, and whatever a single `fileRead` does on the backend's side (a cloud
|
|
130
|
+
* session fetches and stores the file it serves) this does per path too.
|
|
131
|
+
*/
|
|
132
|
+
filesRead: route(
|
|
133
|
+
"POST",
|
|
134
|
+
"/__studio/files/read",
|
|
135
|
+
"Read many files' text content {paths} → ReadFilesResult {files: [path, {content} | " +
|
|
136
|
+
"{error: {status, code?, message}}][], omitted}. At most READ_FILES_MAX_PATHS (200) paths " +
|
|
137
|
+
"and a backend byte budget per call: the excess is listed in `omitted` (never a 400) and " +
|
|
138
|
+
"the first path is always answered. Per-path errors: 404 missing/deleted, 415 binary, 413 " +
|
|
139
|
+
"too large, 502 upstream. Allowed for read-only sessions; keeps fileRead's server-side effects",
|
|
140
|
+
"Adapters fall back to one fileRead per path.",
|
|
141
|
+
),
|
|
114
142
|
fileWrite: route("PUT", "/__studio/file", "Write a file's text content"),
|
|
115
143
|
fileDelete: route("DELETE", "/__studio/file", "Delete a file"),
|
|
116
144
|
fileUpload: route(
|
|
@@ -475,7 +503,10 @@ export function coreRouteNames(): StudioRouteName[] {
|
|
|
475
503
|
);
|
|
476
504
|
}
|
|
477
505
|
|
|
478
|
-
/**
|
|
506
|
+
/**
|
|
507
|
+
* Names of the optional routes (each backs an optional StudioPlatform member, or is an adapter's
|
|
508
|
+
* optimisation with a fallback).
|
|
509
|
+
*/
|
|
479
510
|
export function optionalRouteNames(): StudioRouteName[] {
|
|
480
511
|
return (Object.keys(STUDIO_ROUTES) as StudioRouteName[]).filter(
|
|
481
512
|
(name) => STUDIO_ROUTES[name].optional,
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Studio Backend Protocol — wire types. Every shape a Studio backend serves
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* platforms) share one contract. Environment-agnostic: no DOM, no node —
|
|
2
|
+
* Studio Backend Protocol — wire types. Every shape a Studio backend serves (or a `StudioPlatform`
|
|
3
|
+
* adapter consumes) lives here so client adapters and server implementations (the dev server, the
|
|
4
|
+
* desktop RPC bridge, cloud platforms) share one contract. Environment-agnostic: no DOM, no node —
|
|
6
5
|
* importable in browsers, Bun, and Cloudflare Workers alike.
|
|
7
6
|
*
|
|
8
7
|
* @license MIT
|
|
@@ -42,6 +41,13 @@ export interface DirEntry {
|
|
|
42
41
|
type: "file" | "directory";
|
|
43
42
|
size?: number;
|
|
44
43
|
modified?: string;
|
|
44
|
+
/**
|
|
45
|
+
* An opaque version of the file's bytes: two equal versions mean identical bytes. ABSENT whenever
|
|
46
|
+
* the backend cannot vouch for the bytes — an uncommitted, dirty or just-uploaded file, and every
|
|
47
|
+
* directory. A client may append it to the file's URL (`?v=<version>`) so a host that recognizes
|
|
48
|
+
* it can answer with an immutable cache lifetime; a backend with no such notion never sets it.
|
|
49
|
+
*/
|
|
50
|
+
version?: string;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
/** A filesystem change pushed from the backend (project-relative, forward-slashed path). */
|
|
@@ -87,6 +93,45 @@ export interface UploadResult {
|
|
|
87
93
|
size?: number;
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
/**
|
|
97
|
+
* 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.
|
|
100
|
+
*
|
|
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.
|
|
103
|
+
*/
|
|
104
|
+
export const READ_FILES_MAX_PATHS = 200;
|
|
105
|
+
|
|
106
|
+
/** Body of `POST /__studio/files/read` (`filesRead`): the project-relative paths to read. */
|
|
107
|
+
export interface ReadFilesRequest {
|
|
108
|
+
paths: string[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* One path's answer inside a {@link ReadFilesResult}: its text, or why it has none.
|
|
113
|
+
*
|
|
114
|
+
* Per path rather than per request, because one unreadable file in a batch of two hundred is not a
|
|
115
|
+
* failed batch. `status` borrows the HTTP code the single-file read would have answered (404
|
|
116
|
+
* missing or deleted, 415 binary, 413 too large, 502 upstream), so a client can treat both routes'
|
|
117
|
+
* failures alike.
|
|
118
|
+
*/
|
|
119
|
+
export type ReadFilesOutcome =
|
|
120
|
+
| { content: string }
|
|
121
|
+
| { error: { status: number; code?: string; message: string } };
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Answer of `POST /__studio/files/read`. `files` pairs each answered path with its outcome;
|
|
125
|
+
* `omitted` lists the requested paths this response did not get to (a path or byte budget ran out).
|
|
126
|
+
* Omission is never an error and never a 400: the client asks for those paths again. A backend
|
|
127
|
+
* always answers the FIRST requested path, which is what guarantees that asking again makes
|
|
128
|
+
* progress.
|
|
129
|
+
*/
|
|
130
|
+
export interface ReadFilesResult {
|
|
131
|
+
files: [path: string, outcome: ReadFilesOutcome][];
|
|
132
|
+
omitted: string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
90
135
|
/**
|
|
91
136
|
* What a backend will accept as an upload, when it says.
|
|
92
137
|
*
|
|
@@ -774,10 +819,47 @@ export interface AiModelsResponse {
|
|
|
774
819
|
* true`: Cloudflare being briefly unreachable is not a reason to send someone round an OAuth flow
|
|
775
820
|
* that fixes nothing. `cf_account_required` is a live grant with no account chosen (ai.md §2.1):
|
|
776
821
|
* re-authorizing lands back in the same state, so it calls for the account picker instead.
|
|
822
|
+
*
|
|
823
|
+
* `action_required` is the fourth, and no connection fixes it: the backend requires something of
|
|
824
|
+
* the user before the assistant will run (desktop.md §10.4, ai.md §2.1). It arrives with
|
|
825
|
+
* {@link detail} and {@link actions}, the same members the `action-required` problem carries, so
|
|
826
|
+
* the gate can offer what the backend offers in the backend's own words.
|
|
777
827
|
*/
|
|
778
|
-
code?:
|
|
828
|
+
code?:
|
|
829
|
+
| "cf_not_connected"
|
|
830
|
+
| "cf_reconnect_required"
|
|
831
|
+
| "cf_account_required"
|
|
832
|
+
| "cf_upstream_error"
|
|
833
|
+
| "action_required";
|
|
834
|
+
/** Why the backend refuses, in its own words. Sent with `action_required`. */
|
|
835
|
+
detail?: string;
|
|
836
|
+
/** What the user can do about it. Sent with `action_required`. */
|
|
837
|
+
actions?: OfferedAction[];
|
|
779
838
|
}
|
|
780
839
|
|
|
840
|
+
// ─── Backend-directed access (desktop.md §10.4) ──────────────────────────────
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Something the user can do, offered by the backend: on an `action-required` refusal, a notice, or
|
|
844
|
+
* an account row. The backend decides what it means; the platform adapter performs it.
|
|
845
|
+
*/
|
|
846
|
+
export interface OfferedAction {
|
|
847
|
+
/** Stable within the offer — for the backend's bookkeeping and a test's selector. */
|
|
848
|
+
id: string;
|
|
849
|
+
/** The button's words. */
|
|
850
|
+
label: string;
|
|
851
|
+
/** A page the platform opens for the user. Absent: the platform performs it by `id`. */
|
|
852
|
+
href?: string;
|
|
853
|
+
/** The one action a surface leads with. */
|
|
854
|
+
primary?: boolean;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* What the client does once an offered action is done: `reload` the page (the refusal broke the
|
|
859
|
+
* session), `repeat` (the user runs what was refused again), or `none`.
|
|
860
|
+
*/
|
|
861
|
+
export type RetryHint = "reload" | "repeat" | "none";
|
|
862
|
+
|
|
781
863
|
// ─── Cloudflare publish surface ──────────────────────────────────────────────
|
|
782
864
|
|
|
783
865
|
/**
|