@kottera/tera-web 0.1.0-rc.3 → 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.
@@ -10,16 +10,20 @@ 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 = "/_tera/signout";
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";
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";
19
22
  /** The kit's route table (ADR-162 D3), as sdk/plugin/webui declares it. */
20
23
  export const KIT_ROUTE_TABLE = {
21
24
  prefix: "/tera/v1/",
22
- signout_path: "/_tera/signout",
25
+ signout_path: "_tera/signout",
26
+ phone_path: "_tera/phone",
23
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.",
24
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.",
25
29
  error: {
@@ -32,7 +36,7 @@ export const KIT_ROUTE_TABLE = {
32
36
  name: "me",
33
37
  method: "GET",
34
38
  path: "/tera/v1/me",
35
- 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.",
36
40
  same_origin: false,
37
41
  response: {
38
42
  media_type: "application/json",
@@ -144,5 +148,24 @@ export const KIT_ROUTE_TABLE = {
144
148
  description: "The staged artifact.",
145
149
  },
146
150
  },
151
+ {
152
+ name: "step_up",
153
+ method: "GET",
154
+ path: "/tera/v1/step-ups/{id}",
155
+ 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.",
156
+ same_origin: false,
157
+ path_params: [
158
+ {
159
+ name: "id",
160
+ description: "The step-up's id, as a step_up_required refusal's step_up names it.",
161
+ required: true,
162
+ },
163
+ ],
164
+ response: {
165
+ media_type: "application/json",
166
+ schema: "https://kottera.dev/schemas/plugin/webui/step-up.response/v1",
167
+ description: "The step-up's state.",
168
+ },
169
+ },
147
170
  ],
148
171
  };
@@ -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;
@@ -1383,6 +1404,9 @@ export type WorkOrderGetResult = {
1383
1404
  } | null;
1384
1405
  created_by?: string;
1385
1406
  created_via?: string;
1407
+ arrival_evidence?: {
1408
+ [key: string]: string;
1409
+ };
1386
1410
  created_at: string;
1387
1411
  updated_at: string;
1388
1412
  off_tera: boolean;
@@ -1785,6 +1809,12 @@ export type WorkOrderListResult = {
1785
1809
  describe?: string;
1786
1810
  caller_is_person?: boolean;
1787
1811
  outcome_provenance?: string;
1812
+ step_up?: {
1813
+ id: string;
1814
+ method: string;
1815
+ anchor?: string;
1816
+ at: string;
1817
+ };
1788
1818
  };
1789
1819
  }[] | null;
1790
1820
  };
@@ -1818,6 +1848,12 @@ export type WorkOrderListOffTeraResult = {
1818
1848
  describe?: string;
1819
1849
  caller_is_person?: boolean;
1820
1850
  outcome_provenance?: string;
1851
+ step_up?: {
1852
+ id: string;
1853
+ method: string;
1854
+ anchor?: string;
1855
+ at: string;
1856
+ };
1821
1857
  };
1822
1858
  take_age_seconds?: number;
1823
1859
  /** 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 +1876,12 @@ export type WorkOrderListOffTeraResult = {
1840
1876
  describe?: string;
1841
1877
  caller_is_person?: boolean;
1842
1878
  outcome_provenance?: string;
1879
+ step_up?: {
1880
+ id: string;
1881
+ method: string;
1882
+ anchor?: string;
1883
+ at: string;
1884
+ };
1843
1885
  };
1844
1886
  take_age_seconds?: number;
1845
1887
  /** 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 +1904,12 @@ export type WorkOrderListOffTeraResult = {
1862
1904
  describe?: string;
1863
1905
  caller_is_person?: boolean;
1864
1906
  outcome_provenance?: string;
1907
+ step_up?: {
1908
+ id: string;
1909
+ method: string;
1910
+ anchor?: string;
1911
+ at: string;
1912
+ };
1865
1913
  };
1866
1914
  take_age_seconds?: number;
1867
1915
  /** 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 +2010,12 @@ export type WorkOrderProgressResult = {
1962
2010
  describe?: string;
1963
2011
  caller_is_person?: boolean;
1964
2012
  outcome_provenance?: string;
2013
+ step_up?: {
2014
+ id: string;
2015
+ method: string;
2016
+ anchor?: string;
2017
+ at: string;
2018
+ };
1965
2019
  } | null;
1966
2020
  addressed_events: {
1967
2021
  id: number;
@@ -2550,6 +2604,8 @@ export type WorkOrderSubmitResultParams = {
2550
2604
  };
2551
2605
  /** 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
2606
  seen_through?: number;
2607
+ /** 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. */
2608
+ step_up?: string;
2553
2609
  };
2554
2610
  /**
2555
2611
  * `work_order.submit_result` result (v1).
@@ -2565,6 +2621,12 @@ export type WorkOrderSubmitResultResult = {
2565
2621
  outcome?: {
2566
2622
  outcome_provenance: string;
2567
2623
  attested_by?: string;
2624
+ step_up?: {
2625
+ id: string;
2626
+ method: string;
2627
+ anchor?: string;
2628
+ at: string;
2629
+ };
2568
2630
  };
2569
2631
  checks?: string;
2570
2632
  addressed_events: {
@@ -2612,6 +2674,12 @@ export type WorkOrderTakeResult = {
2612
2674
  describe?: string;
2613
2675
  caller_is_person?: boolean;
2614
2676
  outcome_provenance?: string;
2677
+ step_up?: {
2678
+ id: string;
2679
+ method: string;
2680
+ anchor?: string;
2681
+ at: string;
2682
+ };
2615
2683
  } | null;
2616
2684
  instructions: string;
2617
2685
  requirements?: string;
@@ -2675,6 +2743,8 @@ export type WorkOrderTransitionParams = {
2675
2743
  replace?: boolean;
2676
2744
  /** 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
2745
  reselect?: boolean;
2746
+ /** 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. */
2747
+ step_up?: string;
2678
2748
  /** action=answer only: the operator's answer text. Read work_order.get(id).question first — that is what is being asked. */
2679
2749
  text?: string;
2680
2750
  };
@@ -3102,6 +3172,12 @@ export type WorkOrderTreeResult = {
3102
3172
  outcome?: {
3103
3173
  outcome_provenance: string;
3104
3174
  attested_by?: string;
3175
+ step_up?: {
3176
+ id: string;
3177
+ method: string;
3178
+ anchor?: string;
3179
+ at: string;
3180
+ };
3105
3181
  };
3106
3182
  replaced?: {
3107
3183
  relation: string;
@@ -3326,6 +3402,12 @@ export type WorkOrderTreeResult = {
3326
3402
  outcome?: {
3327
3403
  outcome_provenance: string;
3328
3404
  attested_by?: string;
3405
+ step_up?: {
3406
+ id: string;
3407
+ method: string;
3408
+ anchor?: string;
3409
+ at: string;
3410
+ };
3329
3411
  };
3330
3412
  replaced?: {
3331
3413
  relation: string;
package/dist/index.d.ts CHANGED
@@ -1,12 +1,16 @@
1
- export { ROUTE_PREFIX, routePath } from "./routes.ts";
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";
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";
3
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";
4
6
  export { FeedOpenError, SSEParser, fetchOpener, isRefusal, type FeedOpener, type SSEEvent } from "./feed/sse.ts";
5
7
  export type * from "./generated/feed.ts";
6
8
  export type * from "./generated/kit.ts";
9
+ export type * from "./generated/shares.ts";
7
10
  export type * from "./generated/workorder.ts";
8
11
  export type * from "./generated/workorder-events.ts";
9
12
  export { FEED_SCHEMAS } from "./generated/feed.ts";
10
- 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";
14
+ export { SHARES_SCHEMAS } from "./generated/shares.ts";
11
15
  export { WORK_ORDER_SCHEMAS } from "./generated/workorder.ts";
12
16
  export { WORK_ORDER_EVENT_SCHEMAS } from "./generated/workorder-events.ts";
package/dist/index.js CHANGED
@@ -2,11 +2,14 @@
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
+ export { phoneURL } from "./phone.js";
6
7
  export { TeraClient, KitRequestError, UNRECOGNIZED_RESPONSE, } from "./client.js";
8
+ export { STEP_UP_HEADER, STEP_UP_POLL_MS, STEP_UP_REQUIRED, STEP_UP_RESUME_PARAM, StepUpError, resumedStepUp, stepUpChallengeOf, stepUpPageURL, } from "./stepup.js";
7
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";
8
10
  export { FeedOpenError, SSEParser, fetchOpener, isRefusal } from "./feed/sse.js";
9
11
  export { FEED_SCHEMAS } from "./generated/feed.js";
10
- 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";
13
+ export { SHARES_SCHEMAS } from "./generated/shares.js";
11
14
  export { WORK_ORDER_SCHEMAS } from "./generated/workorder.js";
12
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
@@ -8,3 +8,25 @@ 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;
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
@@ -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,34 @@ 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
+ }
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
+ }