@kottera/tera-web 0.1.0-rc.5 → 0.1.0-rc.7
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 +7 -3
- package/dist/client.d.ts +32 -1
- package/dist/client.js +69 -7
- package/dist/feed/connection.d.ts +6 -0
- package/dist/feed/connection.js +6 -0
- package/dist/generated/kit.d.ts +70 -2
- package/dist/generated/kit.js +86 -0
- package/dist/generated/shares.d.ts +3 -1
- package/dist/generated/workorder-events.d.ts +1 -1
- package/dist/generated/workorder.d.ts +82 -0
- package/dist/generated/wsstream.d.ts +34 -0
- package/dist/generated/wsstream.js +28 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +9 -0
- package/dist/signin.d.ts +48 -0
- package/dist/signin.js +74 -0
- package/dist/stream/connection.d.ts +145 -0
- package/dist/stream/connection.js +377 -0
- package/dist/terminal/view.d.ts +168 -0
- package/dist/terminal/view.js +540 -0
- package/dist/terminalwire/codec.d.ts +176 -0
- package/dist/terminalwire/codec.js +547 -0
- package/dist/unreached.d.ts +26 -0
- package/dist/unreached.js +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,11 +11,15 @@ author's — and has no runtime dependencies. It is not yet published.
|
|
|
11
11
|
|------|---------------|
|
|
12
12
|
| `src/index.ts` | The package's one entry point. Everything public is re-exported here. |
|
|
13
13
|
| `src/routes.ts` | The route prefix, `routePath`, and the app's base (`appBase`, `underBase`) every route and the sign-out are reached under. |
|
|
14
|
-
| `src/client.ts` | `TeraClient`, the typed client over D3's routes (D4 items 1, 3 and 4): `me()`, `invoke(capability, params)`, `feed(options)` (a `FeedConnection` read with the client's fetch), `upload(file, options)` with progress, and `signOut()`. A kit refusal throws `KitRequestError`; a verb's refusal answers as its verdict. |
|
|
14
|
+
| `src/client.ts` | `TeraClient`, the typed client over D3's routes (D4 items 1, 3 and 4): `me()`, `invoke(capability, params)`, `feed(options)` (a `FeedConnection` read with the client's fetch), `stream(options)` (a `StreamConnection` probed with the client's fetch), `terminal(options)` (a `TerminalView` opened the same way), `upload(file, options)` with progress, and `signOut()`. A kit refusal throws `KitRequestError`; a verb's refusal answers as its verdict. |
|
|
15
15
|
| `src/phone.ts` | `phoneURL(me, location, base?)`, the "Open on your phone" link (ADR-177 D4): `me().phone_path` resolved against the app's base (`/` by default, `/plugins/<name>/` on a path-based installation), with `next` set to `location.href`, fragment included. tera's proxy sends it on to tera's phone page, which draws the page's URL as a QR code, so the page needs no knowledge of tera's URL. A `phone_path` that is not relative to the base throws. |
|
|
16
|
+
| `src/signin.ts` | Signing in again after tera forgets the page's session, as a daemon restart does: `isSignInRequired(response)` recognises the UI proxy's 401 `sign_in_required`, and `reSignIn()` loads the page again, which signs the person in with no prompt while their tera session lives. `TeraClient`, `FeedConnection` and `StreamConnection` do it on their own, once; a page reading its own routes with its own `fetch` applies `isSignInRequired`. |
|
|
17
|
+
| `src/unreached.ts` | The codes tera's UI proxy answers the page's script with in the kit's envelope when the plugin did not answer: `PLUGIN_BUSY` and `PLUGIN_STARTING` (503, with `Retry-After`), `PLUGIN_RUNNER_UNREACHABLE` and `PLUGIN_NOT_ANSWERING` (502), all retryable, listed as `PLUGIN_UNREACHED_CODES`. |
|
|
16
18
|
| `src/stepup.ts` | A person's step-up from the page's side (ADR-173 D11): the step-up header, the `return_to` the navigation fallback carries, and `StepUpError`. Every `TeraClient` request — `me()`, `invoke()`, `upload()` and `fetch(path)` to the plugin's own routes — has the person confirm a `step_up_required` and sends the request once more; `stepUp(id)` reads `GET /tera/v1/step-ups/{id}`. |
|
|
17
|
-
| `src/generated/` | The contract, generated from the committed schemas under `sdk/plugin/webui/schema/`, `sdk/workorder/schema/`, `sdk/feed/schema/` and `sdk/
|
|
18
|
-
| `src/feed/` | The
|
|
19
|
+
| `src/generated/` | The contract, generated from the committed schemas under `sdk/plugin/webui/schema/`, `sdk/workorder/schema/`, `sdk/feed/schema/`, `sdk/shares/schema/` and `sdk/wsstream/schema/` (D4 item 1; the share types, ADR-174 D14; the close codes, ADR-181 D15): one type per schema, the kit's route table as `KIT_ROUTE_TABLE`, a closed set of named constants as a map by name (`STREAM_CLOSE_CODES`), and the maps from a route or a feed event to the type it carries. Never hand-edited, and never hand-write a type it declares: `make web-types` regenerates it, and `TestWebTypes_MatchCommitted` (`cmd/internal/genwebtypes`) fails on a hand edit or a stale generation. |
|
|
20
|
+
| `src/feed/` | The feed connection (D4 item 2). |
|
|
21
|
+
| `src/stream/` | `StreamConnection`, one WebSocket to a kit stream route (ADR-181 D9, D11 item 1), reopened by the close code the far side chose: after backoff on `going_away`, `retry_later` and any code it does not know, at once on `reconnect` (three in 30 seconds, then backoff), never on `refused` or `too_big`, and on a normal closure as the protocol above said with `onNormalClose`. A failed handshake is settled by one probe of `GET /tera/v1/me`; more than 1 MiB unsent is a defect, closed and reconnected; `reconnect(reason)` gives up on a socket the protocol above finds dead; `close()` is the page's own end. |
|
|
22
|
+
| `src/terminal/` | `TerminalView`, one view of an attended session's terminal (ADR-181 D11 item 2): `terminalwire` over a `StreamConnection` to the kit's terminal route, holding ADR-180's client duties itself — the resume offset in bytes handed to the renderer, an `ACK` only for bytes the renderer's `parsed` callback reported, at most 64 KiB of `INPUT` beyond `INPUT_ACK` (a paste queued in the view), a one-byte keystroke the window cannot hold sent as `SIGNAL` (P-R2), input dropped while history is drawn, `CONTROL {take}` before a viewer's first keystroke, `RESIZE` only while the view sets the size, and the step after each `CLOSE` reason. The renderer is named by its shape (`TerminalRenderer`, which xterm.js's `Terminal` meets), so xterm.js is the page's dependency, not the package's. Its tests drive it with a scripted socket and a recording renderer; no real browser runs here. |
|
|
19
23
|
| `src/**/*.test.ts` | The suite, beside what it tests. |
|
|
20
24
|
|
|
21
25
|
## Tests
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { FeedConnection, type FeedConnectionOptions } from "./feed/connection.ts";
|
|
2
|
+
import { StreamConnection, type StreamConnectionOptions } from "./stream/connection.ts";
|
|
3
|
+
import { TerminalView, type TerminalViewOptions } from "./terminal/view.ts";
|
|
2
4
|
import type { KitError, KitInvokeResponse, KitMeResponse, KitStepUpResponse, KitUploadResponse } from "./generated/kit.ts";
|
|
3
5
|
import { type StepUpChallenge, type StepUpOptions } from "./stepup.ts";
|
|
4
6
|
/** The body of a kit failure: the envelope's `error` member. */
|
|
@@ -107,6 +109,10 @@ export interface TeraClientOptions {
|
|
|
107
109
|
}
|
|
108
110
|
/** The options of {@link TeraClient.feed}: a {@link FeedConnection}'s, with the opener the client's own. */
|
|
109
111
|
export type FeedOptions = Omit<FeedConnectionOptions, "open" | "base">;
|
|
112
|
+
/** The options of {@link TeraClient.stream}: a {@link StreamConnection}'s, with the probe's fetch the client's own. */
|
|
113
|
+
export type StreamOptions = Omit<StreamConnectionOptions, "fetch" | "base" | "reSignIn">;
|
|
114
|
+
/** What {@link TeraClient.terminal} takes: a view's options but what the client supplies. */
|
|
115
|
+
export type TerminalOptions = Omit<TerminalViewOptions, "fetch" | "base" | "reSignIn">;
|
|
110
116
|
export declare class TeraClient {
|
|
111
117
|
private readonly base;
|
|
112
118
|
private readonly fetchFn;
|
|
@@ -118,6 +124,8 @@ export declare class TeraClient {
|
|
|
118
124
|
private readonly pollMs;
|
|
119
125
|
/** Whether the step-up the page was loaded back with has been taken up. */
|
|
120
126
|
private resumeTaken;
|
|
127
|
+
/** Whether this client has loaded the page again to sign in. */
|
|
128
|
+
private signingIn;
|
|
121
129
|
constructor(options?: TeraClientOptions);
|
|
122
130
|
/** `GET /tera/v1/me`: who is signed in, and where to send them to sign out. */
|
|
123
131
|
me(): Promise<KitMeResponse>;
|
|
@@ -138,6 +146,18 @@ export declare class TeraClient {
|
|
|
138
146
|
* the page no longer wants it.
|
|
139
147
|
*/
|
|
140
148
|
feed(options: FeedOptions): FeedConnection;
|
|
149
|
+
/**
|
|
150
|
+
* A {@link StreamConnection} to one of the kit's stream routes, its failed
|
|
151
|
+
* handshakes probed with this client's fetch. It opens at once: call
|
|
152
|
+
* `close()` on it when the page no longer wants it.
|
|
153
|
+
*/
|
|
154
|
+
stream(options: StreamOptions): StreamConnection;
|
|
155
|
+
/**
|
|
156
|
+
* A {@link TerminalView} of an attended session's terminal at the kit's
|
|
157
|
+
* terminal route, its failed handshakes probed with this client's fetch. It
|
|
158
|
+
* opens at once: call `detach()` on it when the pane closes.
|
|
159
|
+
*/
|
|
160
|
+
terminal(options: TerminalOptions): TerminalView;
|
|
141
161
|
/**
|
|
142
162
|
* `POST /tera/v1/uploads`: sends one file as the signed-in person and
|
|
143
163
|
* answers the staged artifact, whose `handle` a Work Order's attachments
|
|
@@ -159,9 +179,20 @@ export declare class TeraClient {
|
|
|
159
179
|
* person and the request sent once more with `Tera-Step-Up`; any other
|
|
160
180
|
* answer, a success or a refusal, is returned as it arrived. A body sent
|
|
161
181
|
* again must be one that can be: a string, a `Blob`, `FormData` or a
|
|
162
|
-
* buffer, never a stream.
|
|
182
|
+
* buffer, never a stream. A `sign_in_required` answer loads the page again
|
|
183
|
+
* ({@link TeraClient.reSignIn}) and is returned as it arrived.
|
|
163
184
|
*/
|
|
164
185
|
fetch(path: string, init?: RequestInit): Promise<Response>;
|
|
186
|
+
/**
|
|
187
|
+
* Loads the page again to sign the person in to the plugin's origin: a
|
|
188
|
+
* top-level navigation to the page's own URL, which tera's UI proxy answers
|
|
189
|
+
* with its silent sign-in. The client does it itself on a
|
|
190
|
+
* `sign_in_required` answer; a page calls it for one it read on its own.
|
|
191
|
+
* It navigates once per client: a second call does nothing.
|
|
192
|
+
*/
|
|
193
|
+
reSignIn(): void;
|
|
194
|
+
/** Loads the page again when `request` rejects as `sign_in_required`, and rejects as it did. */
|
|
195
|
+
private signInOnRefusal;
|
|
165
196
|
/**
|
|
166
197
|
* Has the person confirm the step-up `challenge` names, and answers its id
|
|
167
198
|
* once it is satisfied, to send the act again with. It opens the step-up's
|
package/dist/client.js
CHANGED
|
@@ -43,6 +43,15 @@
|
|
|
43
43
|
* params. A step-up the person declines, lets expire or walks away from
|
|
44
44
|
* rejects with a {@link StepUpError}, and the request is not sent again.
|
|
45
45
|
*
|
|
46
|
+
* # Signing in again
|
|
47
|
+
*
|
|
48
|
+
* A request tera's UI proxy answers `sign_in_required` — the person's session
|
|
49
|
+
* on the plugin's origin is gone, as it is after a daemon restart — loads the
|
|
50
|
+
* page again ({@link TeraClient.reSignIn}), which signs the person in, and
|
|
51
|
+
* still rejects (or, from {@link TeraClient.fetch}, answers) as it arrived.
|
|
52
|
+
* The page is loaded again once per client, however many requests were
|
|
53
|
+
* refused together. A feed or stream the client opens does the same.
|
|
54
|
+
*
|
|
46
55
|
* It is a plain class with an injected fetch, request factory, navigation,
|
|
47
56
|
* window opener and timer, so its tests drive it against a fake and no
|
|
48
57
|
* network.
|
|
@@ -50,6 +59,9 @@
|
|
|
50
59
|
import { appBase, isBaseRelative, routePath, underBase } from "./routes.js";
|
|
51
60
|
import { FeedConnection } from "./feed/connection.js";
|
|
52
61
|
import { fetchOpener } from "./feed/sse.js";
|
|
62
|
+
import { StreamConnection } from "./stream/connection.js";
|
|
63
|
+
import { TerminalView } from "./terminal/view.js";
|
|
64
|
+
import { SIGN_IN_REQUIRED, isSignInRequired } from "./signin.js";
|
|
53
65
|
import { STEP_UP_HEADER, STEP_UP_POLL_MS, StepUpError, defaultOpenWindow, defaultSleep, resumedStepUp, stepUpChallengeOf, stepUpPageURL, } from "./stepup.js";
|
|
54
66
|
/**
|
|
55
67
|
* The `code` of a {@link KitRequestError} whose answer was not the kit's
|
|
@@ -102,6 +114,8 @@ export class TeraClient {
|
|
|
102
114
|
pollMs;
|
|
103
115
|
/** Whether the step-up the page was loaded back with has been taken up. */
|
|
104
116
|
resumeTaken = false;
|
|
117
|
+
/** Whether this client has loaded the page again to sign in. */
|
|
118
|
+
signingIn = false;
|
|
105
119
|
constructor(options = {}) {
|
|
106
120
|
this.base = appBase(options.base);
|
|
107
121
|
this.fetchFn = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
@@ -157,7 +171,23 @@ export class TeraClient {
|
|
|
157
171
|
* the page no longer wants it.
|
|
158
172
|
*/
|
|
159
173
|
feed(options) {
|
|
160
|
-
return new FeedConnection({ ...options, base: this.base, open: fetchOpener(this.fetchFn) });
|
|
174
|
+
return new FeedConnection({ ...options, base: this.base, open: fetchOpener(this.fetchFn), reSignIn: () => this.reSignIn() });
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A {@link StreamConnection} to one of the kit's stream routes, its failed
|
|
178
|
+
* handshakes probed with this client's fetch. It opens at once: call
|
|
179
|
+
* `close()` on it when the page no longer wants it.
|
|
180
|
+
*/
|
|
181
|
+
stream(options) {
|
|
182
|
+
return new StreamConnection({ ...options, base: this.base, fetch: this.fetchFn, reSignIn: () => this.reSignIn() });
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* A {@link TerminalView} of an attended session's terminal at the kit's
|
|
186
|
+
* terminal route, its failed handshakes probed with this client's fetch. It
|
|
187
|
+
* opens at once: call `detach()` on it when the pane closes.
|
|
188
|
+
*/
|
|
189
|
+
terminal(options) {
|
|
190
|
+
return new TerminalView({ ...options, base: this.base, fetch: this.fetchFn, reSignIn: () => this.reSignIn() });
|
|
161
191
|
}
|
|
162
192
|
/**
|
|
163
193
|
* `POST /tera/v1/uploads`: sends one file as the signed-in person and
|
|
@@ -167,13 +197,13 @@ export class TeraClient {
|
|
|
167
197
|
*/
|
|
168
198
|
async upload(file, options = {}) {
|
|
169
199
|
try {
|
|
170
|
-
return await this.uploadOnce(file, options, null);
|
|
200
|
+
return await this.signInOnRefusal(this.uploadOnce(file, options, null));
|
|
171
201
|
}
|
|
172
202
|
catch (err) {
|
|
173
203
|
const challenge = err instanceof KitRequestError ? stepUpChallengeOf(err.code, err.envelope?.step_up) : null;
|
|
174
204
|
if (challenge === null)
|
|
175
205
|
throw err;
|
|
176
|
-
return this.uploadOnce(file, options, await this.confirmStepUp(challenge));
|
|
206
|
+
return this.signInOnRefusal(this.uploadOnce(file, options, await this.confirmStepUp(challenge)));
|
|
177
207
|
}
|
|
178
208
|
}
|
|
179
209
|
uploadOnce(file, options, stepUpId) {
|
|
@@ -246,15 +276,47 @@ export class TeraClient {
|
|
|
246
276
|
* person and the request sent once more with `Tera-Step-Up`; any other
|
|
247
277
|
* answer, a success or a refusal, is returned as it arrived. A body sent
|
|
248
278
|
* again must be one that can be: a string, a `Blob`, `FormData` or a
|
|
249
|
-
* buffer, never a stream.
|
|
279
|
+
* buffer, never a stream. A `sign_in_required` answer loads the page again
|
|
280
|
+
* ({@link TeraClient.reSignIn}) and is returned as it arrived.
|
|
250
281
|
*/
|
|
251
282
|
async fetch(path, init = {}) {
|
|
252
283
|
const url = underBase(this.base, path);
|
|
253
284
|
const first = await this.fetchSameOrigin(url, init, null);
|
|
285
|
+
if (await isSignInRequired(first)) {
|
|
286
|
+
this.reSignIn();
|
|
287
|
+
return first;
|
|
288
|
+
}
|
|
254
289
|
const challenge = await stepUpRefusalOf(first);
|
|
255
290
|
if (challenge === null)
|
|
256
291
|
return first;
|
|
257
|
-
|
|
292
|
+
const second = await this.fetchSameOrigin(url, init, await this.confirmStepUp(challenge));
|
|
293
|
+
if (await isSignInRequired(second))
|
|
294
|
+
this.reSignIn();
|
|
295
|
+
return second;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Loads the page again to sign the person in to the plugin's origin: a
|
|
299
|
+
* top-level navigation to the page's own URL, which tera's UI proxy answers
|
|
300
|
+
* with its silent sign-in. The client does it itself on a
|
|
301
|
+
* `sign_in_required` answer; a page calls it for one it read on its own.
|
|
302
|
+
* It navigates once per client: a second call does nothing.
|
|
303
|
+
*/
|
|
304
|
+
reSignIn() {
|
|
305
|
+
if (this.signingIn)
|
|
306
|
+
return;
|
|
307
|
+
this.signingIn = true;
|
|
308
|
+
this.navigate(this.pageURL());
|
|
309
|
+
}
|
|
310
|
+
/** Loads the page again when `request` rejects as `sign_in_required`, and rejects as it did. */
|
|
311
|
+
async signInOnRefusal(request) {
|
|
312
|
+
try {
|
|
313
|
+
return await request;
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
if (err instanceof KitRequestError && err.status === 401 && err.code === SIGN_IN_REQUIRED)
|
|
317
|
+
this.reSignIn();
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
258
320
|
}
|
|
259
321
|
/**
|
|
260
322
|
* Has the person confirm the step-up `challenge` names, and answers its id
|
|
@@ -346,13 +408,13 @@ export class TeraClient {
|
|
|
346
408
|
}
|
|
347
409
|
async json(url, init, stepUp = true) {
|
|
348
410
|
try {
|
|
349
|
-
return await this.jsonOnce(url, init, null);
|
|
411
|
+
return await this.signInOnRefusal(this.jsonOnce(url, init, null));
|
|
350
412
|
}
|
|
351
413
|
catch (err) {
|
|
352
414
|
const challenge = stepUp && err instanceof KitRequestError ? stepUpChallengeOf(err.code, err.envelope?.step_up) : null;
|
|
353
415
|
if (challenge === null)
|
|
354
416
|
throw err;
|
|
355
|
-
return this.jsonOnce(url, init, await this.confirmStepUp(challenge));
|
|
417
|
+
return this.signInOnRefusal(this.jsonOnce(url, init, await this.confirmStepUp(challenge)));
|
|
356
418
|
}
|
|
357
419
|
}
|
|
358
420
|
async jsonOnce(url, init, stepUpId) {
|
|
@@ -99,6 +99,12 @@ export interface FeedConnectionOptions {
|
|
|
99
99
|
orderKey?: (record: FeedFrameRecord) => number | undefined;
|
|
100
100
|
/** Opens one stream; {@link fetchOpener} when absent. */
|
|
101
101
|
open?: FeedOpener;
|
|
102
|
+
/**
|
|
103
|
+
* Loads the page again when tera's UI proxy refuses the stream
|
|
104
|
+
* `sign_in_required`, which signs the person in; {@link reSignIn} when
|
|
105
|
+
* absent. A {@link TeraClient}'s feed passes the client's own.
|
|
106
|
+
*/
|
|
107
|
+
reSignIn?: () => void;
|
|
102
108
|
clock?: Clock;
|
|
103
109
|
/** {@link browserSignals} when absent. */
|
|
104
110
|
signals?: FeedSignals;
|
package/dist/feed/connection.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* | a `reauthenticate` event (D7: the token expired) | reconnect at once |
|
|
17
17
|
* | a `credential_expired` status (D7, the same ending) | reconnect at once |
|
|
18
18
|
* | a status reporting loss | one replay, rationed (see Loss) |
|
|
19
|
+
* | a 401 `sign_in_required` (tera forgot the session) | STOP, and load the page again |
|
|
19
20
|
* | any other 4xx, a 501, or a success that is not an | STOP: a refusal. A person has to |
|
|
20
21
|
* | event stream | act ({@link FeedConnection.retry}) |
|
|
21
22
|
*
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
*/
|
|
66
67
|
import { routePath, underBase, ROOT_BASE } from "../routes.js";
|
|
67
68
|
import { fetchOpener, isRefusal } from "./sse.js";
|
|
69
|
+
import { isSignInRequiredAnswer, reSignIn } from "../signin.js";
|
|
68
70
|
export const BASE_DELAY_MS = 1_000;
|
|
69
71
|
export const MAX_DELAY_MS = 30_000;
|
|
70
72
|
/** Replays allowed in {@link REPLAY_WINDOW_MS} before a loss is surfaced. */
|
|
@@ -233,6 +235,10 @@ export class FeedConnection {
|
|
|
233
235
|
this.generation += 1;
|
|
234
236
|
this.teardown();
|
|
235
237
|
this.set({ phase: "refused", refusal: err, lastError: err.message, delayMs: 0 });
|
|
238
|
+
// The person's session on the plugin's origin is gone; only a page
|
|
239
|
+
// load brings it back, and that ends this connection with the page.
|
|
240
|
+
if (isSignInRequiredAnswer(err.status, err.contentType, err.body))
|
|
241
|
+
(this.options.reSignIn ?? reSignIn)();
|
|
236
242
|
return;
|
|
237
243
|
}
|
|
238
244
|
this.reopen(err instanceof Error ? err.message : String(err));
|
package/dist/generated/kit.d.ts
CHANGED
|
@@ -2,11 +2,11 @@ import type { FeedFrameRecord, FeedFrameStatus } from "./feed.ts";
|
|
|
2
2
|
/**
|
|
3
3
|
* The vocabulary of the kit's own error codes (sdk/plugin/webui.KitCodes).
|
|
4
4
|
*
|
|
5
|
-
* Every code the kit answers with when no verb gave a verdict. Only tera_unavailable
|
|
5
|
+
* Every code the kit answers with when no verb gave a verdict, and sign_in_required, which tera's UI proxy answers in front of the kit when a request that is not a page load reaches the plugin's origin with no live session: load the page again to sign in; and the codes the proxy answers such a request with when it did not reach the plugin: plugin_busy (503, the plugin is at its runner link's stream cap, with Retry-After), plugin_starting (503, tera has not yet settled where the plugin runs, with Retry-After), plugin_runner_unreachable (502, the plugin's runner has no live link) and plugin_not_answering (502, the plugin's UI did not answer on its socket). Only tera_unavailable and those four are retryable. A failure envelope may also carry a verb's own code, or tera's own code for a call it refused, verbatim, which is not a member: a reader keeps a code it does not know as it arrived and branches on retryable.
|
|
6
6
|
*
|
|
7
7
|
* Schema: sdk/plugin/webui/schema/v1/error-code.json
|
|
8
8
|
*/
|
|
9
|
-
export type KitErrorCode = "unauthenticated" | "cross_origin" | "route_not_found" | "method_not_allowed" | "route_not_served" | "invalid_request" | "exchange_refused" | "tera_unavailable" | "step_up_required";
|
|
9
|
+
export type KitErrorCode = "unauthenticated" | "cross_origin" | "route_not_found" | "method_not_allowed" | "route_not_served" | "invalid_request" | "exchange_refused" | "tera_unavailable" | "step_up_required" | "sign_in_required" | "plugin_busy" | "plugin_starting" | "plugin_runner_unreachable" | "plugin_not_answering";
|
|
10
10
|
/**
|
|
11
11
|
* Kit error envelope.
|
|
12
12
|
*
|
|
@@ -33,6 +33,7 @@ export type KitError = {
|
|
|
33
33
|
url?: string;
|
|
34
34
|
methods?: string[];
|
|
35
35
|
expires_at?: string;
|
|
36
|
+
label?: string;
|
|
36
37
|
state?: string;
|
|
37
38
|
};
|
|
38
39
|
};
|
|
@@ -282,6 +283,73 @@ export declare const KIT_ROUTE_TABLE: {
|
|
|
282
283
|
readonly schema: "https://kottera.dev/schemas/plugin/webui/step-up.response/v1";
|
|
283
284
|
readonly description: "The step-up's state.";
|
|
284
285
|
};
|
|
286
|
+
}, {
|
|
287
|
+
readonly name: "terminal";
|
|
288
|
+
readonly method: "GET";
|
|
289
|
+
readonly path: "/tera/v1/workorders/{id}/terminal";
|
|
290
|
+
readonly summary: "Open one view of an attended session's terminal as the person (ADR-181 D8): a WebSocket the kit splices to tera's terminal door, opened with the person's token. Which Work Order it is, and whether the person may see it, is tera's to decide; the kit keeps no list of sessions. Every refusal after the upgrade is a close: a query of the wrong shape is refused invalid_request; tera's 401 is reconnect, reason reauthenticate, and a second within the minute refused; its 400, 403 and 404 are refused with tera's code (not_attendee, attend_view_not_granted, not_found); its 410 is refused, reason ended, a session with no process and none coming without the person's act; its 409 (not_running, a session whose process is coming), 408, 429 and 5xx, and no answer, are retry_later with tera's code or tera_unavailable. A text message is refused binary_only.";
|
|
291
|
+
readonly same_origin: true;
|
|
292
|
+
readonly path_params: readonly [{
|
|
293
|
+
readonly name: "id";
|
|
294
|
+
readonly description: "The attended Work Order's id.";
|
|
295
|
+
readonly required: true;
|
|
296
|
+
}];
|
|
297
|
+
readonly query: readonly [{
|
|
298
|
+
readonly name: "view";
|
|
299
|
+
readonly description: "The view's id: a canonical, lower-case UUIDv7 the page mints per pane and keeps across reconnects.";
|
|
300
|
+
readonly required: true;
|
|
301
|
+
}, {
|
|
302
|
+
readonly name: "want";
|
|
303
|
+
readonly description: "Whether the view asks to type (control) or only to watch (view).";
|
|
304
|
+
readonly required: true;
|
|
305
|
+
readonly values: readonly ["control", "view"];
|
|
306
|
+
}, {
|
|
307
|
+
readonly name: "resume";
|
|
308
|
+
readonly description: "The output offset the page has handed to its renderer so far, a decimal integer from 0 to 9007199254740991; absent on a fresh view, which replays.";
|
|
309
|
+
}, {
|
|
310
|
+
readonly name: "cols";
|
|
311
|
+
readonly description: "The view's width in cells, a decimal integer from 1 to 65535.";
|
|
312
|
+
readonly required: true;
|
|
313
|
+
}, {
|
|
314
|
+
readonly name: "rows";
|
|
315
|
+
readonly description: "The view's height in cells, a decimal integer from 1 to 65535.";
|
|
316
|
+
readonly required: true;
|
|
317
|
+
}];
|
|
318
|
+
readonly response: {
|
|
319
|
+
readonly media_type: "application/octet-stream";
|
|
320
|
+
readonly description: "A WebSocket upgrade (101), then messages both ways until either side closes, with the close vocabulary.";
|
|
321
|
+
};
|
|
322
|
+
readonly upgrade: "websocket";
|
|
323
|
+
readonly subprotocol: "tera.terminalwire.v1";
|
|
324
|
+
readonly messages: {
|
|
325
|
+
readonly media_type: "application/octet-stream";
|
|
326
|
+
readonly description: "One sdk/terminalwire frame per binary message, each way, at most 16388 bytes. The kit relays each as it arrived, reading no payload and redacting nothing.";
|
|
327
|
+
};
|
|
328
|
+
}];
|
|
329
|
+
readonly close_codes: readonly [{
|
|
330
|
+
readonly code: 1000;
|
|
331
|
+
readonly name: "ended";
|
|
332
|
+
readonly does: "what the protocol above said last; otherwise nothing";
|
|
333
|
+
}, {
|
|
334
|
+
readonly code: 1001;
|
|
335
|
+
readonly name: "going_away";
|
|
336
|
+
readonly does: "reconnects after backoff: the peer is stopping or stopped answering pings";
|
|
337
|
+
}, {
|
|
338
|
+
readonly code: 1009;
|
|
339
|
+
readonly name: "too_big";
|
|
340
|
+
readonly does: "nothing: a message broke the route's bound, a client defect";
|
|
341
|
+
}, {
|
|
342
|
+
readonly code: 4000;
|
|
343
|
+
readonly name: "reconnect";
|
|
344
|
+
readonly does: "reconnects at once with the same identity: a token renewal";
|
|
345
|
+
}, {
|
|
346
|
+
readonly code: 4001;
|
|
347
|
+
readonly name: "retry_later";
|
|
348
|
+
readonly does: "reconnects after backoff: the far side is unreachable or not ready";
|
|
349
|
+
}, {
|
|
350
|
+
readonly code: 4003;
|
|
351
|
+
readonly name: "refused";
|
|
352
|
+
readonly does: "nothing until something changes; the reason is the error code";
|
|
285
353
|
}];
|
|
286
354
|
};
|
|
287
355
|
/** The name of a route in KIT_ROUTE_TABLE. */
|
package/dist/generated/kit.js
CHANGED
|
@@ -167,5 +167,91 @@ export const KIT_ROUTE_TABLE = {
|
|
|
167
167
|
description: "The step-up's state.",
|
|
168
168
|
},
|
|
169
169
|
},
|
|
170
|
+
{
|
|
171
|
+
name: "terminal",
|
|
172
|
+
method: "GET",
|
|
173
|
+
path: "/tera/v1/workorders/{id}/terminal",
|
|
174
|
+
summary: "Open one view of an attended session's terminal as the person (ADR-181 D8): a WebSocket the kit splices to tera's terminal door, opened with the person's token. Which Work Order it is, and whether the person may see it, is tera's to decide; the kit keeps no list of sessions. Every refusal after the upgrade is a close: a query of the wrong shape is refused invalid_request; tera's 401 is reconnect, reason reauthenticate, and a second within the minute refused; its 400, 403 and 404 are refused with tera's code (not_attendee, attend_view_not_granted, not_found); its 410 is refused, reason ended, a session with no process and none coming without the person's act; its 409 (not_running, a session whose process is coming), 408, 429 and 5xx, and no answer, are retry_later with tera's code or tera_unavailable. A text message is refused binary_only.",
|
|
175
|
+
same_origin: true,
|
|
176
|
+
path_params: [
|
|
177
|
+
{
|
|
178
|
+
name: "id",
|
|
179
|
+
description: "The attended Work Order's id.",
|
|
180
|
+
required: true,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
query: [
|
|
184
|
+
{
|
|
185
|
+
name: "view",
|
|
186
|
+
description: "The view's id: a canonical, lower-case UUIDv7 the page mints per pane and keeps across reconnects.",
|
|
187
|
+
required: true,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "want",
|
|
191
|
+
description: "Whether the view asks to type (control) or only to watch (view).",
|
|
192
|
+
required: true,
|
|
193
|
+
values: [
|
|
194
|
+
"control",
|
|
195
|
+
"view",
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "resume",
|
|
200
|
+
description: "The output offset the page has handed to its renderer so far, a decimal integer from 0 to 9007199254740991; absent on a fresh view, which replays.",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "cols",
|
|
204
|
+
description: "The view's width in cells, a decimal integer from 1 to 65535.",
|
|
205
|
+
required: true,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "rows",
|
|
209
|
+
description: "The view's height in cells, a decimal integer from 1 to 65535.",
|
|
210
|
+
required: true,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
response: {
|
|
214
|
+
media_type: "application/octet-stream",
|
|
215
|
+
description: "A WebSocket upgrade (101), then messages both ways until either side closes, with the close vocabulary.",
|
|
216
|
+
},
|
|
217
|
+
upgrade: "websocket",
|
|
218
|
+
subprotocol: "tera.terminalwire.v1",
|
|
219
|
+
messages: {
|
|
220
|
+
media_type: "application/octet-stream",
|
|
221
|
+
description: "One sdk/terminalwire frame per binary message, each way, at most 16388 bytes. The kit relays each as it arrived, reading no payload and redacting nothing.",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
close_codes: [
|
|
226
|
+
{
|
|
227
|
+
code: 1000,
|
|
228
|
+
name: "ended",
|
|
229
|
+
does: "what the protocol above said last; otherwise nothing",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
code: 1001,
|
|
233
|
+
name: "going_away",
|
|
234
|
+
does: "reconnects after backoff: the peer is stopping or stopped answering pings",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
code: 1009,
|
|
238
|
+
name: "too_big",
|
|
239
|
+
does: "nothing: a message broke the route's bound, a client defect",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
code: 4000,
|
|
243
|
+
name: "reconnect",
|
|
244
|
+
does: "reconnects at once with the same identity: a token renewal",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
code: 4001,
|
|
248
|
+
name: "retry_later",
|
|
249
|
+
does: "reconnects after backoff: the far side is unreachable or not ready",
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
code: 4003,
|
|
253
|
+
name: "refused",
|
|
254
|
+
does: "nothing until something changes; the reason is the error code",
|
|
255
|
+
},
|
|
170
256
|
],
|
|
171
257
|
};
|
|
@@ -124,7 +124,7 @@ export type SharesMakeRequest = {
|
|
|
124
124
|
*
|
|
125
125
|
* Schema: sdk/shares/schema/v1/refusal-code.json
|
|
126
126
|
*/
|
|
127
|
-
export type SharesRefusalCode = "share_sharer_not_a_person" | "share_sharing_disabled" | "share_resource_not_shareable" | "share_resource_unknown" | "share_actor_other_installation" | "share_verb_not_shareable" | "share_verb_confers_sharing" | "share_verb_not_held" | "share_grantee_not_admitted" | "share_guest_cannot_invite" | "share_invitation_refused" | "share_grantee_is_sharer" | "share_expiry_beyond_max" | "share_cap_active" | "share_cap_pending_invitations" | "share_exists" | "share_caller_not_a_person" | "share_not_found" | "share_sharer_lost_access" | "share_expiry_not_later" | "share_admin_required" | "share_list_query" | "shares_unavailable";
|
|
127
|
+
export type SharesRefusalCode = "share_sharer_not_a_person" | "share_sharing_disabled" | "share_resource_not_shareable" | "share_resource_unknown" | "share_actor_other_installation" | "share_verb_not_shareable" | "share_verb_confers_sharing" | "share_verb_not_held" | "share_grantee_not_admitted" | "share_guest_cannot_invite" | "share_invitation_refused" | "share_grantee_is_sharer" | "share_expiry_beyond_max" | "share_cap_active" | "share_cap_pending_invitations" | "share_exists" | "share_plugin_only" | "share_caller_not_a_person" | "share_not_found" | "share_sharer_lost_access" | "share_expiry_not_later" | "share_admin_required" | "share_list_query" | "shares_unavailable";
|
|
128
128
|
/**
|
|
129
129
|
* A share act refused.
|
|
130
130
|
*
|
|
@@ -143,6 +143,8 @@ export type SharesRefusal = {
|
|
|
143
143
|
existing?: string;
|
|
144
144
|
/** On share_invitation_refused, why the address was not invited (inviter_limited, email_unavailable, ...). */
|
|
145
145
|
invitation?: string;
|
|
146
|
+
/** On share_plugin_only, the installation's name: the plugin the share is made or extended through. */
|
|
147
|
+
plugin?: string;
|
|
146
148
|
};
|
|
147
149
|
/**
|
|
148
150
|
* A share.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Schema: sdk/workorder/schema/events/v1/event-kind.workorder.json
|
|
7
7
|
*/
|
|
8
|
-
export type WorkOrderEventKind = "cohort.set" | "off_tera.wake" | "relation.set" | "relation.removed" | "off_tera.taken" | "off_tera.progress" | "off_tera.reposted" | "off_tera.late_report" | "off_tera.checks_started" | "created" | "transitioned" | "acknowledged" | "retention_expired" | "edited" | "seat.reclaimed" | "claim.overdue" | "claim.unreadable" | "subject_claim.overridden" | "seat.cooling" | "seat.credential_held" | "member_credential.requested" | "member_credential.settled" | "seat.acquired" | "seat.runner_released" | "seat.meters_voided" | "step.binding" | "session.terminated" | "postcondition.verdict" | "parked_run.unreadable" | "parked_run.reclaimed" | "dispatch.settled_unobserved" | "parked_run.parked" | "parked_run.resumed" | "resume.refused" | "dispatch.salvaged" | "workspace.retired" | "dispatch.routed" | "session.lost" | "activity_partition.pinned" | "session.continuity" | "session.context_compacted" | "upstream.answer_forwarded" | "invocation.checkpoint" | "invocation.guidance" | "invocation.guidance.ack" | "invocation.reply" | "invocation.checkpoint.unreadable" | "invocation.guidance.unreadable" | "config_pin.unreadable" | "config_pin.repinned" | "type.followed" | "observation.scrubbed" | "observation.secret-shape-redacted" | "observation.promoted" | "observation.refuted" | "observation.purged" | "conversation.input_barrier" | "walk_resume.fell_back" | "store.ahead_of_binary" | "artifact.published" | "history.dropped" | "history.archived" | "seat.work_order_released" | "step.settled_late" | "step.reattached" | "relay.unrebuilt" | "references.recorded";
|
|
8
|
+
export type WorkOrderEventKind = "cohort.set" | "off_tera.wake" | "relation.set" | "relation.removed" | "off_tera.taken" | "off_tera.progress" | "off_tera.reposted" | "off_tera.late_report" | "off_tera.checks_started" | "created" | "transitioned" | "acknowledged" | "retention_expired" | "edited" | "seat.reclaimed" | "claim.overdue" | "claim.unreadable" | "subject_claim.overridden" | "seat.cooling" | "seat.credential_held" | "member_credential.requested" | "member_credential.settled" | "seat.acquired" | "seat.runner_released" | "seat.meters_voided" | "step.binding" | "session.terminated" | "postcondition.verdict" | "parked_run.unreadable" | "parked_run.reclaimed" | "dispatch.settled_unobserved" | "parked_run.parked" | "parked_run.resumed" | "resume.refused" | "dispatch.salvaged" | "workspace.retired" | "dispatch.routed" | "session.lost" | "activity_partition.pinned" | "session.continuity" | "session.context_compacted" | "upstream.answer_forwarded" | "invocation.checkpoint" | "invocation.guidance" | "invocation.guidance.ack" | "invocation.reply" | "invocation.checkpoint.unreadable" | "invocation.guidance.unreadable" | "config_pin.unreadable" | "config_pin.repinned" | "type.followed" | "observation.scrubbed" | "observation.secret-shape-redacted" | "observation.promoted" | "observation.refuted" | "observation.purged" | "conversation.input_barrier" | "walk_resume.fell_back" | "store.ahead_of_binary" | "artifact.published" | "history.dropped" | "history.archived" | "seat.work_order_released" | "step.settled_late" | "step.reattached" | "relay.unrebuilt" | "references.recorded" | "attended.wall_reported" | "attended.signal";
|
|
9
9
|
/**
|
|
10
10
|
* `workorder` event payload (event/workorder).
|
|
11
11
|
*
|