@kottera/tera-web 0.1.0-rc.5 → 0.1.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/client.d.ts +32 -1
- package/dist/client.js +69 -7
- package/dist/feed/connection.d.ts +6 -0
- package/dist/feed/connection.js +6 -0
- package/dist/generated/kit.d.ts +70 -2
- package/dist/generated/kit.js +86 -0
- package/dist/generated/shares.d.ts +3 -1
- package/dist/generated/workorder-events.d.ts +1 -1
- package/dist/generated/workorder.d.ts +82 -0
- package/dist/generated/wsstream.d.ts +34 -0
- package/dist/generated/wsstream.js +28 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +9 -0
- package/dist/signin.d.ts +48 -0
- package/dist/signin.js +74 -0
- package/dist/stream/connection.d.ts +145 -0
- package/dist/stream/connection.js +377 -0
- package/dist/terminal/view.d.ts +168 -0
- package/dist/terminal/view.js +540 -0
- package/dist/terminalwire/codec.d.ts +176 -0
- package/dist/terminalwire/codec.js +547 -0
- package/dist/unreached.d.ts +26 -0
- package/dist/unreached.js +30 -0
- package/package.json +1 -1
|
@@ -476,6 +476,21 @@ export type WorkOrderCreateParams = {
|
|
|
476
476
|
needs_approval?: boolean;
|
|
477
477
|
/** Existing Work Order to file this one beneath, making it part of that train. It must be one you can read with work_order.get; one you cannot is refused exactly as that read would refuse it. The new Work Order takes that parent's cohort, verbatim; beside it `cohort` accepts `{"new": true, "label"?: …}`, a sub-unit nested under the parent's cohort, and `{"of": "<work-order-id>"}`, joining the cohort of a Work Order in the parent's tree. */
|
|
478
478
|
parent_id?: string;
|
|
479
|
+
/** Directories of the filing plugin's data volume this Work Order's dispatch is handed. Only a plugin may name them — as itself or acting for a person, or on create_child as the taker of its own off-tera work — and only for a type of its own vault; any other filer is refused (plugin_data_not_a_plugin). The dispatch is placed on the host holding the volume and sees the named directories and nothing else of it. Name input, output or both; neither may contain the other. */
|
|
480
|
+
plugin_data?: {
|
|
481
|
+
/** The one directory the dispatch reads, mounted read-only and told to the step in TERA_PLUGIN_DATA_INPUT. It must exist when the step starts (plugin_data_unavailable). */
|
|
482
|
+
input?: {
|
|
483
|
+
/** A clean relative path inside the plugin's data volume: no `..`, not absolute. */
|
|
484
|
+
dir: string;
|
|
485
|
+
};
|
|
486
|
+
/** The one directory the dispatch writes, told to the step in TERA_PLUGIN_DATA_OUTPUT. Created if absent; refused when not empty at the step's start (plugin_output_not_empty). */
|
|
487
|
+
output?: {
|
|
488
|
+
/** The most the directory may hold when the step ends: at least 1 GiB, at most the installation's hosted { step_output_limit }. Past it the step fails (plugin_output_bound_exceeded). */
|
|
489
|
+
bound_bytes: number;
|
|
490
|
+
/** A clean relative path inside the plugin's data volume: no `..`, not absolute. */
|
|
491
|
+
dir: string;
|
|
492
|
+
};
|
|
493
|
+
};
|
|
479
494
|
/** Dispatch priority; higher runs sooner among ready Work Orders. */
|
|
480
495
|
priority?: number;
|
|
481
496
|
/** Typed, NON-CAUSAL relations from this Work Order to others (supersedes / duplicate_of / about / related). Each entry is a {relation, work_order_id} pair with an optional note, and `work_order_id` is a bare Work Order id — the same spelling `depends_on` and `parent_id` use, never a `vault://` ref. Set them here when the fact is known at filing time, which for the superseding case it usually is; discovered later, use work_order.relate. Three of the four gate nothing: no dispatch is blocked, delayed or cancelled by `duplicate_of`, `about` or `related`. A `supersedes` entry is the exception and it is the point of setting one here — it RETIRES the Work Order it names, in this call's own transaction: that Work Order moves to `superseded` with `superseded_by` set to this one, and can no longer be retried. The target must be in `failed:awaiting-ack`; one that is still running is refused, and the door is work_order.force_fail(id, reason) first. A terminal one is refused too — there is no retry to close — and so is a `supersedes` entry on a drafted create, since a draft may never be released. To record the claim WITHOUT moving the row, use work_order.relate. Same authorization as work_order.relate — a worker-class caller is refused, and an operator-class one needs `api/v1/workorders/relate` on top of its create grant, because the edge is the relate operation whichever call delivered it. Every target must exist, and no two entries may name the same target. Reach on the target is required for the two EVALUATIVE relations (`supersedes`, `duplicate_of`) and not for the two descriptive ones (`about`, `related`), per entry rather than per payload. */
|
|
@@ -590,6 +605,21 @@ export type WorkOrderCreateChildParams = {
|
|
|
590
605
|
on_child_failure?: "fail-fast" | "wait-others" | "ignore";
|
|
591
606
|
/** The off-tera Work Order you hold, to file this child beneath (ADR-139 D6). Honoured only for its current taker; omit it inside a dispatch, where the parent is the dispatch's own Work Order. */
|
|
592
607
|
parent?: string;
|
|
608
|
+
/** Directories of the filing plugin's data volume this Work Order's dispatch is handed. Only a plugin may name them — as itself or acting for a person, or on create_child as the taker of its own off-tera work — and only for a type of its own vault; any other filer is refused (plugin_data_not_a_plugin). The dispatch is placed on the host holding the volume and sees the named directories and nothing else of it. Name input, output or both; neither may contain the other. */
|
|
609
|
+
plugin_data?: {
|
|
610
|
+
/** The one directory the dispatch reads, mounted read-only and told to the step in TERA_PLUGIN_DATA_INPUT. It must exist when the step starts (plugin_data_unavailable). */
|
|
611
|
+
input?: {
|
|
612
|
+
/** A clean relative path inside the plugin's data volume: no `..`, not absolute. */
|
|
613
|
+
dir: string;
|
|
614
|
+
};
|
|
615
|
+
/** The one directory the dispatch writes, told to the step in TERA_PLUGIN_DATA_OUTPUT. Created if absent; refused when not empty at the step's start (plugin_output_not_empty). */
|
|
616
|
+
output?: {
|
|
617
|
+
/** The most the directory may hold when the step ends: at least 1 GiB, at most the installation's hosted { step_output_limit }. Past it the step fails (plugin_output_bound_exceeded). */
|
|
618
|
+
bound_bytes: number;
|
|
619
|
+
/** A clean relative path inside the plugin's data volume: no `..`, not absolute. */
|
|
620
|
+
dir: string;
|
|
621
|
+
};
|
|
622
|
+
};
|
|
593
623
|
/** Informational: echoed back for your own bookkeeping. What governs whether the parent waits is the wait it declares, not this field. */
|
|
594
624
|
policy?: "wait-for-all" | "wait-for-any" | "fire-and-forget";
|
|
595
625
|
/** Dispatch priority; higher runs sooner among ready Work Orders. */
|
|
@@ -974,6 +1004,9 @@ export type WorkOrderGetResult = {
|
|
|
974
1004
|
at: string;
|
|
975
1005
|
};
|
|
976
1006
|
display_name?: string;
|
|
1007
|
+
work_order?: string;
|
|
1008
|
+
attempt?: number;
|
|
1009
|
+
runner?: string;
|
|
977
1010
|
};
|
|
978
1011
|
};
|
|
979
1012
|
question?: {
|
|
@@ -994,6 +1027,7 @@ export type WorkOrderGetResult = {
|
|
|
994
1027
|
member?: string;
|
|
995
1028
|
pool?: string;
|
|
996
1029
|
pool_cause?: string;
|
|
1030
|
+
pool_note?: string;
|
|
997
1031
|
verb?: string;
|
|
998
1032
|
repairable: boolean;
|
|
999
1033
|
refused_at?: string;
|
|
@@ -1404,12 +1438,35 @@ export type WorkOrderGetResult = {
|
|
|
1404
1438
|
} | null;
|
|
1405
1439
|
created_by?: string;
|
|
1406
1440
|
created_via?: string;
|
|
1441
|
+
released_via?: string;
|
|
1442
|
+
lent_by?: string;
|
|
1407
1443
|
arrival_evidence?: {
|
|
1408
1444
|
[key: string]: string;
|
|
1409
1445
|
};
|
|
1410
1446
|
created_at: string;
|
|
1411
1447
|
updated_at: string;
|
|
1412
1448
|
off_tera: boolean;
|
|
1449
|
+
attended?: {
|
|
1450
|
+
host?: string;
|
|
1451
|
+
personal: boolean;
|
|
1452
|
+
candidate?: string;
|
|
1453
|
+
unsandboxed: boolean;
|
|
1454
|
+
unsandboxed_notice?: string;
|
|
1455
|
+
attendee: string;
|
|
1456
|
+
attached: {
|
|
1457
|
+
views: number;
|
|
1458
|
+
controller?: string;
|
|
1459
|
+
size_setter?: string;
|
|
1460
|
+
since?: string;
|
|
1461
|
+
last_detached_at?: string;
|
|
1462
|
+
} | null;
|
|
1463
|
+
signal: {
|
|
1464
|
+
signal: string;
|
|
1465
|
+
message?: string;
|
|
1466
|
+
at: string;
|
|
1467
|
+
dropped?: number;
|
|
1468
|
+
} | null;
|
|
1469
|
+
};
|
|
1413
1470
|
has_outputs?: boolean;
|
|
1414
1471
|
archived: boolean;
|
|
1415
1472
|
history?: {
|
|
@@ -1739,6 +1796,8 @@ export type WorkOrderInterruptResult = {
|
|
|
1739
1796
|
* Schema: sdk/workorder/schema/v1/list.params.json
|
|
1740
1797
|
*/
|
|
1741
1798
|
export type WorkOrderListParams = {
|
|
1799
|
+
/** Select only attended Work Orders — sessions a person attends in a terminal — and carry on each row the `attended` projection work_order.get answers (host, personal, candidate, unsandboxed, attendee, attached, signal), so a list of sessions needs no get per row. Absent or false is no constraint, and rows then carry no projection. Unindexed on both store backends; pair it with `states` or `parent_id`. */
|
|
1800
|
+
attended?: boolean;
|
|
1742
1801
|
/** Select the Work Orders in this cohort — the undertaking a Work Order belongs to, read off any row's `cohort_id` (a Work Order filed alone is a cohort of one, whose id is its own). A UUID; anything else is refused rather than matched. Unlike `tag` this reads an indexed column on both store backends, so it is the filter to pass alone for 'everything in this undertaking', in every state, terminal included. Composes with the other filters as an AND. */
|
|
1743
1802
|
cohort?: string;
|
|
1744
1803
|
/** Select Work Orders filed with exactly this resolved capacity key. Matched verbatim against the key on the row, which is already materialized against that Work Order's own inputs — pass `prefilter/<subject id>`, not the type's `${inputs...}` template. This is how a caller asks 'is another Work Order of this tier already working my subject?' without reading inputs back out of every row. */
|
|
@@ -1816,6 +1875,27 @@ export type WorkOrderListResult = {
|
|
|
1816
1875
|
at: string;
|
|
1817
1876
|
};
|
|
1818
1877
|
};
|
|
1878
|
+
attended?: {
|
|
1879
|
+
host?: string;
|
|
1880
|
+
personal: boolean;
|
|
1881
|
+
candidate?: string;
|
|
1882
|
+
unsandboxed: boolean;
|
|
1883
|
+
unsandboxed_notice?: string;
|
|
1884
|
+
attendee: string;
|
|
1885
|
+
attached: {
|
|
1886
|
+
views: number;
|
|
1887
|
+
controller?: string;
|
|
1888
|
+
size_setter?: string;
|
|
1889
|
+
since?: string;
|
|
1890
|
+
last_detached_at?: string;
|
|
1891
|
+
} | null;
|
|
1892
|
+
signal: {
|
|
1893
|
+
signal: string;
|
|
1894
|
+
message?: string;
|
|
1895
|
+
at: string;
|
|
1896
|
+
dropped?: number;
|
|
1897
|
+
} | null;
|
|
1898
|
+
};
|
|
1819
1899
|
}[] | null;
|
|
1820
1900
|
};
|
|
1821
1901
|
/**
|
|
@@ -3115,6 +3195,7 @@ export type WorkOrderTreeResult = {
|
|
|
3115
3195
|
member?: string;
|
|
3116
3196
|
pool?: string;
|
|
3117
3197
|
pool_cause?: string;
|
|
3198
|
+
pool_note?: string;
|
|
3118
3199
|
verb?: string;
|
|
3119
3200
|
repairable: boolean;
|
|
3120
3201
|
refused_at?: string;
|
|
@@ -3345,6 +3426,7 @@ export type WorkOrderTreeResult = {
|
|
|
3345
3426
|
member?: string;
|
|
3346
3427
|
pool?: string;
|
|
3347
3428
|
pool_cause?: string;
|
|
3429
|
+
pool_note?: string;
|
|
3348
3430
|
verb?: string;
|
|
3349
3431
|
repairable: boolean;
|
|
3350
3432
|
refused_at?: string;
|
|
@@ -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
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
export { ROOT_BASE, ROUTE_PREFIX, appBase, routePath, underBase } from "./routes.ts";
|
|
2
2
|
export { phoneURL } from "./phone.ts";
|
|
3
|
-
export {
|
|
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";
|
|
4
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";
|
|
5
7
|
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";
|
|
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";
|
|
6
10
|
export { FeedOpenError, SSEParser, fetchOpener, isRefusal, type FeedOpener, type SSEEvent } from "./feed/sse.ts";
|
|
11
|
+
export * as terminalwire from "./terminalwire/codec.ts";
|
|
7
12
|
export type * from "./generated/feed.ts";
|
|
8
13
|
export type * from "./generated/kit.ts";
|
|
9
14
|
export type * from "./generated/shares.ts";
|
|
10
15
|
export type * from "./generated/workorder.ts";
|
|
11
16
|
export type * from "./generated/workorder-events.ts";
|
|
17
|
+
export type * from "./generated/wsstream.ts";
|
|
12
18
|
export { FEED_SCHEMAS } from "./generated/feed.ts";
|
|
13
19
|
export { KIT_ROUTE_PREFIX, KIT_ROUTE_TABLE, KIT_PHONE_PATH, KIT_SCHEMAS, KIT_SIGNOUT_PATH } from "./generated/kit.ts";
|
|
14
20
|
export { SHARES_SCHEMAS } from "./generated/shares.ts";
|
|
15
21
|
export { WORK_ORDER_SCHEMAS } from "./generated/workorder.ts";
|
|
16
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
CHANGED
|
@@ -4,12 +4,21 @@
|
|
|
4
4
|
// because the framework is the author's.
|
|
5
5
|
export { ROOT_BASE, ROUTE_PREFIX, appBase, routePath, underBase } from "./routes.js";
|
|
6
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";
|
|
7
9
|
export { TeraClient, KitRequestError, UNRECOGNIZED_RESPONSE, } from "./client.js";
|
|
8
10
|
export { STEP_UP_HEADER, STEP_UP_POLL_MS, STEP_UP_REQUIRED, STEP_UP_RESUME_PARAM, StepUpError, resumedStepUp, stepUpChallengeOf, stepUpPageURL, } from "./stepup.js";
|
|
9
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";
|
|
10
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";
|
|
11
19
|
export { FEED_SCHEMAS } from "./generated/feed.js";
|
|
12
20
|
export { KIT_ROUTE_PREFIX, KIT_ROUTE_TABLE, KIT_PHONE_PATH, KIT_SCHEMAS, KIT_SIGNOUT_PATH } from "./generated/kit.js";
|
|
13
21
|
export { SHARES_SCHEMAS } from "./generated/shares.js";
|
|
14
22
|
export { WORK_ORDER_SCHEMAS } from "./generated/workorder.js";
|
|
15
23
|
export { WORK_ORDER_EVENT_SCHEMAS } from "./generated/workorder-events.js";
|
|
24
|
+
export { STREAM_CLOSE_CODES, STREAM_SCHEMAS } from "./generated/wsstream.js";
|
package/dist/signin.d.ts
ADDED
|
@@ -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,145 @@
|
|
|
1
|
+
import { type Clock, type FeedSignals } from "../feed/connection.ts";
|
|
2
|
+
/** Immediate reconnects allowed in {@link IMMEDIATE_WINDOW_MS} before the next is taken with backoff. */
|
|
3
|
+
export declare const IMMEDIATE_LIMIT = 3;
|
|
4
|
+
export declare const IMMEDIATE_WINDOW_MS = 30000;
|
|
5
|
+
/** How long a socket stays open before the backoff resets. */
|
|
6
|
+
export declare const STABLE_OPEN_MS = 10000;
|
|
7
|
+
/** The most a page lets wait unsent before it treats the socket as a defect (ADR-181 D6 item 5). */
|
|
8
|
+
export declare const MAX_BUFFERED_BYTES: number;
|
|
9
|
+
/** The close reason the connection sends when it closes a socket for {@link MAX_BUFFERED_BYTES}. */
|
|
10
|
+
export declare const SEND_BUFFER_EXCEEDED = "send_buffer_exceeded";
|
|
11
|
+
/**
|
|
12
|
+
* The part of the browser's `WebSocket` the connection uses; a real one
|
|
13
|
+
* satisfies it.
|
|
14
|
+
*/
|
|
15
|
+
export interface WebSocketLike {
|
|
16
|
+
binaryType: string;
|
|
17
|
+
readonly bufferedAmount: number;
|
|
18
|
+
onopen: ((ev: unknown) => void) | null;
|
|
19
|
+
onmessage: ((ev: {
|
|
20
|
+
data: unknown;
|
|
21
|
+
}) => void) | null;
|
|
22
|
+
onclose: ((ev: {
|
|
23
|
+
code: number;
|
|
24
|
+
reason: string;
|
|
25
|
+
}) => void) | null;
|
|
26
|
+
onerror: ((ev: unknown) => void) | null;
|
|
27
|
+
send(data: ArrayBufferView | ArrayBuffer): void;
|
|
28
|
+
close(code?: number, reason?: string): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Where the connection is.
|
|
32
|
+
*
|
|
33
|
+
* connecting a socket is being opened; attempt 0 is the first, and an
|
|
34
|
+
* immediate reconnect keeps the attempt it had
|
|
35
|
+
* open the socket is open; messages flow
|
|
36
|
+
* reconnecting the socket ended and the next opens in `delayMs`
|
|
37
|
+
* ended nothing will be opened again; `code` is the close that ended
|
|
38
|
+
* it, and `reason` its reason, or what the probe answered
|
|
39
|
+
*/
|
|
40
|
+
export type StreamConnectionState = {
|
|
41
|
+
readonly phase: "connecting";
|
|
42
|
+
readonly attempt: number;
|
|
43
|
+
} | {
|
|
44
|
+
readonly phase: "open";
|
|
45
|
+
} | {
|
|
46
|
+
readonly phase: "reconnecting";
|
|
47
|
+
readonly attempt: number;
|
|
48
|
+
readonly delayMs: number;
|
|
49
|
+
readonly reason: string;
|
|
50
|
+
} | {
|
|
51
|
+
readonly phase: "ended";
|
|
52
|
+
readonly code: number;
|
|
53
|
+
readonly reason: string;
|
|
54
|
+
};
|
|
55
|
+
/** What a normal closure (1000) of the current socket does, as the protocol above decided. */
|
|
56
|
+
export type NormalCloseNext = "reconnect" | "backoff" | "end";
|
|
57
|
+
export interface StreamConnectionOptions {
|
|
58
|
+
/** The stream route's path, built with {@link routePath}. */
|
|
59
|
+
path: string;
|
|
60
|
+
/** The one subprotocol the route speaks. */
|
|
61
|
+
subprotocol: string;
|
|
62
|
+
/** Read at every connect, so a resume offset in it is current. */
|
|
63
|
+
query?: () => URLSearchParams;
|
|
64
|
+
/** Each message, whole. A text message arrives as its UTF-8 bytes. */
|
|
65
|
+
onMessage: (data: ArrayBuffer) => void;
|
|
66
|
+
/** Every change of {@link StreamConnectionState}. */
|
|
67
|
+
onState?: (state: StreamConnectionState) => void;
|
|
68
|
+
/** The path the app is served under on its origin, as a client's `base` option names it; `/` when absent. */
|
|
69
|
+
base?: string;
|
|
70
|
+
/** Opens one socket; the browser's `WebSocket` when absent. */
|
|
71
|
+
socket?: (url: string, protocol: string) => WebSocketLike;
|
|
72
|
+
/** Sends the handshake's probe; the global `fetch` when absent. */
|
|
73
|
+
fetch?: typeof fetch;
|
|
74
|
+
/**
|
|
75
|
+
* Loads the page again when the probe answers `sign_in_required`;
|
|
76
|
+
* {@link reSignIn} when absent. A {@link TeraClient}'s stream passes the
|
|
77
|
+
* client's own, so the page is loaded once per client.
|
|
78
|
+
*/
|
|
79
|
+
reSignIn?: () => void;
|
|
80
|
+
clock?: Clock;
|
|
81
|
+
/** The window's `online` event when absent; only `online` is acted on. */
|
|
82
|
+
signals?: FeedSignals;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* webSocketURL resolves a path on the page's origin to the `ws:` or `wss:` URL
|
|
86
|
+
* a `WebSocket` is opened with. With no page URL — outside a browser — the
|
|
87
|
+
* path is answered as it is.
|
|
88
|
+
*/
|
|
89
|
+
export declare function webSocketURL(path: string, pageURL: string | undefined): string;
|
|
90
|
+
export declare class StreamConnection {
|
|
91
|
+
private readonly options;
|
|
92
|
+
private readonly clock;
|
|
93
|
+
private readonly openSocket;
|
|
94
|
+
private view;
|
|
95
|
+
private unsubscribe;
|
|
96
|
+
private generation;
|
|
97
|
+
private ws;
|
|
98
|
+
private opened;
|
|
99
|
+
private retryTimer;
|
|
100
|
+
private stableTimer;
|
|
101
|
+
private attempt;
|
|
102
|
+
private immediate;
|
|
103
|
+
private normalNext;
|
|
104
|
+
/** Opens the first socket. */
|
|
105
|
+
constructor(options: StreamConnectionOptions);
|
|
106
|
+
get state(): StreamConnectionState;
|
|
107
|
+
/** The socket's URL as of now: the route under the app's base, with the query read afresh. */
|
|
108
|
+
get url(): string;
|
|
109
|
+
/** What the open socket has not yet handed to the network; 0 with none open. */
|
|
110
|
+
get bufferedAmount(): number;
|
|
111
|
+
/**
|
|
112
|
+
* Sends one message, answering whether it was handed to an open socket. A
|
|
113
|
+
* send that leaves more than {@link MAX_BUFFERED_BYTES} unsent closes the
|
|
114
|
+
* socket and reconnects with backoff.
|
|
115
|
+
*/
|
|
116
|
+
send(message: ArrayBufferView): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Says what the normal closure (1000) that follows does: the protocol above
|
|
119
|
+
* read why the far side is ending, which the close code does not carry
|
|
120
|
+
* (ADR-181 D9 item 2). It holds for the current socket only; each new socket
|
|
121
|
+
* starts at `"end"`.
|
|
122
|
+
*/
|
|
123
|
+
onNormalClose(next: NormalCloseNext): void;
|
|
124
|
+
/**
|
|
125
|
+
* Gives up on the open socket: closes it 1000 with `reason` and reconnects
|
|
126
|
+
* at once, within the ration. It is how the protocol above acts on a
|
|
127
|
+
* connection it finds dead, which a page cannot ping. With no socket open
|
|
128
|
+
* it does nothing.
|
|
129
|
+
*/
|
|
130
|
+
reconnect(reason: string): void;
|
|
131
|
+
/** The page's own end: closes the socket 1000, and nothing is opened again. */
|
|
132
|
+
close(): void;
|
|
133
|
+
private set;
|
|
134
|
+
private teardown;
|
|
135
|
+
private connect;
|
|
136
|
+
private closed;
|
|
137
|
+
/** Settles a failed handshake with one same-origin request to the kit's `me` route. */
|
|
138
|
+
private probe;
|
|
139
|
+
/** Reconnects at once, within the ration; past it, with backoff. */
|
|
140
|
+
private reconnectNow;
|
|
141
|
+
private backoff;
|
|
142
|
+
private end;
|
|
143
|
+
/** Closes a socket the connection is giving up on, and stops listening to it. */
|
|
144
|
+
private drop;
|
|
145
|
+
}
|