@kottera/tera-web 0.1.0-rc.4 → 0.1.0-rc.5

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 CHANGED
@@ -12,6 +12,7 @@ author's — and has no runtime dependencies. It is not yet published.
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
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/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. |
15
16
  | `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
17
  | `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. |
17
18
  | `src/feed/` | The stream connection (D4 item 2). |
@@ -37,3 +38,16 @@ is here for an author with one, `tsc -p sdk/web`. What the gate does run over
37
38
  the generated types is `src/contract.test.ts`: every committed schema has a
38
39
  generated type, the route table is the committed one, and a fixture of each
39
40
  D3 shape, annotated with its generated type, is valid against its schema.
41
+
42
+ ## Open on your phone
43
+
44
+ ```ts
45
+ const me = await client.me();
46
+ link.href = phoneURL(me, window.location, "/plugins/wb/"); // omit the base at the root of the plugin's own origin
47
+ ```
48
+
49
+ The link is the plugin's own `_tera/phone`, under its base. tera's proxy
50
+ redirects it to tera's `/oauth/phone` only for a page under this
51
+ installation's own base, and to the app's base for any other. The fragment
52
+ rides along, but survives on the phone only when no sign-in intervenes, so a
53
+ deep link a phone must open on its first visit routes by path or query.
package/dist/client.js CHANGED
@@ -47,7 +47,7 @@
47
47
  * window opener and timer, so its tests drive it against a fake and no
48
48
  * network.
49
49
  */
50
- import { appBase, routePath, underBase } from "./routes.js";
50
+ import { appBase, isBaseRelative, routePath, underBase } from "./routes.js";
51
51
  import { FeedConnection } from "./feed/connection.js";
52
52
  import { fetchOpener } from "./feed/sse.js";
53
53
  import { STEP_UP_HEADER, STEP_UP_POLL_MS, StepUpError, defaultOpenWindow, defaultSleep, resumedStepUp, stepUpChallengeOf, stepUpPageURL, } from "./stepup.js";
@@ -335,7 +335,7 @@ export class TeraClient {
335
335
  */
336
336
  async signOut() {
337
337
  const { signout_path: path } = await this.me();
338
- if (typeof path !== "string" || path === "" || path.startsWith("/") || /^[^/?#]*:/.test(path) || /[\\]/.test(path)) {
338
+ if (!isBaseRelative(path)) {
339
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)}`);
340
340
  }
341
341
  this.navigate(underBase(this.base, path));
@@ -85,6 +85,8 @@ export type KitMeResponse = {
85
85
  subject: string;
86
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. */
87
87
  signout_path: string;
88
+ /** Where to send the person to open a page on their phone: tera's proxy's redirect to tera's phone page, given the page's own URL as the next query parameter. A path relative to the app's base, resolved as signout_path is. It is never root-absolute. */
89
+ phone_path: string;
88
90
  /** 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
91
  display_name?: string;
90
92
  /** 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. */
@@ -155,10 +157,13 @@ export declare const KIT_SCHEMAS: {
155
157
  export declare const KIT_ROUTE_PREFIX = "/tera/v1/";
156
158
  /** tera's own sign-out path, which `GET /tera/v1/me` answers as signout_path: relative, resolved against the app's base. */
157
159
  export declare const KIT_SIGNOUT_PATH = "_tera/signout";
160
+ /** tera's proxy's phone path, which `GET /tera/v1/me` answers as phone_path: relative, resolved against the app's base. */
161
+ export declare const KIT_PHONE_PATH = "_tera/phone";
158
162
  /** The kit's route table (ADR-162 D3), as sdk/plugin/webui declares it. */
159
163
  export declare const KIT_ROUTE_TABLE: {
160
164
  readonly prefix: "/tera/v1/";
161
165
  readonly signout_path: "_tera/signout";
166
+ readonly phone_path: "_tera/phone";
162
167
  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.";
163
168
  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.";
164
169
  readonly error: {
@@ -170,7 +175,7 @@ export declare const KIT_ROUTE_TABLE: {
170
175
  readonly name: "me";
171
176
  readonly method: "GET";
172
177
  readonly path: "/tera/v1/me";
173
- readonly summary: "Who is signed in, and where to send them to sign out.";
178
+ readonly summary: "Who is signed in, where to send them to sign out, and where to send them to open a page on their phone.";
174
179
  readonly same_origin: false;
175
180
  readonly response: {
176
181
  readonly media_type: "application/json";
@@ -17,10 +17,13 @@ export const KIT_SCHEMAS = {
17
17
  export const KIT_ROUTE_PREFIX = "/tera/v1/";
18
18
  /** tera's own sign-out path, which `GET /tera/v1/me` answers as signout_path: relative, resolved against the app's base. */
19
19
  export const KIT_SIGNOUT_PATH = "_tera/signout";
20
+ /** tera's proxy's phone path, which `GET /tera/v1/me` answers as phone_path: relative, resolved against the app's base. */
21
+ export const KIT_PHONE_PATH = "_tera/phone";
20
22
  /** The kit's route table (ADR-162 D3), as sdk/plugin/webui declares it. */
21
23
  export const KIT_ROUTE_TABLE = {
22
24
  prefix: "/tera/v1/",
23
25
  signout_path: "_tera/signout",
26
+ phone_path: "_tera/phone",
24
27
  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.",
25
28
  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.",
26
29
  error: {
@@ -33,7 +36,7 @@ export const KIT_ROUTE_TABLE = {
33
36
  name: "me",
34
37
  method: "GET",
35
38
  path: "/tera/v1/me",
36
- summary: "Who is signed in, and where to send them to sign out.",
39
+ summary: "Who is signed in, where to send them to sign out, and where to send them to open a page on their phone.",
37
40
  same_origin: false,
38
41
  response: {
39
42
  media_type: "application/json",
@@ -1404,6 +1404,9 @@ export type WorkOrderGetResult = {
1404
1404
  } | null;
1405
1405
  created_by?: string;
1406
1406
  created_via?: string;
1407
+ arrival_evidence?: {
1408
+ [key: string]: string;
1409
+ };
1407
1410
  created_at: string;
1408
1411
  updated_at: string;
1409
1412
  off_tera: boolean;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { ROOT_BASE, ROUTE_PREFIX, appBase, routePath, underBase } from "./routes.ts";
2
+ export { phoneURL } from "./phone.ts";
2
3
  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
4
  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";
4
5
  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";
@@ -9,7 +10,7 @@ export type * from "./generated/shares.ts";
9
10
  export type * from "./generated/workorder.ts";
10
11
  export type * from "./generated/workorder-events.ts";
11
12
  export { FEED_SCHEMAS } from "./generated/feed.ts";
12
- export { KIT_ROUTE_PREFIX, KIT_ROUTE_TABLE, KIT_SCHEMAS, KIT_SIGNOUT_PATH } from "./generated/kit.ts";
13
+ export { KIT_ROUTE_PREFIX, KIT_ROUTE_TABLE, KIT_PHONE_PATH, KIT_SCHEMAS, KIT_SIGNOUT_PATH } from "./generated/kit.ts";
13
14
  export { SHARES_SCHEMAS } from "./generated/shares.ts";
14
15
  export { WORK_ORDER_SCHEMAS } from "./generated/workorder.ts";
15
16
  export { WORK_ORDER_EVENT_SCHEMAS } from "./generated/workorder-events.ts";
package/dist/index.js CHANGED
@@ -3,12 +3,13 @@
3
3
  // Framework-free, with no runtime dependencies: it carries no UI component,
4
4
  // because the framework is the author's.
5
5
  export { ROOT_BASE, ROUTE_PREFIX, appBase, routePath, underBase } from "./routes.js";
6
+ export { phoneURL } from "./phone.js";
6
7
  export { TeraClient, KitRequestError, UNRECOGNIZED_RESPONSE, } from "./client.js";
7
8
  export { STEP_UP_HEADER, STEP_UP_POLL_MS, STEP_UP_REQUIRED, STEP_UP_RESUME_PARAM, StepUpError, resumedStepUp, stepUpChallengeOf, stepUpPageURL, } from "./stepup.js";
8
9
  export { BASE_DELAY_MS, MAX_DELAY_MS, REPLAY_LIMIT, REPLAY_WINDOW_MS, HEARTBEAT_GRACE_MS, REAUTHENTICATE_EVENT, FeedConnection, browserSignals, defaultOrderKey, } from "./feed/connection.js";
9
10
  export { FeedOpenError, SSEParser, fetchOpener, isRefusal } from "./feed/sse.js";
10
11
  export { FEED_SCHEMAS } from "./generated/feed.js";
11
- export { KIT_ROUTE_PREFIX, KIT_ROUTE_TABLE, KIT_SCHEMAS, KIT_SIGNOUT_PATH } from "./generated/kit.js";
12
+ export { KIT_ROUTE_PREFIX, KIT_ROUTE_TABLE, KIT_PHONE_PATH, KIT_SCHEMAS, KIT_SIGNOUT_PATH } from "./generated/kit.js";
12
13
  export { SHARES_SCHEMAS } from "./generated/shares.js";
13
14
  export { WORK_ORDER_SCHEMAS } from "./generated/workorder.js";
14
15
  export { WORK_ORDER_EVENT_SCHEMAS } from "./generated/workorder-events.js";
@@ -0,0 +1,11 @@
1
+ import type { KitMeResponse } from "./generated/kit.ts";
2
+ /**
3
+ * phoneURL builds the "Open on your phone" link of the page at `location`:
4
+ * the `phone_path` `me` answers, resolved against the app's `base` (`/`, or
5
+ * `/plugins/<name>/` on a path-based installation, as
6
+ * `TeraClientOptions.base`), with `next` set to `location.href`, fragment
7
+ * included. It answers a root-absolute path on the page's own origin. A
8
+ * `phone_path` that is not a path relative to the base is refused rather
9
+ * than followed.
10
+ */
11
+ export declare function phoneURL(me: Pick<KitMeResponse, "phone_path">, location: Pick<Location, "href">, base?: string): string;
package/dist/phone.js ADDED
@@ -0,0 +1,24 @@
1
+ // "Open on your phone" (ADR-177 D4): a link to tera's phone page, which draws
2
+ // the page's URL as a QR code. A page builds it without knowing tera's URL:
3
+ // it links to its own proxy's phone path, under its app's base, and tera's
4
+ // proxy redirects to tera's phone page for a URL under this installation's
5
+ // own base and to the app's base for any other.
6
+ import { ROOT_BASE, isBaseRelative, underBase } from "./routes.js";
7
+ /** The query parameter naming the page the phone opens. */
8
+ const NEXT_PARAM = "next";
9
+ /**
10
+ * phoneURL builds the "Open on your phone" link of the page at `location`:
11
+ * the `phone_path` `me` answers, resolved against the app's `base` (`/`, or
12
+ * `/plugins/<name>/` on a path-based installation, as
13
+ * `TeraClientOptions.base`), with `next` set to `location.href`, fragment
14
+ * included. It answers a root-absolute path on the page's own origin. A
15
+ * `phone_path` that is not a path relative to the base is refused rather
16
+ * than followed.
17
+ */
18
+ export function phoneURL(me, location, base = ROOT_BASE) {
19
+ const path = me.phone_path;
20
+ if (!isBaseRelative(path)) {
21
+ throw new TypeError(`phoneURL: phone_path is not a path relative to the app's base: ${JSON.stringify(path)}`);
22
+ }
23
+ return underBase(base, path) + "?" + new URLSearchParams({ [NEXT_PARAM]: location.href }).toString();
24
+ }
package/dist/routes.d.ts CHANGED
@@ -23,3 +23,10 @@ export declare function appBase(base?: string): string;
23
23
  * (`_tera/signout`, the kit's `signout_path`) alike.
24
24
  */
25
25
  export declare function underBase(base: string, path: string): string;
26
+ /**
27
+ * isBaseRelative reports whether a path the kit answers (`signout_path`,
28
+ * `phone_path`) is relative to the app's base: not empty, not root-absolute,
29
+ * naming no scheme and holding no backslash, so that {@link underBase} keeps
30
+ * it on the page's own origin under its base.
31
+ */
32
+ export declare function isBaseRelative(path: unknown): path is string;
package/dist/routes.js CHANGED
@@ -58,3 +58,12 @@ export function appBase(base = ROOT_BASE) {
58
58
  export function underBase(base, path) {
59
59
  return appBase(base) + (path.startsWith("/") ? path.slice(1) : path);
60
60
  }
61
+ /**
62
+ * isBaseRelative reports whether a path the kit answers (`signout_path`,
63
+ * `phone_path`) is relative to the app's base: not empty, not root-absolute,
64
+ * naming no scheme and holding no backslash, so that {@link underBase} keeps
65
+ * it on the page's own origin under its base.
66
+ */
67
+ export function isBaseRelative(path) {
68
+ return typeof path === "string" && path !== "" && !path.startsWith("/") && !/^[^/?#]*:/.test(path) && !path.includes("\\");
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kottera/tera-web",
3
- "version": "0.1.0-rc.4",
3
+ "version": "0.1.0-rc.5",
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": {