@kottera/tera-web 0.0.0-stage → 0.1.0-rc.10

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.
Files changed (42) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +28 -0
  3. package/README.md +56 -2
  4. package/dist/client.d.ts +223 -0
  5. package/dist/client.js +500 -0
  6. package/dist/feed/connection.d.ts +179 -0
  7. package/dist/feed/connection.js +457 -0
  8. package/dist/feed/sse.d.ts +51 -0
  9. package/dist/feed/sse.js +134 -0
  10. package/dist/generated/feed.d.ts +199 -0
  11. package/dist/generated/feed.js +16 -0
  12. package/dist/generated/kit.d.ts +374 -0
  13. package/dist/generated/kit.js +258 -0
  14. package/dist/generated/shares.d.ts +214 -0
  15. package/dist/generated/shares.js +17 -0
  16. package/dist/generated/workorder-events.d.ts +47 -0
  17. package/dist/generated/workorder-events.js +10 -0
  18. package/dist/generated/workorder.d.ts +3775 -0
  19. package/dist/generated/workorder.js +74 -0
  20. package/dist/generated/wsstream.d.ts +34 -0
  21. package/dist/generated/wsstream.js +28 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.js +24 -0
  24. package/dist/phone.d.ts +11 -0
  25. package/dist/phone.js +24 -0
  26. package/dist/routes.d.ts +32 -0
  27. package/dist/routes.js +69 -0
  28. package/dist/signin.d.ts +48 -0
  29. package/dist/signin.js +74 -0
  30. package/dist/stepup.d.ts +87 -0
  31. package/dist/stepup.js +66 -0
  32. package/dist/stream/connection.d.ts +145 -0
  33. package/dist/stream/connection.js +377 -0
  34. package/dist/terminal/view.d.ts +168 -0
  35. package/dist/terminal/view.js +546 -0
  36. package/dist/terminalwire/codec.d.ts +182 -0
  37. package/dist/terminalwire/codec.js +556 -0
  38. package/dist/terminalwire/collapse.d.ts +4 -0
  39. package/dist/terminalwire/collapse.js +7 -0
  40. package/dist/unreached.d.ts +26 -0
  41. package/dist/unreached.js +30 -0
  42. package/package.json +32 -4
@@ -0,0 +1,74 @@
1
+ // Code generated by cmd/internal/genwebtypes from sdk/workorder/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 WORK_ORDER_SCHEMAS = {
8
+ WorkOrderAcknowledgeParams: "sdk/workorder/schema/v1/acknowledge.params.json",
9
+ WorkOrderAcknowledgeResult: "sdk/workorder/schema/v1/acknowledge.result.json",
10
+ WorkOrderAnswerParams: "sdk/workorder/schema/v1/answer.params.json",
11
+ WorkOrderAnswerResult: "sdk/workorder/schema/v1/answer.result.json",
12
+ WorkOrderCancelParams: "sdk/workorder/schema/v1/cancel.params.json",
13
+ WorkOrderCancelResult: "sdk/workorder/schema/v1/cancel.result.json",
14
+ WorkOrderCohortSetParams: "sdk/workorder/schema/v1/cohort_set.params.json",
15
+ WorkOrderCohortSetResult: "sdk/workorder/schema/v1/cohort_set.result.json",
16
+ WorkOrderCompleteParams: "sdk/workorder/schema/v1/complete.params.json",
17
+ WorkOrderCompleteResult: "sdk/workorder/schema/v1/complete.result.json",
18
+ WorkOrderCreateParams: "sdk/workorder/schema/v1/create.params.json",
19
+ WorkOrderCreateResult: "sdk/workorder/schema/v1/create.result.json",
20
+ WorkOrderCreateChildParams: "sdk/workorder/schema/v1/create_child.params.json",
21
+ WorkOrderCreateChildResult: "sdk/workorder/schema/v1/create_child.result.json",
22
+ WorkOrderEditParams: "sdk/workorder/schema/v1/edit.params.json",
23
+ WorkOrderEditResult: "sdk/workorder/schema/v1/edit.result.json",
24
+ WorkOrderEventsParams: "sdk/workorder/schema/v1/events.params.json",
25
+ WorkOrderEventsResult: "sdk/workorder/schema/v1/events.result.json",
26
+ WorkOrderFailParams: "sdk/workorder/schema/v1/fail.params.json",
27
+ WorkOrderFailResult: "sdk/workorder/schema/v1/fail.result.json",
28
+ WorkOrderForceFailParams: "sdk/workorder/schema/v1/force_fail.params.json",
29
+ WorkOrderForceFailResult: "sdk/workorder/schema/v1/force_fail.result.json",
30
+ WorkOrderGetParams: "sdk/workorder/schema/v1/get.params.json",
31
+ WorkOrderGetResult: "sdk/workorder/schema/v1/get.result.json",
32
+ WorkOrderGuideParams: "sdk/workorder/schema/v1/guide.params.json",
33
+ WorkOrderGuideResult: "sdk/workorder/schema/v1/guide.result.json",
34
+ WorkOrderHistoryParams: "sdk/workorder/schema/v1/history.params.json",
35
+ WorkOrderHistoryResult: "sdk/workorder/schema/v1/history.result.json",
36
+ WorkOrderHistoryUnblockParams: "sdk/workorder/schema/v1/history_unblock.params.json",
37
+ WorkOrderHistoryUnblockResult: "sdk/workorder/schema/v1/history_unblock.result.json",
38
+ WorkOrderHumanAskParams: "sdk/workorder/schema/v1/human.ask.params.json",
39
+ WorkOrderHumanAskResult: "sdk/workorder/schema/v1/human.ask.result.json",
40
+ WorkOrderInterruptParams: "sdk/workorder/schema/v1/interrupt.params.json",
41
+ WorkOrderInterruptResult: "sdk/workorder/schema/v1/interrupt.result.json",
42
+ WorkOrderListParams: "sdk/workorder/schema/v1/list.params.json",
43
+ WorkOrderListResult: "sdk/workorder/schema/v1/list.result.json",
44
+ WorkOrderListOffTeraParams: "sdk/workorder/schema/v1/list_off_tera.params.json",
45
+ WorkOrderListOffTeraResult: "sdk/workorder/schema/v1/list_off_tera.result.json",
46
+ WorkOrderPauseParams: "sdk/workorder/schema/v1/pause.params.json",
47
+ WorkOrderPauseResult: "sdk/workorder/schema/v1/pause.result.json",
48
+ WorkOrderPrioritizeParams: "sdk/workorder/schema/v1/prioritize.params.json",
49
+ WorkOrderPrioritizeResult: "sdk/workorder/schema/v1/prioritize.result.json",
50
+ WorkOrderProgressParams: "sdk/workorder/schema/v1/progress.params.json",
51
+ WorkOrderProgressResult: "sdk/workorder/schema/v1/progress.result.json",
52
+ WorkOrderRecallParams: "sdk/workorder/schema/v1/recall.params.json",
53
+ WorkOrderRecallResult: "sdk/workorder/schema/v1/recall.result.json",
54
+ WorkOrderRefileParams: "sdk/workorder/schema/v1/refile.params.json",
55
+ WorkOrderRefileResult: "sdk/workorder/schema/v1/refile.result.json",
56
+ WorkOrderRelateParams: "sdk/workorder/schema/v1/relate.params.json",
57
+ WorkOrderRelateResult: "sdk/workorder/schema/v1/relate.result.json",
58
+ WorkOrderReleaseParams: "sdk/workorder/schema/v1/release.params.json",
59
+ WorkOrderReleaseResult: "sdk/workorder/schema/v1/release.result.json",
60
+ WorkOrderRepinParams: "sdk/workorder/schema/v1/repin.params.json",
61
+ WorkOrderRepinResult: "sdk/workorder/schema/v1/repin.result.json",
62
+ WorkOrderRepostParams: "sdk/workorder/schema/v1/repost.params.json",
63
+ WorkOrderRepostResult: "sdk/workorder/schema/v1/repost.result.json",
64
+ WorkOrderRetryParams: "sdk/workorder/schema/v1/retry.params.json",
65
+ WorkOrderRetryResult: "sdk/workorder/schema/v1/retry.result.json",
66
+ WorkOrderSubmitResultParams: "sdk/workorder/schema/v1/submit_result.params.json",
67
+ WorkOrderSubmitResultResult: "sdk/workorder/schema/v1/submit_result.result.json",
68
+ WorkOrderTakeParams: "sdk/workorder/schema/v1/take.params.json",
69
+ WorkOrderTakeResult: "sdk/workorder/schema/v1/take.result.json",
70
+ WorkOrderTransitionParams: "sdk/workorder/schema/v1/transition.params.json",
71
+ WorkOrderTransitionResult: "sdk/workorder/schema/v1/transition.result.json",
72
+ WorkOrderTreeParams: "sdk/workorder/schema/v1/tree.params.json",
73
+ WorkOrderTreeResult: "sdk/workorder/schema/v1/tree.result.json",
74
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The close vocabulary of a tera stream (sdk/wsstream.CloseCodes).
3
+ *
4
+ * The WebSocket close codes a tera stream is closed with. Each states only what the hop that closed decided, and a relay passes a close on as it arrived. The reason beside a code is a code of at most 64 bytes of [a-z0-9_.:-], or "closed". A code is never renamed or given a new meaning; a client that meets a code it does not know treats it as retry_later (4001).
5
+ *
6
+ * Schema: sdk/wsstream/schema/v1/close-code.json
7
+ */
8
+ export type StreamCloseCode = 1000 | 1001 | 1009 | 4000 | 4001 | 4003;
9
+ /**
10
+ * Each member of StreamCloseCode, by name.
11
+ *
12
+ * Schema: sdk/wsstream/schema/v1/close-code.json
13
+ */
14
+ export declare const STREAM_CLOSE_CODES: {
15
+ /** What the client does: what the protocol above said last; otherwise nothing. */
16
+ readonly ended: 1000;
17
+ /** What the client does: reconnects after backoff: the peer is stopping or stopped answering pings. */
18
+ readonly going_away: 1001;
19
+ /** What the client does: nothing: a message broke the route's bound, a client defect. */
20
+ readonly too_big: 1009;
21
+ /** What the client does: reconnects at once with the same identity: a token renewal. */
22
+ readonly reconnect: 4000;
23
+ /** What the client does: reconnects after backoff: the far side is unreachable or not ready. */
24
+ readonly retry_later: 4001;
25
+ /** What the client does: nothing until something changes; the reason is the error code. */
26
+ readonly refused: 4003;
27
+ };
28
+ /**
29
+ * Every type this module declares, mapped to the repository path of the schema
30
+ * it was generated from.
31
+ */
32
+ export declare const STREAM_SCHEMAS: {
33
+ readonly StreamCloseCode: "sdk/wsstream/schema/v1/close-code.json";
34
+ };
@@ -0,0 +1,28 @@
1
+ // Code generated by cmd/internal/genwebtypes from sdk/wsstream/schema/v1/; DO NOT EDIT.
2
+ // Regenerate with `make web-types` (ADR-162 D4 item 1).
3
+ /**
4
+ * Each member of StreamCloseCode, by name.
5
+ *
6
+ * Schema: sdk/wsstream/schema/v1/close-code.json
7
+ */
8
+ export const STREAM_CLOSE_CODES = {
9
+ /** What the client does: what the protocol above said last; otherwise nothing. */
10
+ ended: 1000,
11
+ /** What the client does: reconnects after backoff: the peer is stopping or stopped answering pings. */
12
+ going_away: 1001,
13
+ /** What the client does: nothing: a message broke the route's bound, a client defect. */
14
+ too_big: 1009,
15
+ /** What the client does: reconnects at once with the same identity: a token renewal. */
16
+ reconnect: 4000,
17
+ /** What the client does: reconnects after backoff: the far side is unreachable or not ready. */
18
+ retry_later: 4001,
19
+ /** What the client does: nothing until something changes; the reason is the error code. */
20
+ refused: 4003,
21
+ };
22
+ /**
23
+ * Every type this module declares, mapped to the repository path of the schema
24
+ * it was generated from.
25
+ */
26
+ export const STREAM_SCHEMAS = {
27
+ StreamCloseCode: "sdk/wsstream/schema/v1/close-code.json",
28
+ };
@@ -0,0 +1,23 @@
1
+ export { ROOT_BASE, ROUTE_PREFIX, appBase, routePath, underBase } from "./routes.ts";
2
+ export { phoneURL } from "./phone.ts";
3
+ export { SIGN_IN_REQUIRED, isSignInRequired, isSignInRequiredAnswer, reSignIn, type ReSignInOptions } from "./signin.ts";
4
+ export { PLUGIN_BUSY, PLUGIN_STARTING, PLUGIN_RUNNER_UNREACHABLE, PLUGIN_NOT_ANSWERING, PLUGIN_UNREACHED_CODES, } from "./unreached.ts";
5
+ export { TeraClient, KitRequestError, UNRECOGNIZED_RESPONSE, type FeedOptions, type StreamOptions, type TerminalOptions, type InvokeVerdict, type KitErrorBody, type TeraClientOptions, type UploadOptions, type UploadProgress, type UploadRequest, type VerbRefusal, } from "./client.ts";
6
+ 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";
7
+ export { BASE_DELAY_MS, MAX_DELAY_MS, REPLAY_LIMIT, REPLAY_WINDOW_MS, HEARTBEAT_GRACE_MS, REAUTHENTICATE_EVENT, FeedConnection, browserSignals, defaultOrderKey, type Clock, type FeedBackfill, type FeedConnectionOptions, type FeedConnectionPhase, type FeedConnectionState, type FeedFrame, type FeedLoss, type FeedSignal, type FeedSignals, } from "./feed/connection.ts";
8
+ export { IMMEDIATE_LIMIT, IMMEDIATE_WINDOW_MS, MAX_BUFFERED_BYTES, SEND_BUFFER_EXCEEDED, STABLE_OPEN_MS, StreamConnection, webSocketURL, type NormalCloseNext, type StreamConnectionOptions, type StreamConnectionState, type WebSocketLike, } from "./stream/connection.ts";
9
+ export { ACK_EVERY, INPUT_GRANT_TIMEOUT, INPUT_GRANT_TIMEOUT_MS, INPUT_WINDOW, MAX_QUEUED_INPUT, MAX_SIGNALS_PER_GRANT, RESIZE_DEBOUNCE_MS, TerminalView, newViewId, nextAfterClose, type TerminalRenderer, type TerminalStatus, type TerminalViewOptions, } from "./terminal/view.ts";
10
+ export { FeedOpenError, SSEParser, fetchOpener, isRefusal, type FeedOpener, type SSEEvent } from "./feed/sse.ts";
11
+ export * as terminalwire from "./terminalwire/codec.ts";
12
+ export type * from "./generated/feed.ts";
13
+ export type * from "./generated/kit.ts";
14
+ export type * from "./generated/shares.ts";
15
+ export type * from "./generated/workorder.ts";
16
+ export type * from "./generated/workorder-events.ts";
17
+ export type * from "./generated/wsstream.ts";
18
+ export { FEED_SCHEMAS } from "./generated/feed.ts";
19
+ export { KIT_ROUTE_PREFIX, KIT_ROUTE_TABLE, KIT_PHONE_PATH, KIT_SCHEMAS, KIT_SIGNOUT_PATH } from "./generated/kit.ts";
20
+ export { SHARES_SCHEMAS } from "./generated/shares.ts";
21
+ export { WORK_ORDER_SCHEMAS } from "./generated/workorder.ts";
22
+ export { WORK_ORDER_EVENT_SCHEMAS } from "./generated/workorder-events.ts";
23
+ export { STREAM_CLOSE_CODES, STREAM_SCHEMAS } from "./generated/wsstream.ts";
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ // @kottera/tera-web — the browser half of a UI over tera (ADR-162 D4).
2
+ //
3
+ // Framework-free, with no runtime dependencies: it carries no UI component,
4
+ // because the framework is the author's.
5
+ export { ROOT_BASE, ROUTE_PREFIX, appBase, routePath, underBase } from "./routes.js";
6
+ export { phoneURL } from "./phone.js";
7
+ export { SIGN_IN_REQUIRED, isSignInRequired, isSignInRequiredAnswer, reSignIn } from "./signin.js";
8
+ export { PLUGIN_BUSY, PLUGIN_STARTING, PLUGIN_RUNNER_UNREACHABLE, PLUGIN_NOT_ANSWERING, PLUGIN_UNREACHED_CODES, } from "./unreached.js";
9
+ export { TeraClient, KitRequestError, UNRECOGNIZED_RESPONSE, } from "./client.js";
10
+ export { STEP_UP_HEADER, STEP_UP_POLL_MS, STEP_UP_REQUIRED, STEP_UP_RESUME_PARAM, StepUpError, resumedStepUp, stepUpChallengeOf, stepUpPageURL, } from "./stepup.js";
11
+ export { BASE_DELAY_MS, MAX_DELAY_MS, REPLAY_LIMIT, REPLAY_WINDOW_MS, HEARTBEAT_GRACE_MS, REAUTHENTICATE_EVENT, FeedConnection, browserSignals, defaultOrderKey, } from "./feed/connection.js";
12
+ export { IMMEDIATE_LIMIT, IMMEDIATE_WINDOW_MS, MAX_BUFFERED_BYTES, SEND_BUFFER_EXCEEDED, STABLE_OPEN_MS, StreamConnection, webSocketURL, } from "./stream/connection.js";
13
+ export { ACK_EVERY, INPUT_GRANT_TIMEOUT, INPUT_GRANT_TIMEOUT_MS, INPUT_WINDOW, MAX_QUEUED_INPUT, MAX_SIGNALS_PER_GRANT, RESIZE_DEBOUNCE_MS, TerminalView, newViewId, nextAfterClose, } from "./terminal/view.js";
14
+ export { FeedOpenError, SSEParser, fetchOpener, isRefusal } from "./feed/sse.js";
15
+ // The frames a view of an attended session's terminal speaks (ADR-180 D4):
16
+ // github.com/kottera/sdk/terminalwire's codec in the browser, under one name,
17
+ // so its frame and vocabulary names claim nothing at the package's root.
18
+ export * as terminalwire from "./terminalwire/codec.js";
19
+ export { FEED_SCHEMAS } from "./generated/feed.js";
20
+ export { KIT_ROUTE_PREFIX, KIT_ROUTE_TABLE, KIT_PHONE_PATH, KIT_SCHEMAS, KIT_SIGNOUT_PATH } from "./generated/kit.js";
21
+ export { SHARES_SCHEMAS } from "./generated/shares.js";
22
+ export { WORK_ORDER_SCHEMAS } from "./generated/workorder.js";
23
+ export { WORK_ORDER_EVENT_SCHEMAS } from "./generated/workorder-events.js";
24
+ export { STREAM_CLOSE_CODES, STREAM_SCHEMAS } from "./generated/wsstream.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
+ }
@@ -0,0 +1,32 @@
1
+ /** The prefix every kit route is served under. */
2
+ export declare const ROUTE_PREFIX = "/tera/v1/";
3
+ /**
4
+ * routePath builds a kit route's path from its segments, each encoded as one
5
+ * path segment: a capability or feed name holding `/` stays one segment rather
6
+ * than becoming two. An empty segment, `.` or `..` is refused: each would
7
+ * address a different route than the caller named, since a URL resolver
8
+ * collapses the dot segments and encoding does not change them.
9
+ */
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;
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 ADDED
@@ -0,0 +1,69 @@
1
+ // The browser routes a plugin's webui kit serves, all under one reserved
2
+ // prefix on the plugin's origin (ADR-162 D3). The prefix is not `/_tera/`,
3
+ // which tera's reverse proxy keeps for its own paths and never forwards.
4
+ //
5
+ // The route table itself is declared once, in Go, under sdk/plugin/webui, and
6
+ // generated into ./generated/kit.ts. What is here is how a path is built from
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.
14
+ import { KIT_ROUTE_PREFIX } from "./generated/kit.js";
15
+ /** The prefix every kit route is served under. */
16
+ export const ROUTE_PREFIX = KIT_ROUTE_PREFIX;
17
+ /**
18
+ * routePath builds a kit route's path from its segments, each encoded as one
19
+ * path segment: a capability or feed name holding `/` stays one segment rather
20
+ * than becoming two. An empty segment, `.` or `..` is refused: each would
21
+ * address a different route than the caller named, since a URL resolver
22
+ * collapses the dot segments and encoding does not change them.
23
+ */
24
+ export function routePath(...segments) {
25
+ if (segments.length === 0) {
26
+ throw new TypeError("routePath: at least one segment is required");
27
+ }
28
+ const parts = segments.map((segment, i) => {
29
+ if (segment === "") {
30
+ throw new TypeError(`routePath: segment ${i} is empty`);
31
+ }
32
+ if (segment === "." || segment === "..") {
33
+ throw new TypeError(`routePath: segment ${i} is a dot segment (${segment})`);
34
+ }
35
+ return encodeURIComponent(segment);
36
+ });
37
+ return ROUTE_PREFIX + parts.join("/");
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
+ }
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
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Signing in again after tera forgets the page's session.
3
+ *
4
+ * tera's UI proxy holds a plugin origin's sessions in memory, so a daemon
5
+ * restart forgets every one. Only a page load can sign the person in again —
6
+ * silently, while their tera session lives — so the proxy answers anything
7
+ * else that arrives with no live session, a script's `fetch` among them, 401
8
+ * in the kit's envelope with code {@link SIGN_IN_REQUIRED}. The page recovers
9
+ * by loading itself again ({@link reSignIn}).
10
+ *
11
+ * That cannot loop: a page load never receives this answer, it starts the
12
+ * sign-in. A person whose tera session is gone too lands on tera's sign-in
13
+ * page, which is where they have to be.
14
+ *
15
+ * It is not the kit's `unauthenticated`, which says the request did reach the
16
+ * plugin, with no verified caller; loading the page again does not fix that.
17
+ *
18
+ * {@link TeraClient} and {@link FeedConnection} react on their own. A page
19
+ * reading one of the plugin's routes with its own `fetch` applies
20
+ * {@link isSignInRequired} to the response:
21
+ *
22
+ * const res = await fetch("api/boards");
23
+ * if (await isSignInRequired(res)) return reSignIn();
24
+ */
25
+ /** The code of the proxy's answer to a request that needs the page loaded again to sign in. */
26
+ export declare const SIGN_IN_REQUIRED = "sign_in_required";
27
+ export interface ReSignInOptions {
28
+ /** Sends the page to a URL; `location.assign` when absent. */
29
+ navigate?: (url: string) => void;
30
+ /** The page's own URL, which is loaded again; `location.href` when absent. */
31
+ pageURL?: () => string;
32
+ }
33
+ /**
34
+ * Whether an answer is the proxy's `sign_in_required`: HTTP 401, a JSON body,
35
+ * and the kit's envelope carrying that code. Any other 401 — the kit's own
36
+ * `unauthenticated`, another code, a body that is not JSON — is not.
37
+ */
38
+ export declare function isSignInRequiredAnswer(status: number, contentType: string, body: string): boolean;
39
+ /**
40
+ * Whether `res` is the proxy's `sign_in_required` answer. It reads a copy of
41
+ * the body, so `res` can still be read by the caller.
42
+ */
43
+ export declare function isSignInRequired(res: Response): Promise<boolean>;
44
+ /**
45
+ * Loads the page again, a top-level navigation to its own URL, which signs
46
+ * the person in to the plugin's origin and returns them where they were.
47
+ */
48
+ export declare function reSignIn(options?: ReSignInOptions): void;
package/dist/signin.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Signing in again after tera forgets the page's session.
3
+ *
4
+ * tera's UI proxy holds a plugin origin's sessions in memory, so a daemon
5
+ * restart forgets every one. Only a page load can sign the person in again —
6
+ * silently, while their tera session lives — so the proxy answers anything
7
+ * else that arrives with no live session, a script's `fetch` among them, 401
8
+ * in the kit's envelope with code {@link SIGN_IN_REQUIRED}. The page recovers
9
+ * by loading itself again ({@link reSignIn}).
10
+ *
11
+ * That cannot loop: a page load never receives this answer, it starts the
12
+ * sign-in. A person whose tera session is gone too lands on tera's sign-in
13
+ * page, which is where they have to be.
14
+ *
15
+ * It is not the kit's `unauthenticated`, which says the request did reach the
16
+ * plugin, with no verified caller; loading the page again does not fix that.
17
+ *
18
+ * {@link TeraClient} and {@link FeedConnection} react on their own. A page
19
+ * reading one of the plugin's routes with its own `fetch` applies
20
+ * {@link isSignInRequired} to the response:
21
+ *
22
+ * const res = await fetch("api/boards");
23
+ * if (await isSignInRequired(res)) return reSignIn();
24
+ */
25
+ /** The code of the proxy's answer to a request that needs the page loaded again to sign in. */
26
+ export const SIGN_IN_REQUIRED = "sign_in_required";
27
+ const JSON_TYPE = "application/json";
28
+ /**
29
+ * Whether an answer is the proxy's `sign_in_required`: HTTP 401, a JSON body,
30
+ * and the kit's envelope carrying that code. Any other 401 — the kit's own
31
+ * `unauthenticated`, another code, a body that is not JSON — is not.
32
+ */
33
+ export function isSignInRequiredAnswer(status, contentType, body) {
34
+ if (status !== 401)
35
+ return false;
36
+ if (contentType.toLowerCase().split(";")[0].trim() !== JSON_TYPE)
37
+ return false;
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(body);
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ if (typeof parsed !== "object" || parsed === null)
46
+ return false;
47
+ const error = parsed.error;
48
+ return typeof error === "object" && error !== null && error.code === SIGN_IN_REQUIRED;
49
+ }
50
+ /**
51
+ * Whether `res` is the proxy's `sign_in_required` answer. It reads a copy of
52
+ * the body, so `res` can still be read by the caller.
53
+ */
54
+ export async function isSignInRequired(res) {
55
+ if (res.status !== 401)
56
+ return false;
57
+ let text;
58
+ try {
59
+ text = await res.clone().text();
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ return isSignInRequiredAnswer(res.status, res.headers.get("content-type") ?? "", text);
65
+ }
66
+ /**
67
+ * Loads the page again, a top-level navigation to its own URL, which signs
68
+ * the person in to the plugin's origin and returns them where they were.
69
+ */
70
+ export function reSignIn(options = {}) {
71
+ const navigate = options.navigate ?? ((url) => globalThis.location.assign(url));
72
+ const pageURL = options.pageURL ?? (() => globalThis.location.href);
73
+ navigate(pageURL());
74
+ }
@@ -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
+ }