@kottera/tera-web 0.1.0-rc.3 → 0.1.0-rc.4
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 +3 -2
- package/dist/client.d.ts +59 -5
- package/dist/client.js +203 -16
- package/dist/feed/connection.d.ts +2 -0
- package/dist/feed/connection.js +2 -2
- package/dist/generated/kit.d.ts +61 -6
- package/dist/generated/kit.js +24 -4
- package/dist/generated/shares.d.ts +196 -0
- package/dist/generated/shares.js +17 -0
- package/dist/generated/workorder.d.ts +79 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/routes.d.ts +15 -0
- package/dist/routes.js +28 -0
- package/dist/stepup.d.ts +87 -0
- package/dist/stepup.js +66 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,9 +10,10 @@ author's — and has no runtime dependencies. It is not yet published.
|
|
|
10
10
|
| Path | What it holds |
|
|
11
11
|
|------|---------------|
|
|
12
12
|
| `src/index.ts` | The package's one entry point. Everything public is re-exported here. |
|
|
13
|
-
| `src/routes.ts` | The route prefix and `
|
|
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
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. |
|
|
15
|
-
| `src/
|
|
15
|
+
| `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}`. |
|
|
16
|
+
| `src/generated/` | The contract, generated from the committed schemas under `sdk/plugin/webui/schema/`, `sdk/workorder/schema/`, `sdk/feed/schema/` and `sdk/shares/schema/` (D4 item 1; the share types, ADR-174 D14): one type per schema, the kit's route table as `KIT_ROUTE_TABLE`, 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. |
|
|
16
17
|
| `src/feed/` | The stream connection (D4 item 2). |
|
|
17
18
|
| `src/**/*.test.ts` | The suite, beside what it tests. |
|
|
18
19
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FeedConnection, type FeedConnectionOptions } from "./feed/connection.ts";
|
|
2
|
-
import type { KitError, KitInvokeResponse, KitMeResponse, KitUploadResponse } from "./generated/kit.ts";
|
|
2
|
+
import type { KitError, KitInvokeResponse, KitMeResponse, KitStepUpResponse, KitUploadResponse } from "./generated/kit.ts";
|
|
3
|
+
import { type StepUpChallenge, type StepUpOptions } from "./stepup.ts";
|
|
3
4
|
/** The body of a kit failure: the envelope's `error` member. */
|
|
4
5
|
export type KitErrorBody = KitError["error"];
|
|
5
6
|
/** A verb's refusal, as the kit passed it through: the reply envelope's `error` member. */
|
|
@@ -89,19 +90,34 @@ export interface UploadRequest {
|
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
export interface TeraClientOptions {
|
|
93
|
+
/**
|
|
94
|
+
* The path the app is served under on its origin: `/`, the default, on a
|
|
95
|
+
* plugin's own origin, and `/plugins/<name>/` on an installation that serves
|
|
96
|
+
* plugins by path. Every route, and the sign-out, is reached under it.
|
|
97
|
+
*/
|
|
98
|
+
base?: string;
|
|
92
99
|
/** The fetch every route but the upload is read with; the page's own when absent. */
|
|
93
100
|
fetch?: typeof fetch;
|
|
94
101
|
/** Makes the upload's request; `new XMLHttpRequest()` when absent. */
|
|
95
102
|
uploadRequest?: () => UploadRequest;
|
|
96
|
-
/** Sends the page to a path; `location.assign` when absent. */
|
|
103
|
+
/** Sends the page to a path or URL; `location.assign` when absent. */
|
|
97
104
|
navigate?: (path: string) => void;
|
|
105
|
+
/** How a step-up is confirmed: the window, the page's URL and the polling. */
|
|
106
|
+
stepUp?: StepUpOptions;
|
|
98
107
|
}
|
|
99
108
|
/** The options of {@link TeraClient.feed}: a {@link FeedConnection}'s, with the opener the client's own. */
|
|
100
|
-
export type FeedOptions = Omit<FeedConnectionOptions, "open">;
|
|
109
|
+
export type FeedOptions = Omit<FeedConnectionOptions, "open" | "base">;
|
|
101
110
|
export declare class TeraClient {
|
|
111
|
+
private readonly base;
|
|
102
112
|
private readonly fetchFn;
|
|
103
113
|
private readonly uploadRequest;
|
|
104
114
|
private readonly navigate;
|
|
115
|
+
private readonly openWindow;
|
|
116
|
+
private readonly pageURL;
|
|
117
|
+
private readonly sleep;
|
|
118
|
+
private readonly pollMs;
|
|
119
|
+
/** Whether the step-up the page was loaded back with has been taken up. */
|
|
120
|
+
private resumeTaken;
|
|
105
121
|
constructor(options?: TeraClientOptions);
|
|
106
122
|
/** `GET /tera/v1/me`: who is signed in, and where to send them to sign out. */
|
|
107
123
|
me(): Promise<KitMeResponse>;
|
|
@@ -115,6 +131,7 @@ export declare class TeraClient {
|
|
|
115
131
|
* if (got.status === "success") show(got.result);
|
|
116
132
|
*/
|
|
117
133
|
invoke<Result = unknown, Params = unknown>(capability: string, params?: Params): Promise<InvokeVerdict<Result>>;
|
|
134
|
+
private invokeOnce;
|
|
118
135
|
/**
|
|
119
136
|
* A {@link FeedConnection} over `GET /tera/v1/feeds/{feed}`, read with this
|
|
120
137
|
* client's fetch. It is not started: call `start()` on it, and `stop()` when
|
|
@@ -128,11 +145,48 @@ export declare class TeraClient {
|
|
|
128
145
|
* route's headers, and `onProgress` reports the body as it is sent.
|
|
129
146
|
*/
|
|
130
147
|
upload(file: Blob, options?: UploadOptions): Promise<KitUploadResponse>;
|
|
148
|
+
private uploadOnce;
|
|
149
|
+
/**
|
|
150
|
+
* `GET /tera/v1/step-ups/{id}`: where one of the person's own step-ups is in
|
|
151
|
+
* its life. A step-up that is not theirs is thrown as tera's 404, as an id
|
|
152
|
+
* no step-up carries is.
|
|
153
|
+
*/
|
|
154
|
+
stepUp(id: string): Promise<KitStepUpResponse>;
|
|
155
|
+
/**
|
|
156
|
+
* A request to one of the plugin's own routes, `path` relative to the app's
|
|
157
|
+
* base (`reviews/7/finish`), same-origin. A `step_up_required` answer in the
|
|
158
|
+
* kit's envelope — what `webui.RequireStepUp` writes — is confirmed by the
|
|
159
|
+
* person and the request sent once more with `Tera-Step-Up`; any other
|
|
160
|
+
* answer, a success or a refusal, is returned as it arrived. A body sent
|
|
161
|
+
* again must be one that can be: a string, a `Blob`, `FormData` or a
|
|
162
|
+
* buffer, never a stream.
|
|
163
|
+
*/
|
|
164
|
+
fetch(path: string, init?: RequestInit): Promise<Response>;
|
|
165
|
+
/**
|
|
166
|
+
* Has the person confirm the step-up `challenge` names, and answers its id
|
|
167
|
+
* once it is satisfied, to send the act again with. It opens the step-up's
|
|
168
|
+
* page in a new window, or navigates to it with a `return_to` naming this
|
|
169
|
+
* page when the browser opens none, and reads its state every poll interval
|
|
170
|
+
* until it is satisfied, declined or expired, closing the window it opened.
|
|
171
|
+
*
|
|
172
|
+
* Rejects with a {@link StepUpError} when the step-up ends unconfirmed, the
|
|
173
|
+
* person closes its window while it still waits, or the page navigated away
|
|
174
|
+
* to confirm it. A page loaded back from that navigation carries the id, and
|
|
175
|
+
* the first step-up it is asked for resumes that one when it is satisfied.
|
|
176
|
+
*/
|
|
177
|
+
confirmStepUp(challenge: StepUpChallenge): Promise<string>;
|
|
178
|
+
/** The step-up this page was loaded back with, once. */
|
|
179
|
+
private takeResumed;
|
|
180
|
+
private fetchSameOrigin;
|
|
131
181
|
/**
|
|
132
182
|
* Signs the person out: navigates to the `signout_path` `me()` answers,
|
|
133
|
-
* tera's own
|
|
134
|
-
* is not
|
|
183
|
+
* tera's own sign-out, resolved against the app's base. A `signout_path`
|
|
184
|
+
* that is not a path relative to the base — a root-absolute path, or one
|
|
185
|
+
* naming another origin — is refused rather than followed.
|
|
135
186
|
*/
|
|
136
187
|
signOut(): Promise<void>;
|
|
188
|
+
/** A kit route's path under the app's base. */
|
|
189
|
+
private route;
|
|
137
190
|
private json;
|
|
191
|
+
private jsonOnce;
|
|
138
192
|
}
|
package/dist/client.js
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
*
|
|
24
24
|
* # Same origin
|
|
25
25
|
*
|
|
26
|
-
* Every request is to a path under the kit's prefix
|
|
26
|
+
* Every request is to a path under the kit's prefix, below the app's base
|
|
27
|
+
* ({@link TeraClientOptions.base}), on the page's own origin,
|
|
27
28
|
* with `credentials: "same-origin"`; a fetch is also `mode: "same-origin"`, so
|
|
28
29
|
* a page that somehow aimed one elsewhere is refused by the browser rather
|
|
29
30
|
* than sent. The browser then marks it `Sec-Fetch-Site: same-origin`, which is
|
|
@@ -31,12 +32,25 @@
|
|
|
31
32
|
* claims it: a page cannot set `Sec-Fetch-*` or `Origin`, and the rule rests
|
|
32
33
|
* on that.
|
|
33
34
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* # A step-up
|
|
36
|
+
*
|
|
37
|
+
* Every request it makes handles a `step_up_required` refusal that names a
|
|
38
|
+
* page to confirm the act on (ADR-173 D11): the kit's envelope from a kit
|
|
39
|
+
* route or from the plugin's own route ({@link TeraClient.fetch}), and a Work
|
|
40
|
+
* Order verb's refusal relayed by {@link TeraClient.invoke}. It has the person
|
|
41
|
+
* confirm it ({@link TeraClient.confirmStepUp}) and sends the identical request
|
|
42
|
+
* once more, with `Tera-Step-Up: <id>`, or with `step_up` in an invoke's
|
|
43
|
+
* params. A step-up the person declines, lets expire or walks away from
|
|
44
|
+
* rejects with a {@link StepUpError}, and the request is not sent again.
|
|
45
|
+
*
|
|
46
|
+
* It is a plain class with an injected fetch, request factory, navigation,
|
|
47
|
+
* window opener and timer, so its tests drive it against a fake and no
|
|
48
|
+
* network.
|
|
36
49
|
*/
|
|
37
|
-
import { routePath } from "./routes.js";
|
|
50
|
+
import { appBase, routePath, underBase } from "./routes.js";
|
|
38
51
|
import { FeedConnection } from "./feed/connection.js";
|
|
39
52
|
import { fetchOpener } from "./feed/sse.js";
|
|
53
|
+
import { STEP_UP_HEADER, STEP_UP_POLL_MS, StepUpError, defaultOpenWindow, defaultSleep, resumedStepUp, stepUpChallengeOf, stepUpPageURL, } from "./stepup.js";
|
|
40
54
|
/**
|
|
41
55
|
* The `code` of a {@link KitRequestError} whose answer was not the kit's
|
|
42
56
|
* envelope — a proxy's page, a dropped body, a success that is not the
|
|
@@ -78,17 +92,30 @@ export class KitRequestError extends Error {
|
|
|
78
92
|
}
|
|
79
93
|
const JSON_TYPE = "application/json";
|
|
80
94
|
export class TeraClient {
|
|
95
|
+
base;
|
|
81
96
|
fetchFn;
|
|
82
97
|
uploadRequest;
|
|
83
98
|
navigate;
|
|
99
|
+
openWindow;
|
|
100
|
+
pageURL;
|
|
101
|
+
sleep;
|
|
102
|
+
pollMs;
|
|
103
|
+
/** Whether the step-up the page was loaded back with has been taken up. */
|
|
104
|
+
resumeTaken = false;
|
|
84
105
|
constructor(options = {}) {
|
|
106
|
+
this.base = appBase(options.base);
|
|
85
107
|
this.fetchFn = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
86
108
|
this.uploadRequest = options.uploadRequest ?? (() => new XMLHttpRequest());
|
|
87
109
|
this.navigate = options.navigate ?? ((path) => globalThis.location.assign(path));
|
|
110
|
+
const stepUp = options.stepUp ?? {};
|
|
111
|
+
this.openWindow = stepUp.openWindow ?? defaultOpenWindow;
|
|
112
|
+
this.pageURL = stepUp.pageURL ?? (() => globalThis.location.href);
|
|
113
|
+
this.sleep = stepUp.sleep ?? defaultSleep;
|
|
114
|
+
this.pollMs = stepUp.pollIntervalMs ?? STEP_UP_POLL_MS;
|
|
88
115
|
}
|
|
89
116
|
/** `GET /tera/v1/me`: who is signed in, and where to send them to sign out. */
|
|
90
117
|
async me() {
|
|
91
|
-
return this.json(
|
|
118
|
+
return this.json(this.route("me"), { method: "GET" });
|
|
92
119
|
}
|
|
93
120
|
/**
|
|
94
121
|
* `POST /tera/v1/invoke/{capability}`: invokes one capability as the
|
|
@@ -100,7 +127,17 @@ export class TeraClient {
|
|
|
100
127
|
* if (got.status === "success") show(got.result);
|
|
101
128
|
*/
|
|
102
129
|
async invoke(capability, params) {
|
|
103
|
-
const
|
|
130
|
+
const verdict = await this.invokeOnce(capability, params);
|
|
131
|
+
if (verdict.status !== "error")
|
|
132
|
+
return verdict;
|
|
133
|
+
const challenge = stepUpChallengeOf(verdict.error.code, verdict.error.data?.step_up);
|
|
134
|
+
if (challenge === null || !isParamsObject(params))
|
|
135
|
+
return verdict;
|
|
136
|
+
const id = await this.confirmStepUp(challenge);
|
|
137
|
+
return this.invokeOnce(capability, { ...(params ?? {}), step_up: id });
|
|
138
|
+
}
|
|
139
|
+
async invokeOnce(capability, params) {
|
|
140
|
+
const url = this.route("invoke", capability);
|
|
104
141
|
const reply = await this.json(url, {
|
|
105
142
|
method: "POST",
|
|
106
143
|
headers: { "Content-Type": JSON_TYPE },
|
|
@@ -120,7 +157,7 @@ export class TeraClient {
|
|
|
120
157
|
* the page no longer wants it.
|
|
121
158
|
*/
|
|
122
159
|
feed(options) {
|
|
123
|
-
return new FeedConnection({ ...options, open: fetchOpener(this.fetchFn) });
|
|
160
|
+
return new FeedConnection({ ...options, base: this.base, open: fetchOpener(this.fetchFn) });
|
|
124
161
|
}
|
|
125
162
|
/**
|
|
126
163
|
* `POST /tera/v1/uploads`: sends one file as the signed-in person and
|
|
@@ -128,13 +165,24 @@ export class TeraClient {
|
|
|
128
165
|
* name. Its name and description travel percent-encoded (UTF-8) in the
|
|
129
166
|
* route's headers, and `onProgress` reports the body as it is sent.
|
|
130
167
|
*/
|
|
131
|
-
upload(file, options = {}) {
|
|
168
|
+
async upload(file, options = {}) {
|
|
169
|
+
try {
|
|
170
|
+
return await this.uploadOnce(file, options, null);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
const challenge = err instanceof KitRequestError ? stepUpChallengeOf(err.code, err.envelope?.step_up) : null;
|
|
174
|
+
if (challenge === null)
|
|
175
|
+
throw err;
|
|
176
|
+
return this.uploadOnce(file, options, await this.confirmStepUp(challenge));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
uploadOnce(file, options, stepUpId) {
|
|
132
180
|
const filename = options.filename ?? file.name ?? "";
|
|
133
181
|
if (filename === "") {
|
|
134
182
|
return Promise.reject(new TypeError("upload: a filename is required: pass a File, or options.filename"));
|
|
135
183
|
}
|
|
136
184
|
const contentType = options.contentType || file.type || "application/octet-stream";
|
|
137
|
-
const url =
|
|
185
|
+
const url = this.route("uploads");
|
|
138
186
|
const { signal, onProgress } = options;
|
|
139
187
|
if (signal?.aborted)
|
|
140
188
|
return Promise.reject(abortError());
|
|
@@ -151,6 +199,8 @@ export class TeraClient {
|
|
|
151
199
|
xhr.setRequestHeader("Tera-Upload-Description", encodeURIComponent(options.description));
|
|
152
200
|
}
|
|
153
201
|
xhr.setRequestHeader("Accept", JSON_TYPE);
|
|
202
|
+
if (stepUpId !== null)
|
|
203
|
+
xhr.setRequestHeader(STEP_UP_HEADER, stepUpId);
|
|
154
204
|
if (onProgress !== undefined) {
|
|
155
205
|
xhr.upload.onprogress = (ev) => onProgress({ loaded: ev.loaded, total: ev.lengthComputable ? ev.total : file.size });
|
|
156
206
|
}
|
|
@@ -181,22 +231,138 @@ export class TeraClient {
|
|
|
181
231
|
}
|
|
182
232
|
});
|
|
183
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* `GET /tera/v1/step-ups/{id}`: where one of the person's own step-ups is in
|
|
236
|
+
* its life. A step-up that is not theirs is thrown as tera's 404, as an id
|
|
237
|
+
* no step-up carries is.
|
|
238
|
+
*/
|
|
239
|
+
async stepUp(id) {
|
|
240
|
+
return this.json(this.route("step-ups", id), { method: "GET" }, false);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* A request to one of the plugin's own routes, `path` relative to the app's
|
|
244
|
+
* base (`reviews/7/finish`), same-origin. A `step_up_required` answer in the
|
|
245
|
+
* kit's envelope — what `webui.RequireStepUp` writes — is confirmed by the
|
|
246
|
+
* person and the request sent once more with `Tera-Step-Up`; any other
|
|
247
|
+
* answer, a success or a refusal, is returned as it arrived. A body sent
|
|
248
|
+
* again must be one that can be: a string, a `Blob`, `FormData` or a
|
|
249
|
+
* buffer, never a stream.
|
|
250
|
+
*/
|
|
251
|
+
async fetch(path, init = {}) {
|
|
252
|
+
const url = underBase(this.base, path);
|
|
253
|
+
const first = await this.fetchSameOrigin(url, init, null);
|
|
254
|
+
const challenge = await stepUpRefusalOf(first);
|
|
255
|
+
if (challenge === null)
|
|
256
|
+
return first;
|
|
257
|
+
return this.fetchSameOrigin(url, init, await this.confirmStepUp(challenge));
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Has the person confirm the step-up `challenge` names, and answers its id
|
|
261
|
+
* once it is satisfied, to send the act again with. It opens the step-up's
|
|
262
|
+
* page in a new window, or navigates to it with a `return_to` naming this
|
|
263
|
+
* page when the browser opens none, and reads its state every poll interval
|
|
264
|
+
* until it is satisfied, declined or expired, closing the window it opened.
|
|
265
|
+
*
|
|
266
|
+
* Rejects with a {@link StepUpError} when the step-up ends unconfirmed, the
|
|
267
|
+
* person closes its window while it still waits, or the page navigated away
|
|
268
|
+
* to confirm it. A page loaded back from that navigation carries the id, and
|
|
269
|
+
* the first step-up it is asked for resumes that one when it is satisfied.
|
|
270
|
+
*/
|
|
271
|
+
async confirmStepUp(challenge) {
|
|
272
|
+
const resumed = this.takeResumed();
|
|
273
|
+
if (resumed !== null) {
|
|
274
|
+
const state = await this.stepUp(resumed).then((s) => s.state, () => null);
|
|
275
|
+
if (state === "satisfied")
|
|
276
|
+
return resumed;
|
|
277
|
+
}
|
|
278
|
+
const win = this.openWindow(challenge.url);
|
|
279
|
+
if (win === null) {
|
|
280
|
+
this.navigate(stepUpPageURL(challenge.url, challenge.id, this.pageURL()));
|
|
281
|
+
throw new StepUpError(challenge.id, "navigated", "the browser opened no window, so the page went to tera's to confirm the act");
|
|
282
|
+
}
|
|
283
|
+
let closedSeen = false;
|
|
284
|
+
for (;;) {
|
|
285
|
+
await this.sleep(this.pollMs);
|
|
286
|
+
let state;
|
|
287
|
+
try {
|
|
288
|
+
state = (await this.stepUp(challenge.id)).state;
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
win.close();
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
if (state === "satisfied") {
|
|
295
|
+
win.close();
|
|
296
|
+
return challenge.id;
|
|
297
|
+
}
|
|
298
|
+
if (state !== "pending") {
|
|
299
|
+
win.close();
|
|
300
|
+
throw new StepUpError(challenge.id, state, `the step-up was ${state === "spent" ? "spent by another request" : state}, and the act was not sent again`);
|
|
301
|
+
}
|
|
302
|
+
// One more read after the person closes the window, in case they
|
|
303
|
+
// confirmed it just before; then it is abandoned.
|
|
304
|
+
if (win.closed) {
|
|
305
|
+
if (closedSeen) {
|
|
306
|
+
throw new StepUpError(challenge.id, "abandoned", "the step-up's window was closed before it was confirmed");
|
|
307
|
+
}
|
|
308
|
+
closedSeen = true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** The step-up this page was loaded back with, once. */
|
|
313
|
+
takeResumed() {
|
|
314
|
+
if (this.resumeTaken)
|
|
315
|
+
return null;
|
|
316
|
+
this.resumeTaken = true;
|
|
317
|
+
try {
|
|
318
|
+
return resumedStepUp(this.pageURL());
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
fetchSameOrigin(url, init, stepUpId) {
|
|
325
|
+
const headers = new Headers(init.headers);
|
|
326
|
+
if (stepUpId !== null)
|
|
327
|
+
headers.set(STEP_UP_HEADER, stepUpId);
|
|
328
|
+
return this.fetchFn(url, { ...init, headers, credentials: "same-origin", mode: "same-origin" });
|
|
329
|
+
}
|
|
184
330
|
/**
|
|
185
331
|
* Signs the person out: navigates to the `signout_path` `me()` answers,
|
|
186
|
-
* tera's own
|
|
187
|
-
* is not
|
|
332
|
+
* tera's own sign-out, resolved against the app's base. A `signout_path`
|
|
333
|
+
* that is not a path relative to the base — a root-absolute path, or one
|
|
334
|
+
* naming another origin — is refused rather than followed.
|
|
188
335
|
*/
|
|
189
336
|
async signOut() {
|
|
190
337
|
const { signout_path: path } = await this.me();
|
|
191
|
-
if (typeof path !== "string" ||
|
|
192
|
-
throw new KitRequestError(200, null, `${
|
|
338
|
+
if (typeof path !== "string" || path === "" || path.startsWith("/") || /^[^/?#]*:/.test(path) || /[\\]/.test(path)) {
|
|
339
|
+
throw new KitRequestError(200, null, `${this.route("me")} answered a signout_path that is not a path relative to the app's base: ${JSON.stringify(path)}`);
|
|
193
340
|
}
|
|
194
|
-
this.navigate(path);
|
|
341
|
+
this.navigate(underBase(this.base, path));
|
|
342
|
+
}
|
|
343
|
+
/** A kit route's path under the app's base. */
|
|
344
|
+
route(...segments) {
|
|
345
|
+
return underBase(this.base, routePath(...segments));
|
|
195
346
|
}
|
|
196
|
-
async json(url, init) {
|
|
347
|
+
async json(url, init, stepUp = true) {
|
|
348
|
+
try {
|
|
349
|
+
return await this.jsonOnce(url, init, null);
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
const challenge = stepUp && err instanceof KitRequestError ? stepUpChallengeOf(err.code, err.envelope?.step_up) : null;
|
|
353
|
+
if (challenge === null)
|
|
354
|
+
throw err;
|
|
355
|
+
return this.jsonOnce(url, init, await this.confirmStepUp(challenge));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async jsonOnce(url, init, stepUpId) {
|
|
197
359
|
const response = await this.fetchFn(url, {
|
|
198
360
|
...init,
|
|
199
|
-
headers: {
|
|
361
|
+
headers: {
|
|
362
|
+
Accept: JSON_TYPE,
|
|
363
|
+
...init.headers,
|
|
364
|
+
...(stepUpId === null ? {} : { [STEP_UP_HEADER]: stepUpId }),
|
|
365
|
+
},
|
|
200
366
|
credentials: "same-origin",
|
|
201
367
|
mode: "same-origin",
|
|
202
368
|
cache: "no-store",
|
|
@@ -233,6 +399,27 @@ function answer(url, status, contentType, text) {
|
|
|
233
399
|
}
|
|
234
400
|
throw new KitRequestError(status, envelopeOf(body), `${url} answered HTTP ${status} (${contentType || "no content type"})`);
|
|
235
401
|
}
|
|
402
|
+
/** Whether `params` can carry `step_up` beside what it holds: absent, or a plain object. */
|
|
403
|
+
function isParamsObject(params) {
|
|
404
|
+
return params === undefined || params === null || (typeof params === "object" && !Array.isArray(params));
|
|
405
|
+
}
|
|
406
|
+
/** The step-up a response to the plugin's own route needs confirmed, read from a copy of it. */
|
|
407
|
+
async function stepUpRefusalOf(response) {
|
|
408
|
+
if (response.status !== 403)
|
|
409
|
+
return null;
|
|
410
|
+
const type = (response.headers.get("content-type") ?? "").toLowerCase().split(";")[0].trim();
|
|
411
|
+
if (type !== JSON_TYPE)
|
|
412
|
+
return null;
|
|
413
|
+
let body;
|
|
414
|
+
try {
|
|
415
|
+
body = JSON.parse(await response.clone().text());
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
const e = envelopeOf(body);
|
|
421
|
+
return e === null ? null : stepUpChallengeOf(e.code, e.step_up);
|
|
422
|
+
}
|
|
236
423
|
/** The envelope's `error` member, when `body` is a kit envelope; null otherwise. */
|
|
237
424
|
function envelopeOf(body) {
|
|
238
425
|
if (typeof body !== "object" || body === null)
|
|
@@ -79,6 +79,8 @@ export declare function defaultOrderKey(record: FeedFrameRecord): number | undef
|
|
|
79
79
|
export interface FeedConnectionOptions {
|
|
80
80
|
/** The feed's name, as the plugin's manifest declares it. */
|
|
81
81
|
feed: string;
|
|
82
|
+
/** The path the app is served under on its origin, as a client's `base` option names it; `/` when absent. */
|
|
83
|
+
base?: string;
|
|
82
84
|
/** Narrows the feed to these of its topic filters (the feed's `topics` parameter). */
|
|
83
85
|
topics?: readonly string[];
|
|
84
86
|
/** Each record not already delivered, in order. */
|
package/dist/feed/connection.js
CHANGED
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
* It is a plain class with an injected clock, opener and signals, so its tests
|
|
64
64
|
* drive it with fake timers and a scripted stream and no network.
|
|
65
65
|
*/
|
|
66
|
-
import { routePath } from "../routes.js";
|
|
66
|
+
import { routePath, underBase, ROOT_BASE } from "../routes.js";
|
|
67
67
|
import { fetchOpener, isRefusal } from "./sse.js";
|
|
68
68
|
export const BASE_DELAY_MS = 1_000;
|
|
69
69
|
export const MAX_DELAY_MS = 30_000;
|
|
@@ -164,7 +164,7 @@ export class FeedConnection {
|
|
|
164
164
|
for (const topic of this.options.topics ?? [])
|
|
165
165
|
query.append("topics", topic);
|
|
166
166
|
query.set("backfill", "all");
|
|
167
|
-
return routePath("feeds", this.options.feed) + "?" + query.toString();
|
|
167
|
+
return underBase(this.options.base ?? ROOT_BASE, routePath("feeds", this.options.feed)) + "?" + query.toString();
|
|
168
168
|
}
|
|
169
169
|
/** Opens the first stream. Nothing delivered before is remembered. */
|
|
170
170
|
start() {
|
package/dist/generated/kit.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { FeedFrameRecord, FeedFrameStatus } from "./feed.ts";
|
|
|
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";
|
|
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";
|
|
10
10
|
/**
|
|
11
11
|
* Kit error envelope.
|
|
12
12
|
*
|
|
@@ -27,6 +27,14 @@ export type KitError = {
|
|
|
27
27
|
retry_after_ms?: number;
|
|
28
28
|
/** Structured detail the refusing verb attached, whose shape the verb decides. The kit's own refusals attach none. */
|
|
29
29
|
data?: unknown;
|
|
30
|
+
/** The step-up a step_up_required, step_up_pending or step_up_mismatch refusal of the kit's is about. On a step-up just begun it carries the id, the page (url) the person confirms it on, its expiry and the methods the act accepts: open the page, poll the step-up route until it is satisfied, declined or expired, and send the identical request again with the Tera-Step-Up header set to the id. */
|
|
31
|
+
step_up?: {
|
|
32
|
+
id: string;
|
|
33
|
+
url?: string;
|
|
34
|
+
methods?: string[];
|
|
35
|
+
expires_at?: string;
|
|
36
|
+
state?: string;
|
|
37
|
+
};
|
|
30
38
|
};
|
|
31
39
|
};
|
|
32
40
|
/**
|
|
@@ -75,8 +83,37 @@ export type KitInvokeResponse = {
|
|
|
75
83
|
export type KitMeResponse = {
|
|
76
84
|
/** The signed-in person, as tera's caller assertion names them: spelled as a Work Order that person filed would name them. */
|
|
77
85
|
subject: string;
|
|
78
|
-
/** Where to send the person to sign out: tera's own path, which
|
|
86
|
+
/** Where to send the person to sign out: tera's own sign-out, as a path relative to the app's base, which a page resolves against that base (/ on the plugin's own origin, /plugins/<name>/ on a path-based installation). It is never root-absolute. */
|
|
79
87
|
signout_path: string;
|
|
88
|
+
/** What to call the signed-in person, as tera's record of them holds it; absent when it holds none. Presentation only: subject is the identity, and nothing is decided by this. */
|
|
89
|
+
display_name?: string;
|
|
90
|
+
/** The address tera's record of the signed-in person holds; absent when it holds none. Presentation only: subject is the identity, and nothing is decided by this. */
|
|
91
|
+
email?: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* GET /tera/v1/step-ups/{id} response.
|
|
95
|
+
*
|
|
96
|
+
* Where one of the person's own step-ups is in its life (ADR-173 D11).
|
|
97
|
+
*
|
|
98
|
+
* Schema: sdk/plugin/webui/schema/v1/step-up.response.json
|
|
99
|
+
*/
|
|
100
|
+
export type KitStepUpResponse = {
|
|
101
|
+
/** The step-up's id. */
|
|
102
|
+
id: string;
|
|
103
|
+
/** Where the step-up is in its life: pending until the person confirms or declines it, satisfied once confirmed, spent once the act used it, declined, or expired. A reader polls until it is satisfied, declined or expired. */
|
|
104
|
+
state: "pending" | "satisfied" | "spent" | "declined" | "expired";
|
|
105
|
+
/** tera's own text for the act the step-up confirms. */
|
|
106
|
+
display: string;
|
|
107
|
+
/** The methods the act accepts: passkey, and email_code when the act accepts it. */
|
|
108
|
+
methods: string[] | null;
|
|
109
|
+
/** When the step-up stops waiting, in RFC 3339. */
|
|
110
|
+
expires_at: string;
|
|
111
|
+
/** What satisfied it, passkey or email_code; absent until it is satisfied. */
|
|
112
|
+
method?: string;
|
|
113
|
+
/** For a passkey, the kind of sign-in that registered it; a passkey anchored on email_code is worth an email code. Absent otherwise. */
|
|
114
|
+
anchor?: string;
|
|
115
|
+
/** When the person confirmed it, in RFC 3339; absent until then. */
|
|
116
|
+
satisfied_at?: string;
|
|
80
117
|
};
|
|
81
118
|
/**
|
|
82
119
|
* POST /tera/v1/uploads response.
|
|
@@ -111,16 +148,17 @@ export declare const KIT_SCHEMAS: {
|
|
|
111
148
|
readonly KitFeedEventReauthenticate: "sdk/plugin/webui/schema/v1/feed.event.reauthenticate.json";
|
|
112
149
|
readonly KitInvokeResponse: "sdk/plugin/webui/schema/v1/invoke.response.json";
|
|
113
150
|
readonly KitMeResponse: "sdk/plugin/webui/schema/v1/me.response.json";
|
|
151
|
+
readonly KitStepUpResponse: "sdk/plugin/webui/schema/v1/step-up.response.json";
|
|
114
152
|
readonly KitUploadResponse: "sdk/plugin/webui/schema/v1/upload.response.json";
|
|
115
153
|
};
|
|
116
|
-
/** The prefix every kit route is served under (ADR-162 D3). */
|
|
154
|
+
/** The prefix every kit route is served under (ADR-162 D3), below the app's base. */
|
|
117
155
|
export declare const KIT_ROUTE_PREFIX = "/tera/v1/";
|
|
118
|
-
/** tera's own sign-out path, which `GET /tera/v1/me` answers as signout_path. */
|
|
119
|
-
export declare const KIT_SIGNOUT_PATH = "
|
|
156
|
+
/** tera's own sign-out path, which `GET /tera/v1/me` answers as signout_path: relative, resolved against the app's base. */
|
|
157
|
+
export declare const KIT_SIGNOUT_PATH = "_tera/signout";
|
|
120
158
|
/** The kit's route table (ADR-162 D3), as sdk/plugin/webui declares it. */
|
|
121
159
|
export declare const KIT_ROUTE_TABLE: {
|
|
122
160
|
readonly prefix: "/tera/v1/";
|
|
123
|
-
readonly signout_path: "
|
|
161
|
+
readonly signout_path: "_tera/signout";
|
|
124
162
|
readonly same_origin: "A route marked same_origin refuses a request unless it is same-origin: when the request carries Sec-Fetch-Site, that header must be same-origin; when it does not, its Origin must equal the request's own (scheme http or https, and the Host the request was sent to). The refusal is 403 cross_origin.";
|
|
125
163
|
readonly public: "None. Every route under the prefix refuses a request with no verified caller, 401 unauthenticated, before anything else is decided. Only the app's assets, outside the prefix, are served to an anonymous request.";
|
|
126
164
|
readonly error: {
|
|
@@ -223,6 +261,22 @@ export declare const KIT_ROUTE_TABLE: {
|
|
|
223
261
|
readonly schema: "https://kottera.dev/schemas/plugin/webui/upload.response/v1";
|
|
224
262
|
readonly description: "The staged artifact.";
|
|
225
263
|
};
|
|
264
|
+
}, {
|
|
265
|
+
readonly name: "step_up";
|
|
266
|
+
readonly method: "GET";
|
|
267
|
+
readonly path: "/tera/v1/step-ups/{id}";
|
|
268
|
+
readonly summary: "Read one of the person's own step-ups as the person (ADR-173 D11): what a page polls, after opening the step-up's url, until it is satisfied, declined or expired. A step-up that is not the person's is tera's 404, the same as an id no step-up carries.";
|
|
269
|
+
readonly same_origin: false;
|
|
270
|
+
readonly path_params: readonly [{
|
|
271
|
+
readonly name: "id";
|
|
272
|
+
readonly description: "The step-up's id, as a step_up_required refusal's step_up names it.";
|
|
273
|
+
readonly required: true;
|
|
274
|
+
}];
|
|
275
|
+
readonly response: {
|
|
276
|
+
readonly media_type: "application/json";
|
|
277
|
+
readonly schema: "https://kottera.dev/schemas/plugin/webui/step-up.response/v1";
|
|
278
|
+
readonly description: "The step-up's state.";
|
|
279
|
+
};
|
|
226
280
|
}];
|
|
227
281
|
};
|
|
228
282
|
/** The name of a route in KIT_ROUTE_TABLE. */
|
|
@@ -235,6 +289,7 @@ export type KitRouteResponses = {
|
|
|
235
289
|
me: KitMeResponse;
|
|
236
290
|
invoke: KitInvokeResponse;
|
|
237
291
|
upload: KitUploadResponse;
|
|
292
|
+
step_up: KitStepUpResponse;
|
|
238
293
|
};
|
|
239
294
|
/** The data of each server-sent event a feed stream carries, by event name. */
|
|
240
295
|
export type KitFeedEvents = {
|
package/dist/generated/kit.js
CHANGED
|
@@ -10,16 +10,17 @@ export const KIT_SCHEMAS = {
|
|
|
10
10
|
KitFeedEventReauthenticate: "sdk/plugin/webui/schema/v1/feed.event.reauthenticate.json",
|
|
11
11
|
KitInvokeResponse: "sdk/plugin/webui/schema/v1/invoke.response.json",
|
|
12
12
|
KitMeResponse: "sdk/plugin/webui/schema/v1/me.response.json",
|
|
13
|
+
KitStepUpResponse: "sdk/plugin/webui/schema/v1/step-up.response.json",
|
|
13
14
|
KitUploadResponse: "sdk/plugin/webui/schema/v1/upload.response.json",
|
|
14
15
|
};
|
|
15
|
-
/** The prefix every kit route is served under (ADR-162 D3). */
|
|
16
|
+
/** The prefix every kit route is served under (ADR-162 D3), below the app's base. */
|
|
16
17
|
export const KIT_ROUTE_PREFIX = "/tera/v1/";
|
|
17
|
-
/** tera's own sign-out path, which `GET /tera/v1/me` answers as signout_path. */
|
|
18
|
-
export const KIT_SIGNOUT_PATH = "
|
|
18
|
+
/** tera's own sign-out path, which `GET /tera/v1/me` answers as signout_path: relative, resolved against the app's base. */
|
|
19
|
+
export const KIT_SIGNOUT_PATH = "_tera/signout";
|
|
19
20
|
/** The kit's route table (ADR-162 D3), as sdk/plugin/webui declares it. */
|
|
20
21
|
export const KIT_ROUTE_TABLE = {
|
|
21
22
|
prefix: "/tera/v1/",
|
|
22
|
-
signout_path: "
|
|
23
|
+
signout_path: "_tera/signout",
|
|
23
24
|
same_origin: "A route marked same_origin refuses a request unless it is same-origin: when the request carries Sec-Fetch-Site, that header must be same-origin; when it does not, its Origin must equal the request's own (scheme http or https, and the Host the request was sent to). The refusal is 403 cross_origin.",
|
|
24
25
|
public: "None. Every route under the prefix refuses a request with no verified caller, 401 unauthenticated, before anything else is decided. Only the app's assets, outside the prefix, are served to an anonymous request.",
|
|
25
26
|
error: {
|
|
@@ -144,5 +145,24 @@ export const KIT_ROUTE_TABLE = {
|
|
|
144
145
|
description: "The staged artifact.",
|
|
145
146
|
},
|
|
146
147
|
},
|
|
148
|
+
{
|
|
149
|
+
name: "step_up",
|
|
150
|
+
method: "GET",
|
|
151
|
+
path: "/tera/v1/step-ups/{id}",
|
|
152
|
+
summary: "Read one of the person's own step-ups as the person (ADR-173 D11): what a page polls, after opening the step-up's url, until it is satisfied, declined or expired. A step-up that is not the person's is tera's 404, the same as an id no step-up carries.",
|
|
153
|
+
same_origin: false,
|
|
154
|
+
path_params: [
|
|
155
|
+
{
|
|
156
|
+
name: "id",
|
|
157
|
+
description: "The step-up's id, as a step_up_required refusal's step_up names it.",
|
|
158
|
+
required: true,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
response: {
|
|
162
|
+
media_type: "application/json",
|
|
163
|
+
schema: "https://kottera.dev/schemas/plugin/webui/step-up.response/v1",
|
|
164
|
+
description: "The step-up's state.",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
147
167
|
],
|
|
148
168
|
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ended share.
|
|
3
|
+
*
|
|
4
|
+
* The result of api.v1.shares.delete and api.v1.shares.leave: what the act ended. An ended share decides nothing and is not listed.
|
|
5
|
+
*
|
|
6
|
+
* Schema: sdk/shares/schema/v1/ended.json
|
|
7
|
+
*/
|
|
8
|
+
export type SharesEnded = {
|
|
9
|
+
/** The ended share's id. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** The installation whose resource was shared, <vault>/plugins/<installation>. */
|
|
12
|
+
installation: string;
|
|
13
|
+
/** The resource that was shared. */
|
|
14
|
+
resource: string;
|
|
15
|
+
/** The grantee person's id. */
|
|
16
|
+
grantee: string;
|
|
17
|
+
/** The sharer person's id. */
|
|
18
|
+
sharer: string;
|
|
19
|
+
/** How the share ended, as its record holds it: revoked or left. */
|
|
20
|
+
ended: "revoked" | "left";
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Extend a share.
|
|
24
|
+
*
|
|
25
|
+
* The params of api.v1.shares.patch, PATCH /api/v1/shares/{id} (ADR-174 D11).
|
|
26
|
+
*
|
|
27
|
+
* Schema: sdk/shares/schema/v1/extend.request.json
|
|
28
|
+
*/
|
|
29
|
+
export type SharesExtendRequest = {
|
|
30
|
+
/** The share's id, a UUIDv7. */
|
|
31
|
+
id: string;
|
|
32
|
+
/** The new expiry: later than the share's, at most the installation's max_expiry from now. Absent takes the kind's default_expiry from now, clamped to max_expiry. */
|
|
33
|
+
expires_at?: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Name one share.
|
|
37
|
+
*
|
|
38
|
+
* The params of api.v1.shares.get, api.v1.shares.delete and api.v1.shares.leave: the {id} of /api/v1/shares/{id}.
|
|
39
|
+
*
|
|
40
|
+
* Schema: sdk/shares/schema/v1/id.request.json
|
|
41
|
+
*/
|
|
42
|
+
export type SharesIdRequest = {
|
|
43
|
+
/** The share's id, a UUIDv7. */
|
|
44
|
+
id: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A list of shares.
|
|
48
|
+
*
|
|
49
|
+
* The result of api.v1.shares.list.
|
|
50
|
+
*
|
|
51
|
+
* Schema: sdk/shares/schema/v1/list.json
|
|
52
|
+
*/
|
|
53
|
+
export type SharesList = {
|
|
54
|
+
/** The shares, each with whether it decides now. */
|
|
55
|
+
shares: {
|
|
56
|
+
/** The share's id, a UUIDv7. */
|
|
57
|
+
id: string;
|
|
58
|
+
/** The installation whose resource is shared, <vault>/plugins/<installation>. */
|
|
59
|
+
installation: string;
|
|
60
|
+
/** The resource shared, plugins/<vault>/<installation>/<kind>/<id>. */
|
|
61
|
+
resource: string;
|
|
62
|
+
/** The shared verbs. */
|
|
63
|
+
verbs: string[] | null;
|
|
64
|
+
/** What the grantee is: person. */
|
|
65
|
+
grantee_kind: string;
|
|
66
|
+
/** The grantee person's id. */
|
|
67
|
+
grantee: string;
|
|
68
|
+
/** The id of the person who made the share. */
|
|
69
|
+
sharer: string;
|
|
70
|
+
/** The plugin principal that made the share for its sharer; absent when the sharer made it directly. */
|
|
71
|
+
created_via?: string;
|
|
72
|
+
/** When the share was made. */
|
|
73
|
+
created_at: string;
|
|
74
|
+
/** When the share stops deciding. */
|
|
75
|
+
expires_at: string;
|
|
76
|
+
/** When the share was last extended; absent if never. */
|
|
77
|
+
extended_at?: string;
|
|
78
|
+
/** Whether the share decides at least one of its verbs on the grantee's next request, computed from what decides now. */
|
|
79
|
+
deciding: boolean;
|
|
80
|
+
/** When deciding is false, the one reason why. Absent while the share decides. */
|
|
81
|
+
not_deciding?: "sharer_lost_access" | "sharing_disabled" | "installation_not_standing" | "verb_no_longer_shareable" | "pending_invitation";
|
|
82
|
+
}[] | null;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* List shares.
|
|
86
|
+
*
|
|
87
|
+
* The params of api.v1.shares.list, the query of GET /api/v1/shares. At most one member is named; with none, the caller's own live shares as grantee and as sharer.
|
|
88
|
+
*
|
|
89
|
+
* Schema: sdk/shares/schema/v1/list.request.json
|
|
90
|
+
*/
|
|
91
|
+
export type SharesListRequest = {
|
|
92
|
+
/** An installation, <vault>/plugins/<name>: every share on it. Needs the shares admin grant (api/v1/shares/admin). */
|
|
93
|
+
installation?: string;
|
|
94
|
+
/** One resource, plugins/<vault>/<installation>/<kind>/<id>: every share on it when the caller's own grants include its share-conferring verb, else the caller's own shares on it. */
|
|
95
|
+
resource?: string;
|
|
96
|
+
/** A grantee's person id; someone else's needs the shares admin grant. */
|
|
97
|
+
grantee?: string;
|
|
98
|
+
/** A sharer's person id; someone else's needs the shares admin grant. */
|
|
99
|
+
sharer?: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Make a share.
|
|
103
|
+
*
|
|
104
|
+
* The params of api.v1.shares.create, the body of POST /api/v1/shares (ADR-174 D6).
|
|
105
|
+
*
|
|
106
|
+
* Schema: sdk/shares/schema/v1/make.request.json
|
|
107
|
+
*/
|
|
108
|
+
export type SharesMakeRequest = {
|
|
109
|
+
/** The one resource shared, plugins/<vault>/<installation>/<kind>/<id>. */
|
|
110
|
+
resource: string;
|
|
111
|
+
/** The shared verbs: a non-empty subset of the kind's sharing { verbs }, never its share-conferring verb. */
|
|
112
|
+
verbs: string[];
|
|
113
|
+
/** The grantee person's id. Exactly one of grantee and grantee_address names the grantee. */
|
|
114
|
+
grantee?: string;
|
|
115
|
+
/** An email address the directory resolves to the grantee. An address no person holds is invited, and the share is pending until they sign in, on a kind whose effective grantee class is addresses; otherwise it is refused share_grantee_not_admitted. */
|
|
116
|
+
grantee_address?: string;
|
|
117
|
+
/** The share's expiry, at most the installation's max_expiry from now. Absent takes the kind's default_expiry, clamped to max_expiry. */
|
|
118
|
+
expires_at?: string;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The vocabulary of the share acts' refusal codes (sdk/shares.Codes).
|
|
122
|
+
*
|
|
123
|
+
* Every stable code a share act is refused with. A code is never renamed; a new refusal is a new code, so a reader keeps a code it does not know as it arrived. share_not_found answers a share the caller may not see exactly as an id no share has. share_grantee_not_admitted answers an address no person holds on a kind whose effective grantee class is not addresses; on one that is, the address is invited and the share is pending until the invitee signs in.
|
|
124
|
+
*
|
|
125
|
+
* Schema: sdk/shares/schema/v1/refusal-code.json
|
|
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";
|
|
128
|
+
/**
|
|
129
|
+
* A share act refused.
|
|
130
|
+
*
|
|
131
|
+
* A share act tera refused, as the shares client answers it: the route's stable code and its words, unchanged, and the detail the code declares.
|
|
132
|
+
*
|
|
133
|
+
* Schema: sdk/shares/schema/v1/refusal.json
|
|
134
|
+
*/
|
|
135
|
+
export type SharesRefusal = {
|
|
136
|
+
/** The route's stable code. A code a reader does not know is kept as it arrived. */
|
|
137
|
+
code: string;
|
|
138
|
+
/** The route's words for a person, unchanged. */
|
|
139
|
+
message: string;
|
|
140
|
+
/** On share_verb_not_held, share_verb_not_shareable and share_verb_confers_sharing, the verb refused. */
|
|
141
|
+
verb?: string;
|
|
142
|
+
/** On share_exists, the id of the share already there. */
|
|
143
|
+
existing?: string;
|
|
144
|
+
/** On share_invitation_refused, why the address was not invited (inviter_limited, email_unavailable, ...). */
|
|
145
|
+
invitation?: string;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* A share.
|
|
149
|
+
*
|
|
150
|
+
* One live share, with whether it decides now: the result of api.v1.shares.create, api.v1.shares.get and api.v1.shares.patch, and each member of a list (ADR-174 D14).
|
|
151
|
+
*
|
|
152
|
+
* Schema: sdk/shares/schema/v1/share.json
|
|
153
|
+
*/
|
|
154
|
+
export type SharesShare = {
|
|
155
|
+
/** The share's id, a UUIDv7. */
|
|
156
|
+
id: string;
|
|
157
|
+
/** The installation whose resource is shared, <vault>/plugins/<installation>. */
|
|
158
|
+
installation: string;
|
|
159
|
+
/** The resource shared, plugins/<vault>/<installation>/<kind>/<id>. */
|
|
160
|
+
resource: string;
|
|
161
|
+
/** The shared verbs. */
|
|
162
|
+
verbs: string[] | null;
|
|
163
|
+
/** What the grantee is: person. */
|
|
164
|
+
grantee_kind: string;
|
|
165
|
+
/** The grantee person's id. */
|
|
166
|
+
grantee: string;
|
|
167
|
+
/** The id of the person who made the share. */
|
|
168
|
+
sharer: string;
|
|
169
|
+
/** The plugin principal that made the share for its sharer; absent when the sharer made it directly. */
|
|
170
|
+
created_via?: string;
|
|
171
|
+
/** When the share was made. */
|
|
172
|
+
created_at: string;
|
|
173
|
+
/** When the share stops deciding. */
|
|
174
|
+
expires_at: string;
|
|
175
|
+
/** When the share was last extended; absent if never. */
|
|
176
|
+
extended_at?: string;
|
|
177
|
+
/** Whether the share decides at least one of its verbs on the grantee's next request, computed from what decides now. */
|
|
178
|
+
deciding: boolean;
|
|
179
|
+
/** When deciding is false, the one reason why. Absent while the share decides. */
|
|
180
|
+
not_deciding?: "sharer_lost_access" | "sharing_disabled" | "installation_not_standing" | "verb_no_longer_shareable" | "pending_invitation";
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Every type this module declares, mapped to the repository path of the schema
|
|
184
|
+
* it was generated from.
|
|
185
|
+
*/
|
|
186
|
+
export declare const SHARES_SCHEMAS: {
|
|
187
|
+
readonly SharesEnded: "sdk/shares/schema/v1/ended.json";
|
|
188
|
+
readonly SharesExtendRequest: "sdk/shares/schema/v1/extend.request.json";
|
|
189
|
+
readonly SharesIdRequest: "sdk/shares/schema/v1/id.request.json";
|
|
190
|
+
readonly SharesList: "sdk/shares/schema/v1/list.json";
|
|
191
|
+
readonly SharesListRequest: "sdk/shares/schema/v1/list.request.json";
|
|
192
|
+
readonly SharesMakeRequest: "sdk/shares/schema/v1/make.request.json";
|
|
193
|
+
readonly SharesRefusalCode: "sdk/shares/schema/v1/refusal-code.json";
|
|
194
|
+
readonly SharesRefusal: "sdk/shares/schema/v1/refusal.json";
|
|
195
|
+
readonly SharesShare: "sdk/shares/schema/v1/share.json";
|
|
196
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Code generated by cmd/internal/genwebtypes from sdk/shares/schema/v1/; DO NOT EDIT.
|
|
2
|
+
// Regenerate with `make web-types` (ADR-162 D4 item 1).
|
|
3
|
+
/**
|
|
4
|
+
* Every type this module declares, mapped to the repository path of the schema
|
|
5
|
+
* it was generated from.
|
|
6
|
+
*/
|
|
7
|
+
export const SHARES_SCHEMAS = {
|
|
8
|
+
SharesEnded: "sdk/shares/schema/v1/ended.json",
|
|
9
|
+
SharesExtendRequest: "sdk/shares/schema/v1/extend.request.json",
|
|
10
|
+
SharesIdRequest: "sdk/shares/schema/v1/id.request.json",
|
|
11
|
+
SharesList: "sdk/shares/schema/v1/list.json",
|
|
12
|
+
SharesListRequest: "sdk/shares/schema/v1/list.request.json",
|
|
13
|
+
SharesMakeRequest: "sdk/shares/schema/v1/make.request.json",
|
|
14
|
+
SharesRefusalCode: "sdk/shares/schema/v1/refusal-code.json",
|
|
15
|
+
SharesRefusal: "sdk/shares/schema/v1/refusal.json",
|
|
16
|
+
SharesShare: "sdk/shares/schema/v1/share.json",
|
|
17
|
+
};
|
|
@@ -48,6 +48,8 @@ export type WorkOrderAnswerParams = {
|
|
|
48
48
|
replace?: boolean;
|
|
49
49
|
/** Cross ONE refusal: a select site where this Work Order's live step binding is running a candidate the new pin no longer offers (ADR-130 D5, ratified crossable 2026-09-20). The binding row is RETAINED at the same chain generation and the resume RE-SELECTS from the new candidate set, running the candidate loop against live capacity exactly as a first admission would. It does NOT preserve the candidate the run is on — that candidate is gone, which is why the refusal exists. It lifts nothing else: a capability or operation switch at a fixed site has no candidate to re-select, and a withdrawn `yields_turn` offer is frozen for the life of a pinned conversation (ADR-129 D9). Over a site whose candidate IS still offered, or a fixed site, it is a no-op and says so in the manifest rather than refusing. Only meaningful with `repin`. Authorized against `api/v1/workorders/repin/reselect` IN ADDITION to `api/v1/workorders/repin`. Pass `dry_run` first: the manifest's `would_reselect` names the site, the candidate being given up and the set the resume will choose from. */
|
|
50
50
|
reselect?: boolean;
|
|
51
|
+
/** The id of the step-up a person confirmed this act with, when the Work Order's type requires one for it (ADR-173 D10): a review verdict, an answer, or an off-tera `complete` / `failed`. Call WITHOUT it first: the call is refused `step_up_required` and its error data names the step-up's `id` and the `url` the person opens to confirm it — show them that url; you cannot confirm it yourself. Then retry the IDENTICAL call with step_up = that id. `step_up_pending` means they have not confirmed yet: wait and retry the same call. A step-up confirms exactly one act with exactly this content, once. */
|
|
52
|
+
step_up?: string;
|
|
51
53
|
/** The operator's exact prose. Interactive answers become one durable ledger message; other answers are wrapped as {"answer": "<text>"}. Ignored when `payload` is supplied. */
|
|
52
54
|
text?: string;
|
|
53
55
|
};
|
|
@@ -965,6 +967,13 @@ export type WorkOrderGetResult = {
|
|
|
965
967
|
authorized_for?: string;
|
|
966
968
|
plugin?: string;
|
|
967
969
|
evidence_class?: string;
|
|
970
|
+
step_up?: {
|
|
971
|
+
id: string;
|
|
972
|
+
method: string;
|
|
973
|
+
anchor?: string;
|
|
974
|
+
at: string;
|
|
975
|
+
};
|
|
976
|
+
display_name?: string;
|
|
968
977
|
};
|
|
969
978
|
};
|
|
970
979
|
question?: {
|
|
@@ -1217,10 +1226,22 @@ export type WorkOrderGetResult = {
|
|
|
1217
1226
|
describe?: string;
|
|
1218
1227
|
caller_is_person?: boolean;
|
|
1219
1228
|
outcome_provenance?: string;
|
|
1229
|
+
step_up?: {
|
|
1230
|
+
id: string;
|
|
1231
|
+
method: string;
|
|
1232
|
+
anchor?: string;
|
|
1233
|
+
at: string;
|
|
1234
|
+
};
|
|
1220
1235
|
};
|
|
1221
1236
|
outcome?: {
|
|
1222
1237
|
outcome_provenance: string;
|
|
1223
1238
|
attested_by?: string;
|
|
1239
|
+
step_up?: {
|
|
1240
|
+
id: string;
|
|
1241
|
+
method: string;
|
|
1242
|
+
anchor?: string;
|
|
1243
|
+
at: string;
|
|
1244
|
+
};
|
|
1224
1245
|
};
|
|
1225
1246
|
relations: {
|
|
1226
1247
|
relation: string;
|
|
@@ -1785,6 +1806,12 @@ export type WorkOrderListResult = {
|
|
|
1785
1806
|
describe?: string;
|
|
1786
1807
|
caller_is_person?: boolean;
|
|
1787
1808
|
outcome_provenance?: string;
|
|
1809
|
+
step_up?: {
|
|
1810
|
+
id: string;
|
|
1811
|
+
method: string;
|
|
1812
|
+
anchor?: string;
|
|
1813
|
+
at: string;
|
|
1814
|
+
};
|
|
1788
1815
|
};
|
|
1789
1816
|
}[] | null;
|
|
1790
1817
|
};
|
|
@@ -1818,6 +1845,12 @@ export type WorkOrderListOffTeraResult = {
|
|
|
1818
1845
|
describe?: string;
|
|
1819
1846
|
caller_is_person?: boolean;
|
|
1820
1847
|
outcome_provenance?: string;
|
|
1848
|
+
step_up?: {
|
|
1849
|
+
id: string;
|
|
1850
|
+
method: string;
|
|
1851
|
+
anchor?: string;
|
|
1852
|
+
at: string;
|
|
1853
|
+
};
|
|
1821
1854
|
};
|
|
1822
1855
|
take_age_seconds?: number;
|
|
1823
1856
|
/** On a held entry: the JSON Schema this Work Order was pinned to at its first take, which submit_result grades the record against. Absent on posted and taken entries, and when the type declares none. */
|
|
@@ -1840,6 +1873,12 @@ export type WorkOrderListOffTeraResult = {
|
|
|
1840
1873
|
describe?: string;
|
|
1841
1874
|
caller_is_person?: boolean;
|
|
1842
1875
|
outcome_provenance?: string;
|
|
1876
|
+
step_up?: {
|
|
1877
|
+
id: string;
|
|
1878
|
+
method: string;
|
|
1879
|
+
anchor?: string;
|
|
1880
|
+
at: string;
|
|
1881
|
+
};
|
|
1843
1882
|
};
|
|
1844
1883
|
take_age_seconds?: number;
|
|
1845
1884
|
/** On a held entry: the JSON Schema this Work Order was pinned to at its first take, which submit_result grades the record against. Absent on posted and taken entries, and when the type declares none. */
|
|
@@ -1862,6 +1901,12 @@ export type WorkOrderListOffTeraResult = {
|
|
|
1862
1901
|
describe?: string;
|
|
1863
1902
|
caller_is_person?: boolean;
|
|
1864
1903
|
outcome_provenance?: string;
|
|
1904
|
+
step_up?: {
|
|
1905
|
+
id: string;
|
|
1906
|
+
method: string;
|
|
1907
|
+
anchor?: string;
|
|
1908
|
+
at: string;
|
|
1909
|
+
};
|
|
1865
1910
|
};
|
|
1866
1911
|
take_age_seconds?: number;
|
|
1867
1912
|
/** On a held entry: the JSON Schema this Work Order was pinned to at its first take, which submit_result grades the record against. Absent on posted and taken entries, and when the type declares none. */
|
|
@@ -1962,6 +2007,12 @@ export type WorkOrderProgressResult = {
|
|
|
1962
2007
|
describe?: string;
|
|
1963
2008
|
caller_is_person?: boolean;
|
|
1964
2009
|
outcome_provenance?: string;
|
|
2010
|
+
step_up?: {
|
|
2011
|
+
id: string;
|
|
2012
|
+
method: string;
|
|
2013
|
+
anchor?: string;
|
|
2014
|
+
at: string;
|
|
2015
|
+
};
|
|
1965
2016
|
} | null;
|
|
1966
2017
|
addressed_events: {
|
|
1967
2018
|
id: number;
|
|
@@ -2550,6 +2601,8 @@ export type WorkOrderSubmitResultParams = {
|
|
|
2550
2601
|
};
|
|
2551
2602
|
/** The id of the last event you have processed on this Work Order. The response carries the events addressed to you after it (a repost, a cancel, a wake, an answer, posted guidance), at most 32 with `more` set when there were more, plus `newest_event_id`. The server keeps no cursor: omit it, or pass 0, for everything addressed so far. */
|
|
2552
2603
|
seen_through?: number;
|
|
2604
|
+
/** The id of the step-up a person confirmed this act with, when the Work Order's type requires one for it (ADR-173 D10): a review verdict, an answer, or an off-tera `complete` / `failed`. Call WITHOUT it first: the call is refused `step_up_required` and its error data names the step-up's `id` and the `url` the person opens to confirm it — show them that url; you cannot confirm it yourself. Then retry the IDENTICAL call with step_up = that id. `step_up_pending` means they have not confirmed yet: wait and retry the same call. A step-up confirms exactly one act with exactly this content, once. */
|
|
2605
|
+
step_up?: string;
|
|
2553
2606
|
};
|
|
2554
2607
|
/**
|
|
2555
2608
|
* `work_order.submit_result` result (v1).
|
|
@@ -2565,6 +2618,12 @@ export type WorkOrderSubmitResultResult = {
|
|
|
2565
2618
|
outcome?: {
|
|
2566
2619
|
outcome_provenance: string;
|
|
2567
2620
|
attested_by?: string;
|
|
2621
|
+
step_up?: {
|
|
2622
|
+
id: string;
|
|
2623
|
+
method: string;
|
|
2624
|
+
anchor?: string;
|
|
2625
|
+
at: string;
|
|
2626
|
+
};
|
|
2568
2627
|
};
|
|
2569
2628
|
checks?: string;
|
|
2570
2629
|
addressed_events: {
|
|
@@ -2612,6 +2671,12 @@ export type WorkOrderTakeResult = {
|
|
|
2612
2671
|
describe?: string;
|
|
2613
2672
|
caller_is_person?: boolean;
|
|
2614
2673
|
outcome_provenance?: string;
|
|
2674
|
+
step_up?: {
|
|
2675
|
+
id: string;
|
|
2676
|
+
method: string;
|
|
2677
|
+
anchor?: string;
|
|
2678
|
+
at: string;
|
|
2679
|
+
};
|
|
2615
2680
|
} | null;
|
|
2616
2681
|
instructions: string;
|
|
2617
2682
|
requirements?: string;
|
|
@@ -2675,6 +2740,8 @@ export type WorkOrderTransitionParams = {
|
|
|
2675
2740
|
replace?: boolean;
|
|
2676
2741
|
/** Cross ONE refusal: a select site where this Work Order's live step binding is running a candidate the new pin no longer offers (ADR-130 D5, ratified crossable 2026-09-20). The binding row is RETAINED at the same chain generation and the resume RE-SELECTS from the new candidate set, running the candidate loop against live capacity exactly as a first admission would. It does NOT preserve the candidate the run is on — that candidate is gone, which is why the refusal exists. It lifts nothing else: a capability or operation switch at a fixed site has no candidate to re-select, and a withdrawn `yields_turn` offer is frozen for the life of a pinned conversation (ADR-129 D9). Over a site whose candidate IS still offered, or a fixed site, it is a no-op and says so in the manifest rather than refusing. Only meaningful with `repin`. Authorized against `api/v1/workorders/repin/reselect` IN ADDITION to `api/v1/workorders/repin`. Pass `dry_run` first: the manifest's `would_reselect` names the site, the candidate being given up and the set the resume will choose from. */
|
|
2677
2742
|
reselect?: boolean;
|
|
2743
|
+
/** The id of the step-up a person confirmed this act with, when the Work Order's type requires one for it (ADR-173 D10): a review verdict, an answer, or an off-tera `complete` / `failed`. Call WITHOUT it first: the call is refused `step_up_required` and its error data names the step-up's `id` and the `url` the person opens to confirm it — show them that url; you cannot confirm it yourself. Then retry the IDENTICAL call with step_up = that id. `step_up_pending` means they have not confirmed yet: wait and retry the same call. A step-up confirms exactly one act with exactly this content, once. */
|
|
2744
|
+
step_up?: string;
|
|
2678
2745
|
/** action=answer only: the operator's answer text. Read work_order.get(id).question first — that is what is being asked. */
|
|
2679
2746
|
text?: string;
|
|
2680
2747
|
};
|
|
@@ -3102,6 +3169,12 @@ export type WorkOrderTreeResult = {
|
|
|
3102
3169
|
outcome?: {
|
|
3103
3170
|
outcome_provenance: string;
|
|
3104
3171
|
attested_by?: string;
|
|
3172
|
+
step_up?: {
|
|
3173
|
+
id: string;
|
|
3174
|
+
method: string;
|
|
3175
|
+
anchor?: string;
|
|
3176
|
+
at: string;
|
|
3177
|
+
};
|
|
3105
3178
|
};
|
|
3106
3179
|
replaced?: {
|
|
3107
3180
|
relation: string;
|
|
@@ -3326,6 +3399,12 @@ export type WorkOrderTreeResult = {
|
|
|
3326
3399
|
outcome?: {
|
|
3327
3400
|
outcome_provenance: string;
|
|
3328
3401
|
attested_by?: string;
|
|
3402
|
+
step_up?: {
|
|
3403
|
+
id: string;
|
|
3404
|
+
method: string;
|
|
3405
|
+
anchor?: string;
|
|
3406
|
+
at: string;
|
|
3407
|
+
};
|
|
3329
3408
|
};
|
|
3330
3409
|
replaced?: {
|
|
3331
3410
|
relation: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
export { ROUTE_PREFIX, routePath } from "./routes.ts";
|
|
1
|
+
export { ROOT_BASE, ROUTE_PREFIX, appBase, routePath, underBase } from "./routes.ts";
|
|
2
2
|
export { TeraClient, KitRequestError, UNRECOGNIZED_RESPONSE, type FeedOptions, type InvokeVerdict, type KitErrorBody, type TeraClientOptions, type UploadOptions, type UploadProgress, type UploadRequest, type VerbRefusal, } from "./client.ts";
|
|
3
|
+
export { STEP_UP_HEADER, STEP_UP_POLL_MS, STEP_UP_REQUIRED, STEP_UP_RESUME_PARAM, StepUpError, resumedStepUp, stepUpChallengeOf, stepUpPageURL, type StepUpChallenge, type StepUpEnd, type StepUpOptions, type StepUpWindow, } from "./stepup.ts";
|
|
3
4
|
export { BASE_DELAY_MS, MAX_DELAY_MS, REPLAY_LIMIT, REPLAY_WINDOW_MS, HEARTBEAT_GRACE_MS, REAUTHENTICATE_EVENT, FeedConnection, browserSignals, defaultOrderKey, type Clock, type FeedConnectionOptions, type FeedConnectionPhase, type FeedConnectionState, type FeedFrame, type FeedLoss, type FeedSignal, type FeedSignals, } from "./feed/connection.ts";
|
|
4
5
|
export { FeedOpenError, SSEParser, fetchOpener, isRefusal, type FeedOpener, type SSEEvent } from "./feed/sse.ts";
|
|
5
6
|
export type * from "./generated/feed.ts";
|
|
6
7
|
export type * from "./generated/kit.ts";
|
|
8
|
+
export type * from "./generated/shares.ts";
|
|
7
9
|
export type * from "./generated/workorder.ts";
|
|
8
10
|
export type * from "./generated/workorder-events.ts";
|
|
9
11
|
export { FEED_SCHEMAS } from "./generated/feed.ts";
|
|
10
12
|
export { KIT_ROUTE_PREFIX, KIT_ROUTE_TABLE, KIT_SCHEMAS, KIT_SIGNOUT_PATH } from "./generated/kit.ts";
|
|
13
|
+
export { SHARES_SCHEMAS } from "./generated/shares.ts";
|
|
11
14
|
export { WORK_ORDER_SCHEMAS } from "./generated/workorder.ts";
|
|
12
15
|
export { WORK_ORDER_EVENT_SCHEMAS } from "./generated/workorder-events.ts";
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Framework-free, with no runtime dependencies: it carries no UI component,
|
|
4
4
|
// because the framework is the author's.
|
|
5
|
-
export { ROUTE_PREFIX, routePath } from "./routes.js";
|
|
5
|
+
export { ROOT_BASE, ROUTE_PREFIX, appBase, routePath, underBase } from "./routes.js";
|
|
6
6
|
export { TeraClient, KitRequestError, UNRECOGNIZED_RESPONSE, } from "./client.js";
|
|
7
|
+
export { STEP_UP_HEADER, STEP_UP_POLL_MS, STEP_UP_REQUIRED, STEP_UP_RESUME_PARAM, StepUpError, resumedStepUp, stepUpChallengeOf, stepUpPageURL, } from "./stepup.js";
|
|
7
8
|
export { BASE_DELAY_MS, MAX_DELAY_MS, REPLAY_LIMIT, REPLAY_WINDOW_MS, HEARTBEAT_GRACE_MS, REAUTHENTICATE_EVENT, FeedConnection, browserSignals, defaultOrderKey, } from "./feed/connection.js";
|
|
8
9
|
export { FeedOpenError, SSEParser, fetchOpener, isRefusal } from "./feed/sse.js";
|
|
9
10
|
export { FEED_SCHEMAS } from "./generated/feed.js";
|
|
10
11
|
export { KIT_ROUTE_PREFIX, KIT_ROUTE_TABLE, KIT_SCHEMAS, KIT_SIGNOUT_PATH } from "./generated/kit.js";
|
|
12
|
+
export { SHARES_SCHEMAS } from "./generated/shares.js";
|
|
11
13
|
export { WORK_ORDER_SCHEMAS } from "./generated/workorder.js";
|
|
12
14
|
export { WORK_ORDER_EVENT_SCHEMAS } from "./generated/workorder-events.js";
|
package/dist/routes.d.ts
CHANGED
|
@@ -8,3 +8,18 @@ export declare const ROUTE_PREFIX = "/tera/v1/";
|
|
|
8
8
|
* collapses the dot segments and encoding does not change them.
|
|
9
9
|
*/
|
|
10
10
|
export declare function routePath(...segments: string[]): string;
|
|
11
|
+
/** The base of an app served at the root of its origin. */
|
|
12
|
+
export declare const ROOT_BASE = "/";
|
|
13
|
+
/**
|
|
14
|
+
* appBase checks the path an app is served under on its origin and answers it
|
|
15
|
+
* ending in `/`: `/` on a plugin's own origin, `/plugins/<name>/` on a
|
|
16
|
+
* path-based installation. A base that is not a root-absolute path on the
|
|
17
|
+
* page's own origin is refused.
|
|
18
|
+
*/
|
|
19
|
+
export declare function appBase(base?: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* underBase places a path the kit names under an app's base: a kit path
|
|
22
|
+
* (`/tera/v1/me`, from {@link routePath}) or a path relative to the base
|
|
23
|
+
* (`_tera/signout`, the kit's `signout_path`) alike.
|
|
24
|
+
*/
|
|
25
|
+
export declare function underBase(base: string, path: string): string;
|
package/dist/routes.js
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
// The route table itself is declared once, in Go, under sdk/plugin/webui, and
|
|
6
6
|
// generated into ./generated/kit.ts. What is here is how a path is built from
|
|
7
7
|
// it.
|
|
8
|
+
//
|
|
9
|
+
// The table's paths are the kit's own: tera's proxy serves a plugin either at
|
|
10
|
+
// the root of its own origin or, on a path-based installation, under
|
|
11
|
+
// `/plugins/<name>/` on tera's origin, and strips that prefix before the kit
|
|
12
|
+
// sees a request. A page therefore reaches every route, and tera's sign-out,
|
|
13
|
+
// under its app's base (`/` or `/plugins/<name>/`), which only the page knows.
|
|
8
14
|
import { KIT_ROUTE_PREFIX } from "./generated/kit.js";
|
|
9
15
|
/** The prefix every kit route is served under. */
|
|
10
16
|
export const ROUTE_PREFIX = KIT_ROUTE_PREFIX;
|
|
@@ -30,3 +36,25 @@ export function routePath(...segments) {
|
|
|
30
36
|
});
|
|
31
37
|
return ROUTE_PREFIX + parts.join("/");
|
|
32
38
|
}
|
|
39
|
+
/** The base of an app served at the root of its origin. */
|
|
40
|
+
export const ROOT_BASE = "/";
|
|
41
|
+
/**
|
|
42
|
+
* appBase checks the path an app is served under on its origin and answers it
|
|
43
|
+
* ending in `/`: `/` on a plugin's own origin, `/plugins/<name>/` on a
|
|
44
|
+
* path-based installation. A base that is not a root-absolute path on the
|
|
45
|
+
* page's own origin is refused.
|
|
46
|
+
*/
|
|
47
|
+
export function appBase(base = ROOT_BASE) {
|
|
48
|
+
if (!base.startsWith("/") || base.startsWith("//") || /[?#\\]/.test(base)) {
|
|
49
|
+
throw new TypeError(`appBase: ${JSON.stringify(base)} is not a root-absolute path on this origin`);
|
|
50
|
+
}
|
|
51
|
+
return base.endsWith("/") ? base : base + "/";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* underBase places a path the kit names under an app's base: a kit path
|
|
55
|
+
* (`/tera/v1/me`, from {@link routePath}) or a path relative to the base
|
|
56
|
+
* (`_tera/signout`, the kit's `signout_path`) alike.
|
|
57
|
+
*/
|
|
58
|
+
export function underBase(base, path) {
|
|
59
|
+
return appBase(base) + (path.startsWith("/") ? path.slice(1) : path);
|
|
60
|
+
}
|
package/dist/stepup.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A person's step-up, from the page's side (ADR-173 D11 step 3).
|
|
3
|
+
*
|
|
4
|
+
* An act the plugin requires the person's confirmation for is refused
|
|
5
|
+
* `step_up_required`: by the plugin's own route, in the kit's envelope with
|
|
6
|
+
* `error.step_up`, or by a Work Order verb, relayed by the invoke route with
|
|
7
|
+
* the challenge in the refusal's `data.step_up`. Either names the step-up's
|
|
8
|
+
* id and the page on tera's origin the person confirms it on. The client then:
|
|
9
|
+
*
|
|
10
|
+
* 1. opens that page in a new window, and when the browser will not open one,
|
|
11
|
+
* navigates to it instead with a `return_to` naming this page, which tera
|
|
12
|
+
* accepts only on the plugin's own origin;
|
|
13
|
+
* 2. polls `GET /tera/v1/step-ups/{id}` until the step-up is satisfied,
|
|
14
|
+
* declined or expired, and closes the window it opened;
|
|
15
|
+
* 3. sends the identical request again with `Tera-Step-Up: <id>` — or, for a
|
|
16
|
+
* Work Order verb, with `step_up` in its params.
|
|
17
|
+
*
|
|
18
|
+
* A step-up confirms one act once: nothing here holds an id past the retry it
|
|
19
|
+
* was confirmed for, except across the navigation fallback, where the id rides
|
|
20
|
+
* back on the `return_to` as {@link STEP_UP_RESUME_PARAM} and is spent by the
|
|
21
|
+
* first request that needs a step-up after the page loads again.
|
|
22
|
+
*/
|
|
23
|
+
import type { KitError } from "./generated/kit.ts";
|
|
24
|
+
/** The request header that carries a confirmed step-up's id to the plugin's route. */
|
|
25
|
+
export declare const STEP_UP_HEADER = "Tera-Step-Up";
|
|
26
|
+
/** The code of a refusal that needs the person's confirmation. */
|
|
27
|
+
export declare const STEP_UP_REQUIRED = "step_up_required";
|
|
28
|
+
/**
|
|
29
|
+
* The query parameter the navigation fallback adds to its `return_to`, naming
|
|
30
|
+
* the step-up the person went to confirm, so the page that loads again
|
|
31
|
+
* resumes it rather than beginning another.
|
|
32
|
+
*/
|
|
33
|
+
export declare const STEP_UP_RESUME_PARAM = "tera_step_up";
|
|
34
|
+
/** How often the step-up's state is read while the person confirms it, by default. */
|
|
35
|
+
export declare const STEP_UP_POLL_MS = 1000;
|
|
36
|
+
/** What a refusal names of the step-up it needs: the kit envelope's `error.step_up`. */
|
|
37
|
+
export type StepUpChallenge = NonNullable<KitError["error"]["step_up"]>;
|
|
38
|
+
/** The part of a `Window` the client uses: whether the person closed it, and closing it. */
|
|
39
|
+
export interface StepUpWindow {
|
|
40
|
+
readonly closed: boolean;
|
|
41
|
+
close(): void;
|
|
42
|
+
}
|
|
43
|
+
export interface StepUpOptions {
|
|
44
|
+
/** Opens the step-up's page in a new window; `window.open(url, "_blank")` when absent. Answers null when the browser would not open one. */
|
|
45
|
+
openWindow?: (url: string) => StepUpWindow | null;
|
|
46
|
+
/** The page's own URL, which the navigation fallback returns to; `location.href` when absent. */
|
|
47
|
+
pageURL?: () => string;
|
|
48
|
+
/** Waits between reads of the step-up's state; a timer when absent. */
|
|
49
|
+
sleep?: (ms: number) => Promise<void>;
|
|
50
|
+
/** How long to wait between reads; {@link STEP_UP_POLL_MS} when absent. */
|
|
51
|
+
pollIntervalMs?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* How a step-up ended without being confirmed:
|
|
55
|
+
* - `declined` — the person said they did not ask for it;
|
|
56
|
+
* - `expired` — nobody confirmed it in time;
|
|
57
|
+
* - `spent` — it confirmed another request already;
|
|
58
|
+
* - `abandoned` — the person closed its window and it is still waiting;
|
|
59
|
+
* - `navigated` — the browser would not open a window, so the page was sent to
|
|
60
|
+
* tera's; it returns here, and the next request that needs a step-up resumes
|
|
61
|
+
* this one.
|
|
62
|
+
*/
|
|
63
|
+
export type StepUpEnd = "declined" | "expired" | "spent" | "abandoned" | "navigated";
|
|
64
|
+
/** A step-up the person did not confirm: the request it was for was not sent again. */
|
|
65
|
+
export declare class StepUpError extends Error {
|
|
66
|
+
/** The step-up's id. */
|
|
67
|
+
readonly stepUpId: string;
|
|
68
|
+
/** How it ended. */
|
|
69
|
+
readonly end: StepUpEnd;
|
|
70
|
+
constructor(stepUpId: string, end: StepUpEnd, message: string);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The step-up a refusal needs confirmed, when it is one this client can have
|
|
74
|
+
* confirmed: code `step_up_required` with a page to open. A refusal naming an
|
|
75
|
+
* ended step-up carries no page, and is not one.
|
|
76
|
+
*/
|
|
77
|
+
export declare function stepUpChallengeOf(code: string, stepUp: unknown): StepUpChallenge | null;
|
|
78
|
+
/**
|
|
79
|
+
* The step-up page's URL with `return_to` set to `returnTo`, itself carrying
|
|
80
|
+
* {@link STEP_UP_RESUME_PARAM}. tera follows it back only when it is on the
|
|
81
|
+
* plugin's own origin.
|
|
82
|
+
*/
|
|
83
|
+
export declare function stepUpPageURL(pageURL: string, stepUpId: string, returnTo: string): string;
|
|
84
|
+
/** The step-up id a page loaded through the navigation fallback carries back, or null. */
|
|
85
|
+
export declare function resumedStepUp(pageURL: string): string | null;
|
|
86
|
+
export declare function defaultOpenWindow(url: string): StepUpWindow | null;
|
|
87
|
+
export declare function defaultSleep(ms: number): Promise<void>;
|
package/dist/stepup.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** The request header that carries a confirmed step-up's id to the plugin's route. */
|
|
2
|
+
export const STEP_UP_HEADER = "Tera-Step-Up";
|
|
3
|
+
/** The code of a refusal that needs the person's confirmation. */
|
|
4
|
+
export const STEP_UP_REQUIRED = "step_up_required";
|
|
5
|
+
/**
|
|
6
|
+
* The query parameter the navigation fallback adds to its `return_to`, naming
|
|
7
|
+
* the step-up the person went to confirm, so the page that loads again
|
|
8
|
+
* resumes it rather than beginning another.
|
|
9
|
+
*/
|
|
10
|
+
export const STEP_UP_RESUME_PARAM = "tera_step_up";
|
|
11
|
+
/** How often the step-up's state is read while the person confirms it, by default. */
|
|
12
|
+
export const STEP_UP_POLL_MS = 1000;
|
|
13
|
+
/** A step-up the person did not confirm: the request it was for was not sent again. */
|
|
14
|
+
export class StepUpError extends Error {
|
|
15
|
+
/** The step-up's id. */
|
|
16
|
+
stepUpId;
|
|
17
|
+
/** How it ended. */
|
|
18
|
+
end;
|
|
19
|
+
constructor(stepUpId, end, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "StepUpError";
|
|
22
|
+
this.stepUpId = stepUpId;
|
|
23
|
+
this.end = end;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The step-up a refusal needs confirmed, when it is one this client can have
|
|
28
|
+
* confirmed: code `step_up_required` with a page to open. A refusal naming an
|
|
29
|
+
* ended step-up carries no page, and is not one.
|
|
30
|
+
*/
|
|
31
|
+
export function stepUpChallengeOf(code, stepUp) {
|
|
32
|
+
if (code !== STEP_UP_REQUIRED || typeof stepUp !== "object" || stepUp === null)
|
|
33
|
+
return null;
|
|
34
|
+
const c = stepUp;
|
|
35
|
+
if (typeof c.id !== "string" || c.id === "" || typeof c.url !== "string" || c.url === "")
|
|
36
|
+
return null;
|
|
37
|
+
return c;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The step-up page's URL with `return_to` set to `returnTo`, itself carrying
|
|
41
|
+
* {@link STEP_UP_RESUME_PARAM}. tera follows it back only when it is on the
|
|
42
|
+
* plugin's own origin.
|
|
43
|
+
*/
|
|
44
|
+
export function stepUpPageURL(pageURL, stepUpId, returnTo) {
|
|
45
|
+
const back = new URL(returnTo);
|
|
46
|
+
back.searchParams.set(STEP_UP_RESUME_PARAM, stepUpId);
|
|
47
|
+
const page = new URL(pageURL);
|
|
48
|
+
page.searchParams.set("return_to", back.toString());
|
|
49
|
+
return page.toString();
|
|
50
|
+
}
|
|
51
|
+
/** The step-up id a page loaded through the navigation fallback carries back, or null. */
|
|
52
|
+
export function resumedStepUp(pageURL) {
|
|
53
|
+
try {
|
|
54
|
+
const id = new URL(pageURL).searchParams.get(STEP_UP_RESUME_PARAM);
|
|
55
|
+
return id === null || id === "" ? null : id;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function defaultOpenWindow(url) {
|
|
62
|
+
return globalThis.open?.(url, "_blank") ?? null;
|
|
63
|
+
}
|
|
64
|
+
export function defaultSleep(ms) {
|
|
65
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kottera/tera-web",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.4",
|
|
4
4
|
"description": "The browser half of a UI over tera: a framework-free client for the /tera/v1/ routes a plugin's webui kit serves (ADR-162 D4).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|