@jxsuite/protocol 2.0.0 → 2.2.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 +11 -10
- package/package.json +2 -2
- package/src/routes.ts +54 -9
- package/src/types.ts +177 -7
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# @jxsuite/protocol
|
|
2
2
|
|
|
3
3
|
The **Studio Backend Protocol**: the wire types and canonical route table that
|
|
4
|
-
every Jx Studio backend implements. Jx Studio is backend-agnostic
|
|
4
|
+
every Jx Studio backend implements. Jx Studio is backend-agnostic. The editor
|
|
5
5
|
talks to whatever registered a `StudioPlatform` adapter (see
|
|
6
6
|
`@jxsuite/studio/platform`), and every adapter ultimately speaks the contract
|
|
7
7
|
defined here. This package is the single source of truth for that contract:
|
|
8
8
|
|
|
9
|
-
- `@jxsuite/protocol/types
|
|
9
|
+
- `@jxsuite/protocol/types`: the request/response shapes (`DirEntry`,
|
|
10
10
|
`GitStatusResult`, `StarterInfo`, `AiModelsResponse`, `CfConnection`, …).
|
|
11
|
-
Environment-agnostic
|
|
12
|
-
Cloudflare Workers alike.
|
|
13
|
-
- `@jxsuite/protocol/routes
|
|
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
14
|
with methods, one-line contract summaries, and per-route optionality.
|
|
15
15
|
|
|
16
16
|
## Who implements this
|
|
17
17
|
|
|
18
18
|
| Backend | Transport |
|
|
19
19
|
| ------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
20
|
-
| `@jxsuite/server` (dev server) | The literal `/__studio/*` HTTP routes
|
|
20
|
+
| `@jxsuite/server` (dev server) | The literal `/__studio/*` HTTP routes (the reference implementation) |
|
|
21
21
|
| Desktop (Electrobun / Chromium) | Typed RPC / WebSocket JSON-RPC dispatching into the same handlers |
|
|
22
22
|
| Cloud platforms | HTTP behind a session gateway (e.g. `/api/v1/p/:owner/:repo/:branch/studio/*`), same sub-paths and shapes |
|
|
23
23
|
|
|
@@ -28,7 +28,7 @@ when any shape changes incompatibly.
|
|
|
28
28
|
## Core vs optional
|
|
29
29
|
|
|
30
30
|
Optional routes back optional `StudioPlatform` members. Omitting one is not an
|
|
31
|
-
error
|
|
31
|
+
error. Studio degrades exactly as each entry's `degradation` describes (the
|
|
32
32
|
starter picker empties, the Projects catalogue hides, the Publish panel
|
|
33
33
|
explains git-push publishing, and so on). `coreRouteNames()` /
|
|
34
34
|
`optionalRouteNames()` split the table programmatically; a conformance test
|
|
@@ -51,9 +51,10 @@ doc?, options? }` through the project's format registry; without it only
|
|
|
51
51
|
backend holds credentials (Studio then unlocks the assistant without a
|
|
52
52
|
locally stored key) and `managed: true` when the platform brokers them.
|
|
53
53
|
- **`cf/proxy`** is an allowlisted Cloudflare API passthrough (accounts and
|
|
54
|
-
Pages projects/deployments only). Credentials are injected by the backend
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
57
58
|
- **Errors** are `{ error, code?, detail? }` (`ErrorBody`) with a meaningful
|
|
58
59
|
HTTP status; `code` is the machine-readable discriminator Studio switches
|
|
59
60
|
on (e.g. `remote_moved`, `cf_not_connected`, `needs_installation_access`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/protocol",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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": "^1.
|
|
30
|
+
"@jxsuite/schema": "^2.1.0"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/src/routes.ts
CHANGED
|
@@ -107,7 +107,8 @@ export const STUDIO_ROUTES = {
|
|
|
107
107
|
files: route(
|
|
108
108
|
"GET",
|
|
109
109
|
"/__studio/files",
|
|
110
|
-
"List a directory (DirEntry[]); with ?glob=<pattern>, search matching
|
|
110
|
+
"List a directory (DirEntry[]) in stable path order; with ?glob=<pattern>, search matching " +
|
|
111
|
+
"files project-wide",
|
|
111
112
|
),
|
|
112
113
|
fileRead: route("GET", "/__studio/file", "Read a file's text content"),
|
|
113
114
|
fileWrite: route("PUT", "/__studio/file", "Write a file's text content"),
|
|
@@ -140,13 +141,20 @@ export const STUDIO_ROUTES = {
|
|
|
140
141
|
"The canvas cannot address project files by URL, so a host whose origin does not serve the " +
|
|
141
142
|
"site URL space renders no component $refs and no images.",
|
|
142
143
|
),
|
|
143
|
-
fileRename: route(
|
|
144
|
+
fileRename: route(
|
|
145
|
+
"POST",
|
|
146
|
+
"/__studio/file/rename",
|
|
147
|
+
"Rename/move (+ refactor report). `from`/`to` are server-root-relative; every path in the " +
|
|
148
|
+
"report is relative to the active project (specs/server.md §4.1)",
|
|
149
|
+
),
|
|
144
150
|
references: route(
|
|
145
151
|
"GET",
|
|
146
152
|
"/__studio/references",
|
|
147
153
|
"Where a file or a component tag is used (?path=&tag=, at least one) → ReferencesResult " +
|
|
148
154
|
"{files, filesReferencing, refsTotal} — the read side of the rename refactor's own walker, " +
|
|
149
|
-
"cached until the backend's watcher sees the tree move (backs findReferences)"
|
|
155
|
+
"cached until the backend's watcher sees the tree move (backs findReferences). `?path=` is " +
|
|
156
|
+
"server-root-relative and every path in the answer is relative to the active project; a " +
|
|
157
|
+
"target outside that project is a 400, never a zero-result 200 (specs/server.md §4.1)",
|
|
150
158
|
'Usage counts are hidden: no "Used on N pages" in the inspector, no Selection: Find Usages, ' +
|
|
151
159
|
"and delete/rename confirmations state no reference count.",
|
|
152
160
|
),
|
|
@@ -235,12 +243,49 @@ export const STUDIO_ROUTES = {
|
|
|
235
243
|
* these paths mean the project's SOURCES, and a built page addresses its own OUTPUT by the same
|
|
236
244
|
* paths (`/components/x.js` is the formula module here and the custom element there), so a
|
|
237
245
|
* backend serves the built site somewhere of its own and says where.
|
|
246
|
+
*
|
|
247
|
+
* A backend that cannot run the compiler at all — a hosted one executes no project JS and has no
|
|
248
|
+
* bundler, image pipeline or filesystem — may instead serve the working tree as a site and let
|
|
249
|
+
* the runtime assemble each page in the reader's browser. `mode` reports which happened, and an
|
|
250
|
+
* absent `mode` means `built`, so a backend predating the field keeps its meaning.
|
|
238
251
|
*/
|
|
239
252
|
buildSite: route(
|
|
240
253
|
"POST",
|
|
241
254
|
"/__studio/build",
|
|
242
|
-
"Build the site
|
|
243
|
-
"
|
|
255
|
+
"Build or render the site → {routes, files, errors, mode?, url}",
|
|
256
|
+
"Build Site reports that this target cannot build.",
|
|
257
|
+
),
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Preview the site LIVE at a route, with no build on the path.
|
|
261
|
+
*
|
|
262
|
+
* The sibling of `buildSite` and what `Open in Browser` reaches first. Nothing is compiled: the
|
|
263
|
+
* working tree is composed on demand and the runtime assembles each page in the reader's browser,
|
|
264
|
+
* so what opens carries edits nobody has saved (see `previewOverlay`).
|
|
265
|
+
*
|
|
266
|
+
* `reused` is the answer a caller must honour: `true` means a client already holding this
|
|
267
|
+
* project's preview took the route, and opening a tab anyway would leave the author with two on
|
|
268
|
+
* one project.
|
|
269
|
+
*/
|
|
270
|
+
previewSite: route(
|
|
271
|
+
"POST",
|
|
272
|
+
"/__studio/preview",
|
|
273
|
+
"Preview the working tree at a route → {routes, files, errors, mode, url, reused}",
|
|
274
|
+
"Open in Browser falls back to buildSite.",
|
|
275
|
+
),
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The unsaved bytes a live preview reads before the disk.
|
|
279
|
+
*
|
|
280
|
+
* POST publishes one document's bytes, DELETE retracts one (or all, with no path). The editor
|
|
281
|
+
* owns the lifecycle — it knows which documents are dirty and when a save, a close or a discard
|
|
282
|
+
* ends that — so a backend stores what it is told and decides nothing.
|
|
283
|
+
*/
|
|
284
|
+
previewOverlay: route(
|
|
285
|
+
"POST",
|
|
286
|
+
"/__studio/preview/overlay",
|
|
287
|
+
"Publish (POST) or retract (DELETE) a document's unsaved bytes → 204",
|
|
288
|
+
"A live preview shows the last saved state instead of the editor's.",
|
|
244
289
|
),
|
|
245
290
|
|
|
246
291
|
// ─── Packages ─────────────────────────────────────────────────────────────
|
|
@@ -259,11 +304,11 @@ export const STUDIO_ROUTES = {
|
|
|
259
304
|
"Whether node_modules is stale",
|
|
260
305
|
"The install-on-open prompt never shows.",
|
|
261
306
|
),
|
|
262
|
-
|
|
307
|
+
packagesVersions: route(
|
|
263
308
|
"GET",
|
|
264
|
-
"/__studio/packages/
|
|
265
|
-
"
|
|
266
|
-
"The update affordances are hidden.",
|
|
309
|
+
"/__studio/packages/versions",
|
|
310
|
+
"Newest published version of each dependency (PackageVersionInfo[])",
|
|
311
|
+
"The Latest column stays empty and the update affordances are hidden.",
|
|
267
312
|
),
|
|
268
313
|
packagesSetVersions: route(
|
|
269
314
|
"POST",
|
package/src/types.ts
CHANGED
|
@@ -453,15 +453,20 @@ export interface PackageInfo {
|
|
|
453
453
|
dev?: boolean;
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
/**
|
|
457
|
-
|
|
456
|
+
/**
|
|
457
|
+
* A dependency's pinned range beside the newest version published for it on the npm registry.
|
|
458
|
+
*
|
|
459
|
+
* Reported for EVERY dependency whose spec is a registry range, whether or not it is behind — "is
|
|
460
|
+
* this outdated?" is a comparison the caller makes, not a filter the backend applies. That matters
|
|
461
|
+
* for the Packages table, which shows a Latest column for every row: a backend that only answered
|
|
462
|
+
* about outdated packages could describe the up-to-date ones only as a blank.
|
|
463
|
+
*/
|
|
464
|
+
export interface PackageVersionInfo {
|
|
458
465
|
name: string;
|
|
459
466
|
/** The version range pinned in package.json (e.g. "^0.19.0"). */
|
|
460
467
|
current: string;
|
|
461
|
-
/** The newest published
|
|
468
|
+
/** The newest version published for this package on the registry. */
|
|
462
469
|
latest: string;
|
|
463
|
-
/** The newest version satisfying the current range, if known. */
|
|
464
|
-
wanted?: string;
|
|
465
470
|
dev?: boolean;
|
|
466
471
|
}
|
|
467
472
|
|
|
@@ -531,26 +536,139 @@ export interface ImportProgressEvent {
|
|
|
531
536
|
total?: number;
|
|
532
537
|
}
|
|
533
538
|
|
|
539
|
+
/**
|
|
540
|
+
* How many of a site's declared breakpoints the imported project keeps.
|
|
541
|
+
*
|
|
542
|
+
* A real site declares as many as it has accumulated frameworks — nine is ordinary — and each one
|
|
543
|
+
* becomes a canvas size in Studio and a column in every style editor. `limit` is the default
|
|
544
|
+
* because that is the complaint it answers; `explicit` is for an author who already knows the
|
|
545
|
+
* widths their project should have. `rounding` decides which DECLARED width backs a kept one, since
|
|
546
|
+
* the styles flip where the site says they do, not where the author wishes they did.
|
|
547
|
+
*/
|
|
548
|
+
export type BreakpointRounding = "nearest" | "down" | "up";
|
|
549
|
+
|
|
550
|
+
export interface ImportBreakpointPolicy {
|
|
551
|
+
mode: "all" | "limit" | "explicit";
|
|
552
|
+
/** `limit` only — how many to keep, evenly spaced across the declared range. */
|
|
553
|
+
count?: number;
|
|
554
|
+
/** `explicit` only — the widths the project should have, in CSS pixels. */
|
|
555
|
+
widths?: number[];
|
|
556
|
+
/** `limit` and `explicit` — how a kept width matches a declared one. */
|
|
557
|
+
rounding?: BreakpointRounding;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* The line the import stream sends the moment the destination exists and holds a `project.json`.
|
|
562
|
+
*
|
|
563
|
+
* It is what makes an import watchable. The emit phase rewrites that file completely, so nothing
|
|
564
|
+
* here is a guess about the result — it says the directory is now a PROJECT, so the caller can open
|
|
565
|
+
* it and see the pages, components and assets arrive in its own file tree over the several minutes
|
|
566
|
+
* a crawl takes, rather than facing a welcome screen until the run ends.
|
|
567
|
+
*/
|
|
568
|
+
export interface ImportReadyEvent {
|
|
569
|
+
/** The project root, in the platform's own form. */
|
|
570
|
+
root: string;
|
|
571
|
+
}
|
|
572
|
+
|
|
534
573
|
/** Options for the import-site pipeline (StudioPlatform.importSite). */
|
|
535
574
|
export interface ImportSiteOptions {
|
|
536
575
|
/** The live site to clone; must be http(s). */
|
|
537
576
|
url: string;
|
|
538
577
|
/** Display name for the new project. */
|
|
539
578
|
name: string;
|
|
540
|
-
/**
|
|
579
|
+
/**
|
|
580
|
+
* Where the project goes, interpreted by the platform that receives it: an absolute path on
|
|
581
|
+
* desktop, project-relative on the dev server, `owner/repo` on a host whose projects are
|
|
582
|
+
* repositories. Never a path the caller assumes a filesystem for.
|
|
583
|
+
*/
|
|
541
584
|
directory: string;
|
|
542
585
|
/** Max crawl depth; 0 = single page. */
|
|
543
586
|
depth: number;
|
|
544
587
|
/** Max pages to capture. */
|
|
545
588
|
maxPages: number;
|
|
589
|
+
/** Which of the site's declared breakpoints the project keeps. Omitted means the default limit. */
|
|
590
|
+
breakpoints?: ImportBreakpointPolicy;
|
|
546
591
|
/** Refine component/prop names with the LLM (requires a key). */
|
|
547
592
|
aiComponents: boolean;
|
|
593
|
+
/**
|
|
594
|
+
* Build the emitted project and screenshot-diff every page against the original.
|
|
595
|
+
*
|
|
596
|
+
* Off by default: it runs a full compile and a second browser pass, so it roughly doubles the
|
|
597
|
+
* run. What it buys is the one number the assistant can act on — a page at 61% fidelity is a
|
|
598
|
+
* question worth asking, and every other finding is a count of things that were skipped.
|
|
599
|
+
*/
|
|
600
|
+
verify?: boolean;
|
|
601
|
+
/**
|
|
602
|
+
* Pixelmatch's per-pixel COLOUR tolerance, 0..1, for `verify` (default 0.15).
|
|
603
|
+
*
|
|
604
|
+
* It decides when two pixels count as the same colour, so it moves the score and never the
|
|
605
|
+
* outcome. `verifyMinFidelity` is the bar.
|
|
606
|
+
*/
|
|
607
|
+
verifyThreshold?: number;
|
|
608
|
+
/**
|
|
609
|
+
* The average fidelity, 0..100, this run has to reach for `verify` to report `passed`.
|
|
610
|
+
*
|
|
611
|
+
* A finding rather than a failure on this transport: the project is written and opened either
|
|
612
|
+
* way, and the summary says the bar was missed. `jx-import` uses the same number to decide an
|
|
613
|
+
* exit code, because a script in a pipeline has no reader to tell.
|
|
614
|
+
*/
|
|
615
|
+
verifyMinFidelity?: number;
|
|
548
616
|
/** OpenAI-compatible credentials, from the user's AI settings. */
|
|
549
617
|
apiKey?: string;
|
|
550
618
|
baseUrl?: string;
|
|
551
619
|
model?: string;
|
|
552
620
|
}
|
|
553
621
|
|
|
622
|
+
/** Per-page fidelity from the `verify` pass. */
|
|
623
|
+
export interface ImportVerifyPage {
|
|
624
|
+
route: string;
|
|
625
|
+
/** 0..100 — the percentage of pixels that matched the original. */
|
|
626
|
+
fidelity: number;
|
|
627
|
+
/**
|
|
628
|
+
* Console errors and uncaught exceptions the rendered page produced.
|
|
629
|
+
*
|
|
630
|
+
* What a percentage cannot say. A page that 404s on fifteen images scores badly, and only this
|
|
631
|
+
* and `failedRequests` name the reason (issue #232).
|
|
632
|
+
*/
|
|
633
|
+
consoleErrors?: number;
|
|
634
|
+
/** Requests the rendered page made that failed or answered 4xx/5xx. */
|
|
635
|
+
failedRequests?: number;
|
|
636
|
+
/** Why this page has no score at all — it could not be rendered. */
|
|
637
|
+
error?: string;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* What the run produced, carried on the import stream's terminal `done` line.
|
|
642
|
+
*
|
|
643
|
+
* It exists because the pipeline computed all of this and the HTTP layer threw it away: the `done`
|
|
644
|
+
* line carried `{root, config}` and nothing else, so the caller could report that an import had
|
|
645
|
+
* happened and nothing whatever about what it found. Every field is optional — a backend that does
|
|
646
|
+
* not send one is not broken, and an older client ignores them.
|
|
647
|
+
*/
|
|
648
|
+
export interface ImportSiteSummary {
|
|
649
|
+
pages?: { route: string; title: string; nodeCount: number }[];
|
|
650
|
+
fileCount?: number;
|
|
651
|
+
/** Soft failures the pipeline recorded: assets that would not download, pages it skipped. */
|
|
652
|
+
warnings?: string[];
|
|
653
|
+
/** Present only when `verify` was requested and reference screenshots were captured. */
|
|
654
|
+
verify?: {
|
|
655
|
+
averageFidelity: number;
|
|
656
|
+
reportDir: string;
|
|
657
|
+
pages?: ImportVerifyPage[];
|
|
658
|
+
/**
|
|
659
|
+
* Whether the run met its bar: the project built cleanly, every page rendered, and the average
|
|
660
|
+
* fidelity reached `minFidelity`. Over HTTP that bar is 0 by default, so `passed: false` from
|
|
661
|
+
* this backend means the project did not build or a page did not render — not that the clone
|
|
662
|
+
* merely scored low. The `jx-import` CLI sets a real floor and exits non-zero.
|
|
663
|
+
*/
|
|
664
|
+
passed?: boolean;
|
|
665
|
+
/** The bar `passed` was measured against, 0..100. */
|
|
666
|
+
minFidelity?: number;
|
|
667
|
+
/** Errors the compiler reported building the emitted project. */
|
|
668
|
+
buildErrors?: string[];
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
554
672
|
// ─── AI proxy ────────────────────────────────────────────────────────────────
|
|
555
673
|
|
|
556
674
|
/** One entry of the AI proxy's model catalogue. */
|
|
@@ -587,9 +705,61 @@ export interface AiModelsResponse {
|
|
|
587
705
|
|
|
588
706
|
// ─── Cloudflare publish surface ──────────────────────────────────────────────
|
|
589
707
|
|
|
590
|
-
/**
|
|
708
|
+
/**
|
|
709
|
+
* Cloudflare connection state (see StudioPlatform.cfConnection).
|
|
710
|
+
*
|
|
711
|
+
* Three answers, not two, and a reader tells them apart by SHAPE:
|
|
712
|
+
*
|
|
713
|
+
* - `null` — no credential and no row exists at all; nothing was ever connected here.
|
|
714
|
+
* - `{connected: false}` — a credential exists but the provider rejected it. That is the dev server's
|
|
715
|
+
* stored-token-invalid case, and the publish panel's "Replace token" branch keys on it.
|
|
716
|
+
* - `{connected: true, needsReconnect: true}` — a brokered row exists whose OAuth grant has lapsed.
|
|
717
|
+
* Its access token is gone or unrenewable, so every Cloudflare call will fail until the user goes
|
|
718
|
+
* back through the hosted flow.
|
|
719
|
+
*
|
|
720
|
+
* Each platform emits a strict subset: **cloud never emits `{connected: false}`** (a broker row it
|
|
721
|
+
* cannot use is a reconnect, not a bad token) and **the dev server never emits `needsReconnect`**
|
|
722
|
+
* (it holds a pasted token, not a grant, so there is nothing to lapse). Collapsing the lapsed case
|
|
723
|
+
* into a healthy one is precisely the defect that let the connect popup close over a dead row while
|
|
724
|
+
* the user was still typing their Cloudflare password.
|
|
725
|
+
*/
|
|
591
726
|
export interface CfConnection {
|
|
592
727
|
connected: boolean;
|
|
593
728
|
accountId?: string | undefined;
|
|
594
729
|
accountName?: string | undefined;
|
|
730
|
+
/** A broker row exists but its grant has lapsed — reconnect before any Cloudflare call. */
|
|
731
|
+
needsReconnect?: boolean | undefined;
|
|
732
|
+
/** Connected and usable, but no account has been chosen yet (multi-account connect). */
|
|
733
|
+
needsAccount?: boolean | undefined;
|
|
734
|
+
code?: "cf_reconnect_required" | "cf_account_required" | undefined;
|
|
735
|
+
reason?: string | undefined;
|
|
736
|
+
/**
|
|
737
|
+
* Whether the broker holds a refresh token for this row. Diagnostic: without one the connection
|
|
738
|
+
* dies at the access token's expiry and cannot renew itself silently, which is a degraded state
|
|
739
|
+
* worth naming on screen rather than discovering an hour later.
|
|
740
|
+
*/
|
|
741
|
+
hasRefreshToken?: boolean | undefined;
|
|
742
|
+
/** Unix seconds at which the current access token expires (after any silent refresh). */
|
|
743
|
+
expiresAt?: number | undefined;
|
|
595
744
|
}
|
|
745
|
+
|
|
746
|
+
/** One Cloudflare account the connected grant can reach (the account picker's row). */
|
|
747
|
+
export interface CfAccountSummary {
|
|
748
|
+
id: string;
|
|
749
|
+
name: string;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* How an interactive `cfConnect` ended.
|
|
754
|
+
*
|
|
755
|
+
* A connect flow has four endings and only one of them is a failure, so they are a union rather
|
|
756
|
+
* than a nullable connection: `redirect` means the page is already navigating away and the caller
|
|
757
|
+
* must render nothing, `timeout` means the deadline passed with no confirmation, and `canceled`
|
|
758
|
+
* means the user closed the popup. Returning `null` for all three made "the user changed their
|
|
759
|
+
* mind" indistinguishable from "this browser blocked the popup", and the UI apologised for both.
|
|
760
|
+
*/
|
|
761
|
+
export type CfConnectOutcome =
|
|
762
|
+
| { status: "connected"; connection: CfConnection }
|
|
763
|
+
| { status: "redirect" }
|
|
764
|
+
| { status: "timeout" }
|
|
765
|
+
| { status: "canceled" };
|