@jxsuite/protocol 2.1.0 → 2.3.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 +12 -47
- package/package.json +2 -2
- package/src/routes.ts +10 -0
- package/src/types.ts +125 -2
package/README.md
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
# @jxsuite/protocol
|
|
2
2
|
|
|
3
|
-
The **Studio Backend Protocol**: the wire types and canonical route table that
|
|
4
|
-
every Jx Studio backend implements. Jx Studio is backend-agnostic. The editor
|
|
5
|
-
talks to whatever registered a `StudioPlatform` adapter (see
|
|
6
|
-
`@jxsuite/studio/platform`), and every adapter ultimately speaks the contract
|
|
7
|
-
defined here. This package is the single source of truth for that contract:
|
|
3
|
+
The **Studio Backend Protocol**: the wire types and canonical route table that every Jx Studio backend implements. Jx Studio is backend-agnostic. The editor talks to whatever registered a `StudioPlatform` adapter (see `@jxsuite/studio/platform`), and every adapter ultimately speaks the contract defined here. This package is the single source of truth for that contract:
|
|
8
4
|
|
|
9
|
-
- `@jxsuite/protocol/types`: the request/response shapes (`DirEntry`,
|
|
10
|
-
|
|
11
|
-
Environment-agnostic, with no DOM and no node, so the types are importable
|
|
12
|
-
in browsers, Bun, and Cloudflare Workers alike.
|
|
13
|
-
- `@jxsuite/protocol/routes`: `STUDIO_ROUTES`, the canonical endpoint table
|
|
14
|
-
with methods, one-line contract summaries, and per-route optionality.
|
|
5
|
+
- `@jxsuite/protocol/types`: the request/response shapes (`DirEntry`, `GitStatusResult`, `StarterInfo`, `AiModelsResponse`, `CfConnection`, …). Environment-agnostic, with no DOM and no node, so the types are importable in browsers, Bun, and Cloudflare Workers alike.
|
|
6
|
+
- `@jxsuite/protocol/routes`: `STUDIO_ROUTES`, the canonical endpoint table with methods, one-line contract summaries, and per-route optionality.
|
|
15
7
|
|
|
16
8
|
## Who implements this
|
|
17
9
|
|
|
@@ -21,48 +13,21 @@ defined here. This package is the single source of truth for that contract:
|
|
|
21
13
|
| Desktop (Electrobun / Chromium) | Typed RPC / WebSocket JSON-RPC dispatching into the same handlers |
|
|
22
14
|
| Cloud platforms | HTTP behind a session gateway (e.g. `/api/v1/p/:owner/:repo/:branch/studio/*`), same sub-paths and shapes |
|
|
23
15
|
|
|
24
|
-
A backend is free to change the transport and path prefix; the **sub-path,
|
|
25
|
-
method, and body shapes are the contract**. `STUDIO_PROTOCOL_VERSION` bumps
|
|
26
|
-
when any shape changes incompatibly.
|
|
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.
|
|
27
17
|
|
|
28
18
|
## Core vs optional
|
|
29
19
|
|
|
30
|
-
Optional routes back optional `StudioPlatform` members. Omitting one is not an
|
|
31
|
-
error. Studio degrades exactly as each entry's `degradation` describes (the
|
|
32
|
-
starter picker empties, the Projects catalogue hides, the Publish panel
|
|
33
|
-
explains git-push publishing, and so on). `coreRouteNames()` /
|
|
34
|
-
`optionalRouteNames()` split the table programmatically; a conformance test
|
|
35
|
-
for a new backend can iterate `STUDIO_ROUTES` and assert every core route is
|
|
36
|
-
served.
|
|
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.
|
|
37
21
|
|
|
38
22
|
Highlights an implementer should know:
|
|
39
23
|
|
|
40
|
-
- **`git/commit`** commits the staged files if any are staged, otherwise all
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- **`
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
doc?, options? }` through the project's format registry; without it only
|
|
47
|
-
`.json` documents open.
|
|
48
|
-
- **`ai/chat`** accepts `{ messages, tools, systemPrompt, model }` and streams
|
|
49
|
-
the normalized `StreamEvent` SSE defined by `@jxsuite/ai/streaming-client`.
|
|
50
|
-
`ai/models` returns `AiModelsResponse`; report `configured: true` when the
|
|
51
|
-
backend holds credentials (Studio then unlocks the assistant without a
|
|
52
|
-
locally stored key) and `managed: true` when the platform brokers them.
|
|
53
|
-
- **`cf/proxy`** is an allowlisted Cloudflare API passthrough (accounts and
|
|
54
|
-
Pages projects/deployments only). Credentials are injected by the backend
|
|
55
|
-
and must never be persisted by stateless implementations. On the dev server
|
|
56
|
-
the credential is a header-borne user token, and on cloud platforms a
|
|
57
|
-
vaulted OAuth token.
|
|
58
|
-
- **Errors** are `{ error, code?, detail? }` (`ErrorBody`) with a meaningful
|
|
59
|
-
HTTP status; `code` is the machine-readable discriminator Studio switches
|
|
60
|
-
on (e.g. `remote_moved`, `cf_not_connected`, `needs_installation_access`).
|
|
24
|
+
- **`git/commit`** commits the staged files if any are staged, otherwise all dirty files. Cloud backends may make commit+push atomic and treat `git/push` as a sync check (`ahead` stays 0).
|
|
25
|
+
- **`git/pull`** returns `409 { code: "pull_conflict", conflicts: [paths] }` when local dirty files overlap remote changes.
|
|
26
|
+
- **`format`** dispatches `{ format, action: "parse" | "serialize", source? | doc?, options? }` through the project's format registry; without it only `.json` documents open.
|
|
27
|
+
- **`ai/chat`** accepts `{ messages, tools, systemPrompt, model }` and streams the normalized `StreamEvent` SSE defined by `@jxsuite/ai/streaming-client`. `ai/models` returns `AiModelsResponse`; report `configured: true` when the backend holds credentials (Studio then unlocks the assistant without a locally stored key) and `managed: true` when the platform brokers them.
|
|
28
|
+
- **`cf/proxy`** is an allowlisted Cloudflare API passthrough (accounts and Pages projects/deployments only). Credentials are injected by the backend and must never be persisted by stateless implementations. On the dev server the credential is a header-borne user token, and on cloud platforms a vaulted OAuth token.
|
|
29
|
+
- **Errors** are `{ error, code?, detail? }` (`ErrorBody`) with a meaningful HTTP status; `code` is the machine-readable discriminator Studio switches on (e.g. `remote_moved`, `cf_not_connected`, `needs_installation_access`).
|
|
61
30
|
|
|
62
31
|
## Versioning
|
|
63
32
|
|
|
64
|
-
The types are published as TypeScript source (like every `@jxsuite` package)
|
|
65
|
-
and follow the monorepo's release train. Within the monorepo, packages depend
|
|
66
|
-
on it via `workspace:^`. Consumers install `@jxsuite/protocol` and import its
|
|
67
|
-
`/types` and `/routes` entrypoints; its only runtime dependency is
|
|
68
|
-
`@jxsuite/schema`.
|
|
33
|
+
The types are published as TypeScript source (like every `@jxsuite` package) and follow the monorepo's release train. Within the monorepo, packages depend on it via `workspace:^`. Consumers install `@jxsuite/protocol` and import its `/types` and `/routes` entrypoints; its only runtime dependency is `@jxsuite/schema`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/protocol",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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": "^2.
|
|
30
|
+
"@jxsuite/schema": "^2.2.0"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/src/routes.ts
CHANGED
|
@@ -190,6 +190,16 @@ export const STUDIO_ROUTES = {
|
|
|
190
190
|
"Only .json documents open (backs listFormats); descriptor-contributed settings sections " +
|
|
191
191
|
"do not appear.",
|
|
192
192
|
),
|
|
193
|
+
extensionCatalog: route(
|
|
194
|
+
"GET",
|
|
195
|
+
"/__studio/catalog",
|
|
196
|
+
"Extensions this backend can OFFER (ExtensionCatalogEntry[]) — the shipped first-party " +
|
|
197
|
+
"catalogue plus any package in the project's own dependencies exporting a jx-extension.json, " +
|
|
198
|
+
"each marked with whether the host resolves it without a project install and whether the " +
|
|
199
|
+
"project has it installed. A capability rather than a constant: a Worker ships a fixed set " +
|
|
200
|
+
"of extension packages (specs/extensions.md §5.5). Backs listExtensionCatalog.",
|
|
201
|
+
"The Extensions section offers no catalogue; adding an extension stays a typed package name.",
|
|
202
|
+
),
|
|
193
203
|
projectSchemas: route(
|
|
194
204
|
"GET",
|
|
195
205
|
"/__studio/project-schemas",
|
package/src/types.ts
CHANGED
|
@@ -251,6 +251,73 @@ export interface ExtensionsInfo {
|
|
|
251
251
|
}[];
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
// ─── Extension catalogue ─────────────────────────────────────────────────────
|
|
255
|
+
/* The AVAILABLE half of the pair whose ENABLED half is ExtensionsInfo above. A backend answers it
|
|
256
|
+
for itself rather than serving a constant, because not every host can run every extension: a
|
|
257
|
+
Worker ships a fixed set of extension packages (specs/extensions.md §5.5), and one it does not
|
|
258
|
+
bundle is dropped from the registry before composition. Offering such an extension would promise
|
|
259
|
+
an action the host would refuse. */
|
|
260
|
+
|
|
261
|
+
/** One `project.json` section an extension's classes claim (specs/extensions.md §9). */
|
|
262
|
+
export interface ExtensionSectionInfo {
|
|
263
|
+
/** The `project.json` top-level property, e.g. "content". */
|
|
264
|
+
key: string;
|
|
265
|
+
/** The owning class's `project.title`, e.g. "Content Types". */
|
|
266
|
+
title?: string;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** One extension a backend can offer a project, whether or not the project enables it. */
|
|
270
|
+
export interface ExtensionCatalogEntry {
|
|
271
|
+
/** Package name: the identity, and the `bun add` argument. */
|
|
272
|
+
name: string;
|
|
273
|
+
/**
|
|
274
|
+
* The string that goes in `project.json` `extensions[]`. Usually `name`, but a linked or
|
|
275
|
+
* path-installed package enables under a different specifier — the same distinction
|
|
276
|
+
* {@link ExtensionsInfo} draws between `specifier` and `name`. Absent means they agree.
|
|
277
|
+
*/
|
|
278
|
+
specifier?: string;
|
|
279
|
+
title?: string;
|
|
280
|
+
description?: string;
|
|
281
|
+
/**
|
|
282
|
+
* The sections enabling it makes legal.
|
|
283
|
+
*
|
|
284
|
+
* The one field a client cannot compute for itself: `listExtensions` describes only extensions
|
|
285
|
+
* already enabled, which is the state the reader is trying to leave. It is also what lets a
|
|
286
|
+
* surface warn before disabling something whose sections the project still uses.
|
|
287
|
+
*/
|
|
288
|
+
sections: ExtensionSectionInfo[];
|
|
289
|
+
/** File extensions its format classes claim (".md", ".csv"), when it claims any. */
|
|
290
|
+
formats?: string[];
|
|
291
|
+
/** Other catalogue members it depends on — auth needs connector. */
|
|
292
|
+
requires?: string[];
|
|
293
|
+
/**
|
|
294
|
+
* THIS host resolves the package without a project install, so enabling it is a config write
|
|
295
|
+
* alone. Probed per host, never declared: what a desktop build stages and what a Worker bundles
|
|
296
|
+
* are different facts, and neither is knowable from the package list.
|
|
297
|
+
*/
|
|
298
|
+
bundled?: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* The PROJECT resolves it.
|
|
301
|
+
*
|
|
302
|
+
* Answered here rather than derived from `listPackages`, because that member does not mean one
|
|
303
|
+
* thing across backends: the dev server drops a declared dependency it cannot resolve, while the
|
|
304
|
+
* desktop reads the manifest and keeps it. A host with no module resolution at all degrades this
|
|
305
|
+
* to "declared in package.json" and says so in its own documentation.
|
|
306
|
+
*/
|
|
307
|
+
installed?: boolean;
|
|
308
|
+
/** The shipped catalogue, or a package discovered in this project's own dependencies. */
|
|
309
|
+
source: "first-party" | "project";
|
|
310
|
+
/** Documentation deep link. */
|
|
311
|
+
docs?: string;
|
|
312
|
+
/**
|
|
313
|
+
* Why this entry cannot be enabled as it stands, as one sentence for a disabled control.
|
|
314
|
+
*
|
|
315
|
+
* Set rather than dropped: a package the reader installed on purpose vanishing with no
|
|
316
|
+
* explanation is worse than a row that says what is wrong with it.
|
|
317
|
+
*/
|
|
318
|
+
problem?: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
254
321
|
/**
|
|
255
322
|
* Response of the project-schemas route: the project's generated entry documents
|
|
256
323
|
* (project.schema.json / document.schema.json), PRE-BUNDLED into self-contained compound schemas so
|
|
@@ -576,7 +643,11 @@ export interface ImportSiteOptions {
|
|
|
576
643
|
url: string;
|
|
577
644
|
/** Display name for the new project. */
|
|
578
645
|
name: string;
|
|
579
|
-
/**
|
|
646
|
+
/**
|
|
647
|
+
* Where the project goes, interpreted by the platform that receives it: an absolute path on
|
|
648
|
+
* desktop, project-relative on the dev server, `owner/repo` on a host whose projects are
|
|
649
|
+
* repositories. Never a path the caller assumes a filesystem for.
|
|
650
|
+
*/
|
|
580
651
|
directory: string;
|
|
581
652
|
/** Max crawl depth; 0 = single page. */
|
|
582
653
|
depth: number;
|
|
@@ -701,9 +772,61 @@ export interface AiModelsResponse {
|
|
|
701
772
|
|
|
702
773
|
// ─── Cloudflare publish surface ──────────────────────────────────────────────
|
|
703
774
|
|
|
704
|
-
/**
|
|
775
|
+
/**
|
|
776
|
+
* Cloudflare connection state (see StudioPlatform.cfConnection).
|
|
777
|
+
*
|
|
778
|
+
* Three answers, not two, and a reader tells them apart by SHAPE:
|
|
779
|
+
*
|
|
780
|
+
* - `null` — no credential and no row exists at all; nothing was ever connected here.
|
|
781
|
+
* - `{connected: false}` — a credential exists but the provider rejected it. That is the dev server's
|
|
782
|
+
* stored-token-invalid case, and the publish panel's "Replace token" branch keys on it.
|
|
783
|
+
* - `{connected: true, needsReconnect: true}` — a brokered row exists whose OAuth grant has lapsed.
|
|
784
|
+
* Its access token is gone or unrenewable, so every Cloudflare call will fail until the user goes
|
|
785
|
+
* back through the hosted flow.
|
|
786
|
+
*
|
|
787
|
+
* Each platform emits a strict subset: **cloud never emits `{connected: false}`** (a broker row it
|
|
788
|
+
* cannot use is a reconnect, not a bad token) and **the dev server never emits `needsReconnect`**
|
|
789
|
+
* (it holds a pasted token, not a grant, so there is nothing to lapse). Collapsing the lapsed case
|
|
790
|
+
* into a healthy one is precisely the defect that let the connect popup close over a dead row while
|
|
791
|
+
* the user was still typing their Cloudflare password.
|
|
792
|
+
*/
|
|
705
793
|
export interface CfConnection {
|
|
706
794
|
connected: boolean;
|
|
707
795
|
accountId?: string | undefined;
|
|
708
796
|
accountName?: string | undefined;
|
|
797
|
+
/** A broker row exists but its grant has lapsed — reconnect before any Cloudflare call. */
|
|
798
|
+
needsReconnect?: boolean | undefined;
|
|
799
|
+
/** Connected and usable, but no account has been chosen yet (multi-account connect). */
|
|
800
|
+
needsAccount?: boolean | undefined;
|
|
801
|
+
code?: "cf_reconnect_required" | "cf_account_required" | undefined;
|
|
802
|
+
reason?: string | undefined;
|
|
803
|
+
/**
|
|
804
|
+
* Whether the broker holds a refresh token for this row. Diagnostic: without one the connection
|
|
805
|
+
* dies at the access token's expiry and cannot renew itself silently, which is a degraded state
|
|
806
|
+
* worth naming on screen rather than discovering an hour later.
|
|
807
|
+
*/
|
|
808
|
+
hasRefreshToken?: boolean | undefined;
|
|
809
|
+
/** Unix seconds at which the current access token expires (after any silent refresh). */
|
|
810
|
+
expiresAt?: number | undefined;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** One Cloudflare account the connected grant can reach (the account picker's row). */
|
|
814
|
+
export interface CfAccountSummary {
|
|
815
|
+
id: string;
|
|
816
|
+
name: string;
|
|
709
817
|
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* How an interactive `cfConnect` ended.
|
|
821
|
+
*
|
|
822
|
+
* A connect flow has four endings and only one of them is a failure, so they are a union rather
|
|
823
|
+
* than a nullable connection: `redirect` means the page is already navigating away and the caller
|
|
824
|
+
* must render nothing, `timeout` means the deadline passed with no confirmation, and `canceled`
|
|
825
|
+
* means the user closed the popup. Returning `null` for all three made "the user changed their
|
|
826
|
+
* mind" indistinguishable from "this browser blocked the popup", and the UI apologised for both.
|
|
827
|
+
*/
|
|
828
|
+
export type CfConnectOutcome =
|
|
829
|
+
| { status: "connected"; connection: CfConnection }
|
|
830
|
+
| { status: "redirect" }
|
|
831
|
+
| { status: "timeout" }
|
|
832
|
+
| { status: "canceled" };
|