iterate 0.2.6 → 0.3.0

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 (218) hide show
  1. package/README.md +86 -76
  2. package/THIRD_PARTY_NOTICES.md +55 -0
  3. package/bin/iterate.js +18 -3
  4. package/dist/api-url-B6404M82.mjs +17 -0
  5. package/dist/api-url-B6404M82.mjs.map +1 -0
  6. package/dist/app-ref-BipL0feU.mjs +35 -0
  7. package/dist/app-ref-BipL0feU.mjs.map +1 -0
  8. package/dist/app-ref-C1CrgXqX.mjs +7 -0
  9. package/dist/app-ref-C1CrgXqX.mjs.map +1 -0
  10. package/dist/app-ref-DYai_om1.mjs +7 -0
  11. package/dist/app-ref-DYai_om1.mjs.map +1 -0
  12. package/dist/cli-D0c-pDL_.mjs +1010 -0
  13. package/dist/cli-D0c-pDL_.mjs.map +1 -0
  14. package/dist/client.d.ts +3 -0
  15. package/dist/client.mjs +4 -0
  16. package/dist/cloudflare-BTm90gQ4.mjs +951 -0
  17. package/dist/cloudflare-BTm90gQ4.mjs.map +1 -0
  18. package/dist/contract-s4FW4eES.mjs +309 -0
  19. package/dist/contract-s4FW4eES.mjs.map +1 -0
  20. package/dist/document-review/index.d.ts +5 -0
  21. package/dist/document-review/types.d.ts +107 -0
  22. package/dist/document-review.mjs +7015 -0
  23. package/dist/document-review.mjs.map +1 -0
  24. package/dist/durable-object-processor-durability-CNsTjAJS.mjs +205 -0
  25. package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +1 -0
  26. package/dist/idempotency-DleloJNt.mjs +28 -0
  27. package/dist/idempotency-DleloJNt.mjs.map +1 -0
  28. package/dist/index.mjs +1 -1
  29. package/dist/itx/api-url.d.ts +6 -0
  30. package/dist/itx/itx-node-client.d.ts +65 -0
  31. package/dist/itx/itx-session.d.ts +215 -0
  32. package/dist/itx/owned-rpc-session.d.ts +14 -0
  33. package/dist/itx/query-client.d.ts +10 -0
  34. package/dist/itx-api.generated.d.ts +6195 -0
  35. package/dist/itx-session-sjud8GiT.mjs +534 -0
  36. package/dist/itx-session-sjud8GiT.mjs.map +1 -0
  37. package/dist/live-state-BJNqOwFw.mjs +299 -0
  38. package/dist/live-state-BJNqOwFw.mjs.map +1 -0
  39. package/dist/next/api.d.ts +479 -0
  40. package/dist/next/api.mjs +0 -0
  41. package/dist/next/app-server.d.ts +44 -0
  42. package/dist/next/app-server.mjs +479 -0
  43. package/dist/next/app-server.mjs.map +1 -0
  44. package/dist/next/app-session.d.ts +49 -0
  45. package/dist/next/app-session.mjs +238 -0
  46. package/dist/next/app-session.mjs.map +1 -0
  47. package/dist/next/app.d.ts +29 -0
  48. package/dist/next/app.mjs +141 -0
  49. package/dist/next/app.mjs.map +1 -0
  50. package/dist/next/client/live-state.d.ts +63 -0
  51. package/dist/next/client/oauth.d.ts +12 -0
  52. package/dist/next/client/react.d.ts +109 -0
  53. package/dist/next/client/socket.d.ts +6 -0
  54. package/dist/next/client.mjs +156 -0
  55. package/dist/next/client.mjs.map +1 -0
  56. package/dist/next/expression.d.ts +146 -0
  57. package/dist/next/expression.mjs +399 -0
  58. package/dist/next/expression.mjs.map +1 -0
  59. package/dist/next/lib.d.ts +56 -0
  60. package/dist/next/lib.mjs +199 -0
  61. package/dist/next/lib.mjs.map +1 -0
  62. package/dist/next/oauth-scopes.d.ts +32 -0
  63. package/dist/next/oauth-scopes.mjs +40 -0
  64. package/dist/next/oauth-scopes.mjs.map +1 -0
  65. package/dist/next/oauth.mjs +29 -0
  66. package/dist/next/oauth.mjs.map +1 -0
  67. package/dist/next/principal.d.ts +64 -0
  68. package/dist/next/principal.mjs +98 -0
  69. package/dist/next/principal.mjs.map +1 -0
  70. package/dist/next/project-ingress.d.ts +37 -0
  71. package/dist/next/project-ingress.mjs +75 -0
  72. package/dist/next/project-ingress.mjs.map +1 -0
  73. package/dist/next/react.mjs +285 -0
  74. package/dist/next/react.mjs.map +1 -0
  75. package/dist/next/sdk/auth.d.ts +5 -0
  76. package/dist/next/sdk/index.d.ts +112 -0
  77. package/dist/next/sdk.mjs +139 -0
  78. package/dist/next/sdk.mjs.map +1 -0
  79. package/dist/next/stream/processor.d.ts +378 -0
  80. package/dist/next/stream/processor.mjs +582 -0
  81. package/dist/next/stream/processor.mjs.map +1 -0
  82. package/dist/next/stream/run.d.ts +58 -0
  83. package/dist/next/stream/run.mjs +40 -0
  84. package/dist/next/stream/run.mjs.map +1 -0
  85. package/dist/next-node.d.ts +15 -0
  86. package/dist/next-node.mjs +51 -0
  87. package/dist/next-node.mjs.map +1 -0
  88. package/dist/node.d.ts +3 -0
  89. package/dist/node.mjs +185 -0
  90. package/dist/node.mjs.map +1 -0
  91. package/dist/processor-host-capabilities-BMFH3KTM.mjs +56 -0
  92. package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +1 -0
  93. package/dist/processors/cloudflare.d.ts +3 -0
  94. package/dist/processors/durable-object-processor-durability.d.ts +79 -0
  95. package/dist/processors/event-consumption-metrics.d.ts +82 -0
  96. package/dist/processors/idempotency.d.ts +13 -0
  97. package/dist/processors/index.d.ts +12 -0
  98. package/dist/processors/processor-contracts.d.ts +342 -0
  99. package/dist/processors/processor-facet.d.ts +186 -0
  100. package/dist/processors/processor-host-capabilities.d.ts +60 -0
  101. package/dist/processors/prompt-sections.d.ts +17 -0
  102. package/dist/processors/rpc-types.d.ts +515 -0
  103. package/dist/processors/schemas.d.ts +102 -0
  104. package/dist/processors/stream-handle.d.ts +45 -0
  105. package/dist/processors/stream-processor-keepalive.d.ts +95 -0
  106. package/dist/processors/stream-processor-registry.d.ts +233 -0
  107. package/dist/processors/stream-processor-runner.d.ts +289 -0
  108. package/dist/processors/stream-processor.d.ts +339 -0
  109. package/dist/processors/stream-runtime-metrics.d.ts +107 -0
  110. package/dist/processors/testing.d.ts +302 -0
  111. package/dist/processors-BoNyeBfQ.mjs +10 -0
  112. package/dist/processors-BoNyeBfQ.mjs.map +1 -0
  113. package/dist/processors-cloudflare.mjs +3 -0
  114. package/dist/processors-testing.mjs +435 -0
  115. package/dist/processors-testing.mjs.map +1 -0
  116. package/dist/processors.mjs +52 -0
  117. package/dist/processors.mjs.map +1 -0
  118. package/dist/protocol-DnK_f2m6.mjs +251 -0
  119. package/dist/protocol-DnK_f2m6.mjs.map +1 -0
  120. package/dist/sdk/capnweb/index.d.ts +2 -0
  121. package/dist/sdk/capnweb/live-state/compact.d.ts +5 -0
  122. package/dist/sdk/capnweb/live-state/diff.d.ts +41 -0
  123. package/dist/sdk/capnweb/live-state/engine.d.ts +44 -0
  124. package/dist/sdk/capnweb/live-state/index.d.ts +41 -0
  125. package/dist/sdk/capnweb/live-state/protocol.d.ts +87 -0
  126. package/dist/sdk/capnweb/live-state/retain.d.ts +23 -0
  127. package/dist/sdk/capnweb/live-state/store.d.ts +20 -0
  128. package/dist/sdk/capnweb/live-state/types.d.ts +11 -0
  129. package/dist/sdk/capnweb/react.d.ts +45 -0
  130. package/dist/sdk/capnweb/react.mjs +316 -0
  131. package/dist/sdk/capnweb/react.mjs.map +1 -0
  132. package/dist/sdk/capnweb.mjs +4 -0
  133. package/dist/sdk/itx/react.d.ts +191 -0
  134. package/dist/sdk/itx/react.mjs +383 -0
  135. package/dist/sdk/itx/react.mjs.map +1 -0
  136. package/dist/sdk-DMB-IM11.mjs +933 -0
  137. package/dist/sdk-DMB-IM11.mjs.map +1 -0
  138. package/dist/sdk.d.ts +339 -0
  139. package/dist/sdk.mjs +2 -0
  140. package/dist/serve-itx.d.ts +46 -0
  141. package/dist/starter-apps/flake-dashboard/app-ref.d.ts +31 -0
  142. package/dist/starter-apps/flake-dashboard/configured-worker.mjs +1055 -0
  143. package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +1 -0
  144. package/dist/starter-apps/flake-dashboard/contract.d.ts +4839 -0
  145. package/dist/starter-apps/flake-dashboard/contract.mjs +2 -0
  146. package/dist/starter-apps/flake-dashboard/index.d.ts +17 -0
  147. package/dist/starter-apps/flake-dashboard/index.mjs +56 -0
  148. package/dist/starter-apps/flake-dashboard/index.mjs.map +1 -0
  149. package/dist/starter-apps/flake-dashboard/worker.d.ts +4607 -0
  150. package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +8914 -0
  151. package/dist/starter-apps/github-ai-linter/configured-worker.mjs +17987 -0
  152. package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +1 -0
  153. package/dist/starter-apps/github-ai-linter/contract.d.ts +9193 -0
  154. package/dist/starter-apps/github-ai-linter/index.d.ts +10 -0
  155. package/dist/starter-apps/github-ai-linter/index.mjs +36 -0
  156. package/dist/starter-apps/github-ai-linter/index.mjs.map +1 -0
  157. package/dist/starter-apps/github-ai-linter/prompt.d.ts +13 -0
  158. package/dist/starter-apps/github-ai-linter/review-bot.d.ts +808 -0
  159. package/dist/starter-apps/github-ai-linter/rules.d.ts +34 -0
  160. package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +19 -0
  161. package/dist/starter-apps/github-ai-linter/worker.d.ts +19 -0
  162. package/dist/starter-apps/github-ai-linter/worker.mjs +947 -0
  163. package/dist/starter-apps/github-ai-linter/worker.mjs.map +1 -0
  164. package/dist/starter-apps/guestbook/app-ref.d.ts +27 -0
  165. package/dist/starter-apps/guestbook/client.d.ts +7 -0
  166. package/dist/starter-apps/guestbook/client.mjs +59 -0
  167. package/dist/starter-apps/guestbook/configured-worker.mjs +205 -0
  168. package/dist/starter-apps/guestbook/configured-worker.mjs.map +1 -0
  169. package/dist/starter-apps/guestbook/index.d.ts +9 -0
  170. package/dist/starter-apps/guestbook/index.mjs +31 -0
  171. package/dist/starter-apps/guestbook/index.mjs.map +1 -0
  172. package/dist/starter-apps/guestbook/processor.d.ts +2267 -0
  173. package/dist/starter-apps/guestbook/worker.d.ts +26 -0
  174. package/dist/starter-apps/guestbook/worker.mjs +191 -0
  175. package/dist/starter-apps/guestbook/worker.mjs.map +1 -0
  176. package/dist/starter-apps/media/configured-worker.mjs +577 -0
  177. package/dist/starter-apps/media/configured-worker.mjs.map +1 -0
  178. package/dist/starter-apps/media/index.mjs +36 -0
  179. package/dist/starter-apps/media/index.mjs.map +1 -0
  180. package/dist/starter-apps/media/ref.mjs +20 -0
  181. package/dist/starter-apps/media/ref.mjs.map +1 -0
  182. package/dist/starter-apps/media/worker.mjs +579 -0
  183. package/dist/starter-apps/media/worker.mjs.map +1 -0
  184. package/dist/starter-apps/notes/configured-worker.mjs +6134 -0
  185. package/dist/starter-apps/notes/configured-worker.mjs.map +1 -0
  186. package/dist/starter-apps/notes/index.mjs +23 -0
  187. package/dist/starter-apps/notes/index.mjs.map +1 -0
  188. package/dist/starter-apps/notes/ref.mjs +21 -0
  189. package/dist/starter-apps/notes/ref.mjs.map +1 -0
  190. package/dist/starter-apps/notes/worker.mjs +427 -0
  191. package/dist/starter-apps/notes/worker.mjs.map +1 -0
  192. package/dist/starter-apps/todo/client.mjs +59 -0
  193. package/dist/starter-apps/todo/configured-worker.mjs +2864 -0
  194. package/dist/starter-apps/todo/configured-worker.mjs.map +1 -0
  195. package/dist/starter-apps/todo/index.d.ts +8 -0
  196. package/dist/starter-apps/todo/index.mjs +29 -0
  197. package/dist/starter-apps/todo/index.mjs.map +1 -0
  198. package/dist/stream-processor-keepalive-DAQTP6m3.mjs +2082 -0
  199. package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +1 -0
  200. package/dist/usingCtx-inzbY1Qz.mjs +57 -0
  201. package/dist/usingCtx-mZx5nsAW.mjs +11800 -0
  202. package/dist/usingCtx-mZx5nsAW.mjs.map +1 -0
  203. package/dist/worker-ref-DZxPDmb_.mjs +390 -0
  204. package/dist/worker-ref-DZxPDmb_.mjs.map +1 -0
  205. package/menubar/Iterate.entitlements +12 -0
  206. package/menubar/Iterate.swift +914 -0
  207. package/menubar/IterateIcon.swift +145 -0
  208. package/menubar/README.md +28 -0
  209. package/menubar/build-menubar-app.sh +59 -0
  210. package/package.json +235 -18
  211. package/dist/cli-DMS4kJph.mjs +0 -868
  212. package/dist/cli-DMS4kJph.mjs.map +0 -1
  213. package/dist/config-DtnR7Lv7.mjs +0 -170
  214. package/dist/config-DtnR7Lv7.mjs.map +0 -1
  215. package/dist/index.d.mts.map +0 -1
  216. package/dist/stream-tui/agent-chat-terminal.d.mts +0 -1
  217. package/dist/stream-tui/agent-chat-terminal.mjs +0 -933
  218. package/dist/stream-tui/agent-chat-terminal.mjs.map +0 -1
@@ -0,0 +1,238 @@
1
+ import { isLocalOrigin } from "./lib.mjs";
2
+ import { OAuthScopes } from "./oauth-scopes.mjs";
3
+ import { t as _usingCtx } from "../usingCtx-inzbY1Qz.mjs";
4
+ import { authorizationCodeRequest } from "./oauth.mjs";
5
+ import { DurableObject } from "cloudflare:workers";
6
+ import { z } from "zod";
7
+ import { newHttpBatchRpcSession } from "capnweb";
8
+ import * as oauth from "oauth4webapi";
9
+ //#region src/next/app-session.ts
10
+ /** Each app binds this same class. Token exchange and logout use the issuer's
11
+ * public protocol, so separately deployed apps need no platform bindings.
12
+ * The input gate serializes refresh and logout across tabs. */
13
+ var BrowserSession = class extends DurableObject {
14
+ begin(host, next) {
15
+ return this.#serial(async () => {
16
+ if (await this.ctx.storage.get("session")) throw new Error("Browser session already exists");
17
+ const origin = new URL(host.origin);
18
+ const local = isLocalOrigin(host.origin);
19
+ if (origin.protocol !== "https:" && !local) throw new Error("Browser login requires HTTPS");
20
+ let clientId = host.client?.id || `${host.origin}/.auth/client.json`;
21
+ if (local) {
22
+ const response = await fetch(`${host.issuer}/oauth2/register`, {
23
+ method: "POST",
24
+ headers: { "content-type": "application/json" },
25
+ body: JSON.stringify({
26
+ client_name: host.client?.name || origin.host,
27
+ client_uri: host.origin,
28
+ logo_uri: host.client?.logoUri,
29
+ redirect_uris: [`${host.origin}/.auth/callback`],
30
+ token_endpoint_auth_method: "none",
31
+ grant_types: ["authorization_code", "refresh_token"],
32
+ response_types: ["code"]
33
+ }),
34
+ signal: AbortSignal.timeout(1e4)
35
+ });
36
+ if (!response.ok) throw new Error(`Local client registration failed (${response.status})`);
37
+ clientId = z.object({ client_id: z.string().min(1) }).parse(await response.json()).client_id;
38
+ }
39
+ const { url, state, verifier } = await authorizationCodeRequest({
40
+ issuer: host.issuer,
41
+ clientId,
42
+ redirectUri: `${host.origin}/.auth/callback`,
43
+ resources: [host.resource],
44
+ scopes: host.scopes
45
+ });
46
+ const data = {
47
+ ...host,
48
+ next,
49
+ clientId,
50
+ phase: "pending",
51
+ state,
52
+ verifier,
53
+ until: Date.now() + 10 * 6e4
54
+ };
55
+ await this.ctx.storage.put("session", data);
56
+ await this.ctx.storage.setAlarm(data.until);
57
+ return url.href;
58
+ });
59
+ }
60
+ /** The issuer's redirect back to `/.auth/callback`, its query string. oauth4webapi validates it
61
+ * (the state matches this browser's pending flow, the `iss` matches the issuer, no `error`), then
62
+ * exchanges the code for tokens. */
63
+ complete(callbackQuery) {
64
+ return this.#serial(async () => {
65
+ const data = await this.ctx.storage.get("session");
66
+ if (!data || data.phase !== "pending" || data.until <= Date.now()) return { error: "This sign-in expired or does not match this browser. Start sign-in again." };
67
+ const as = {
68
+ issuer: data.issuer,
69
+ token_endpoint: `${data.issuer}/oauth2/token`,
70
+ authorization_response_iss_parameter_supported: true
71
+ };
72
+ const client = { client_id: data.clientId };
73
+ let callback;
74
+ try {
75
+ callback = oauth.validateAuthResponse(as, client, new URLSearchParams(callbackQuery), data.state);
76
+ } catch (error) {
77
+ if (error instanceof oauth.AuthorizationResponseError) {
78
+ await this.#clear();
79
+ return { error: "Authorization was declined." };
80
+ }
81
+ return { error: "This sign-in expired or does not match this browser. Start sign-in again." };
82
+ }
83
+ const started = Date.now();
84
+ let tokens;
85
+ try {
86
+ const response = await oauth.authorizationCodeGrantRequest(as, client, oauth.None(), callback, `${data.origin}/.auth/callback`, data.verifier, this.#tokenOptions(data));
87
+ tokens = await oauth.processAuthorizationCodeResponse(as, client, response);
88
+ } catch (error) {
89
+ await this.#endOnDeadGrant(error);
90
+ return { error: "Sign-in could not complete. Start sign-in again." };
91
+ }
92
+ await this.#activate(data, tokens, started);
93
+ return { next: data.next };
94
+ });
95
+ }
96
+ bearer() {
97
+ return this.#serial(() => this.#bearer());
98
+ }
99
+ /** THE ISSUER THIS SESSION IS BOUND TO and the resource its tokens are for — written once at
100
+ * `begin`, never steered by a request: the app's `/api` proxy, its login probe and its logout read
101
+ * them from here, so a browser connected to one issuer can only ever spend its credential there.
102
+ * Null when no session was begun. */
103
+ async host() {
104
+ const data = await this.ctx.storage.get("session");
105
+ return data ? {
106
+ issuer: data.issuer,
107
+ resource: data.resource
108
+ } : null;
109
+ }
110
+ async scopes() {
111
+ const data = await this.ctx.storage.get("session");
112
+ return data?.phase === "active" ? data.scopes : [];
113
+ }
114
+ /** Public client metadata chosen by the app before consent; never a browser-supplied claim. */
115
+ async client() {
116
+ const data = await this.ctx.storage.get("session");
117
+ return data?.phase === "active" ? data.client : void 0;
118
+ }
119
+ /** A verified 401 means this local credential no longer grants access. */
120
+ discard() {
121
+ return this.#serial(() => this.#clear());
122
+ }
123
+ end() {
124
+ return this.#serial(async () => {
125
+ const token = await this.#bearer();
126
+ const data = await this.ctx.storage.get("session");
127
+ if (token && data) {
128
+ const probe = await fetch(new Request(data.resource, {
129
+ method: "POST",
130
+ headers: { Authorization: `Bearer ${token}` },
131
+ body: "",
132
+ signal: AbortSignal.timeout(1e4)
133
+ }));
134
+ await probe.body?.cancel();
135
+ if (probe.status !== 401) try {
136
+ var _usingCtx$1 = _usingCtx();
137
+ if (!probe.ok) throw new Error(`Sign-out could not reach Iterate (${probe.status}). Try again.`);
138
+ const api = _usingCtx$1.u(newHttpBatchRpcSession(new Request(data.resource, {
139
+ headers: { Authorization: `Bearer ${token}` },
140
+ signal: AbortSignal.timeout(1e4)
141
+ })));
142
+ await _usingCtx$1.u(api.authenticate({ type: "from-server-cookie" })).logout();
143
+ } catch (_) {
144
+ _usingCtx$1.e = _;
145
+ } finally {
146
+ _usingCtx$1.d();
147
+ }
148
+ }
149
+ await this.#clear();
150
+ });
151
+ }
152
+ async alarm() {
153
+ await this.#clear();
154
+ }
155
+ async #bearer() {
156
+ const data = await this.ctx.storage.get("session");
157
+ if (!data || data.phase !== "active") return null;
158
+ if (data.until <= Date.now()) {
159
+ await this.#clear();
160
+ return null;
161
+ }
162
+ if (data.expiresAt > Date.now() + 3e4) return data.accessToken;
163
+ const as = {
164
+ issuer: data.issuer,
165
+ token_endpoint: `${data.issuer}/oauth2/token`
166
+ };
167
+ const client = { client_id: data.clientId };
168
+ const started = Date.now();
169
+ let tokens;
170
+ try {
171
+ const response = await oauth.refreshTokenGrantRequest(as, client, oauth.None(), data.refreshToken, this.#tokenOptions(data));
172
+ tokens = await oauth.processRefreshTokenResponse(as, client, response);
173
+ } catch (error) {
174
+ await this.#endOnDeadGrant(error);
175
+ return null;
176
+ }
177
+ return (await this.#activate(data, tokens, started)).accessToken;
178
+ }
179
+ /** The token request's shared options: the audience (RFC 8707 resource), a bounded timeout, and —
180
+ * only for a LOCAL APP (`begin` already admits http there) — oauth4webapi's opt-out of its
181
+ * HTTPS-only default. Keyed on the app's origin, never the issuer's: an issuer a person typed can
182
+ * not talk a deployed app into sending its codes in the clear. */
183
+ #tokenOptions(data) {
184
+ const options = {
185
+ additionalParameters: { resource: data.resource },
186
+ signal: AbortSignal.timeout(1e4)
187
+ };
188
+ if (isLocalOrigin(data.origin)) options[oauth.allowInsecureRequests] = true;
189
+ return options;
190
+ }
191
+ /** Write the active session from a token response. A refresh reuses the prior refresh token when
192
+ * the issuer does not rotate it. */
193
+ async #activate(data, tokens, started) {
194
+ const refreshToken = tokens.refresh_token || (data.phase === "active" ? data.refreshToken : void 0);
195
+ if (!refreshToken) throw new Error("Iterate returned no refresh token.");
196
+ const { origin, issuer, resource, clientId, next } = data;
197
+ const stored = {
198
+ origin,
199
+ issuer,
200
+ resource,
201
+ clientId,
202
+ client: data.client,
203
+ next,
204
+ phase: "active",
205
+ scopes: tokens.scope ? OAuthScopes.parse(tokens.scope.split(" ").filter(Boolean)) : data.scopes,
206
+ accessToken: tokens.access_token,
207
+ refreshToken,
208
+ expiresAt: started + (tokens.expires_in ?? 0) * 1e3,
209
+ until: data.phase === "pending" ? started + 720 * 36e5 : data.until
210
+ };
211
+ await this.ctx.storage.put("session", stored);
212
+ await this.ctx.storage.setAlarm(stored.until);
213
+ return stored;
214
+ }
215
+ /** A dead code or refresh token (`invalid_grant`) ends the session; other failures are transient. */
216
+ async #endOnDeadGrant(error) {
217
+ if (error instanceof oauth.ResponseBodyError && error.error === "invalid_grant") {
218
+ await this.#clear();
219
+ return;
220
+ }
221
+ throw error instanceof oauth.ResponseBodyError ? /* @__PURE__ */ new Error(`Iterate token exchange failed (${error.status}). Try again.`) : error;
222
+ }
223
+ /** An operation failure must not reset the DO or fail other tabs' requests. */
224
+ #serial(work) {
225
+ return this.ctx.blockConcurrencyWhile(() => work().then((value) => ({ value }), (error) => ({ error }))).then((result) => {
226
+ if ("error" in result) throw result.error;
227
+ return result.value;
228
+ });
229
+ }
230
+ async #clear() {
231
+ await this.ctx.storage.deleteAll();
232
+ await this.ctx.storage.deleteAlarm();
233
+ }
234
+ };
235
+ //#endregion
236
+ export { BrowserSession };
237
+
238
+ //# sourceMappingURL=app-session.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-session.mjs","names":["#serial","#clear","#tokenOptions","#endOnDeadGrant","#activate","#bearer"],"sources":["../../src/next/app-session.ts"],"sourcesContent":["import { DurableObject } from \"cloudflare:workers\";\n// eslint-disable-next-line iterate/no-capnweb-http-batch -- One bounded logout request, no live capabilities.\nimport { newHttpBatchRpcSession } from \"capnweb\";\nimport * as oauth from \"oauth4webapi\";\nimport { z } from \"zod\";\nimport { authorizationCodeRequest } from \"./client/oauth.ts\";\nimport { OAuthScopes } from \"./oauth-scopes.ts\";\nimport { isLocalOrigin } from \"./lib.ts\";\nimport type { IterateApi } from \"./api.ts\";\n\nexport type BrowserHost = {\n origin: string;\n issuer: string;\n resource: string;\n scopes: string[];\n client?: { id?: string; name: string; logoUri: string };\n};\ntype Base = BrowserHost & { clientId: string; next: string; until: number };\ntype Pending = Base & { phase: \"pending\"; state: string; verifier: string };\ntype Active = Base & {\n phase: \"active\";\n accessToken: string;\n refreshToken: string;\n expiresAt: number;\n};\ntype StoredSession = Pending | Active;\n\n/** Each app binds this same class. Token exchange and logout use the issuer's\n * public protocol, so separately deployed apps need no platform bindings.\n * The input gate serializes refresh and logout across tabs. */\nexport class BrowserSession extends DurableObject {\n begin(host: BrowserHost, next: string) {\n return this.#serial(async () => {\n if (await this.ctx.storage.get(\"session\")) throw new Error(\"Browser session already exists\");\n const origin = new URL(host.origin);\n const local = isLocalOrigin(host.origin);\n if (origin.protocol !== \"https:\" && !local) throw new Error(\"Browser login requires HTTPS\");\n let clientId = host.client?.id || `${host.origin}/.auth/client.json`;\n if (local) {\n const response = await fetch(`${host.issuer}/oauth2/register`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n client_name: host.client?.name || origin.host,\n client_uri: host.origin,\n logo_uri: host.client?.logoUri,\n redirect_uris: [`${host.origin}/.auth/callback`],\n token_endpoint_auth_method: \"none\",\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n }),\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) throw new Error(`Local client registration failed (${response.status})`);\n clientId = z\n .object({ client_id: z.string().min(1) })\n .parse(await response.json()).client_id;\n }\n const { url, state, verifier } = await authorizationCodeRequest({\n issuer: host.issuer,\n clientId,\n redirectUri: `${host.origin}/.auth/callback`,\n resources: [host.resource],\n scopes: host.scopes,\n });\n const data: Pending = {\n ...host,\n next,\n clientId,\n phase: \"pending\",\n state,\n verifier,\n until: Date.now() + 10 * 60_000,\n };\n await this.ctx.storage.put(\"session\", data);\n await this.ctx.storage.setAlarm(data.until);\n return url.href;\n });\n }\n\n /** The issuer's redirect back to `/.auth/callback`, its query string. oauth4webapi validates it\n * (the state matches this browser's pending flow, the `iss` matches the issuer, no `error`), then\n * exchanges the code for tokens. */\n complete(callbackQuery: string) {\n return this.#serial(async () => {\n const data = await this.ctx.storage.get<StoredSession>(\"session\");\n if (!data || data.phase !== \"pending\" || data.until <= Date.now())\n return {\n error: \"This sign-in expired or does not match this browser. Start sign-in again.\",\n };\n const as: oauth.AuthorizationServer = {\n issuer: data.issuer,\n token_endpoint: `${data.issuer}/oauth2/token`,\n authorization_response_iss_parameter_supported: true,\n };\n const client: oauth.Client = { client_id: data.clientId };\n let callback: URLSearchParams;\n try {\n callback = oauth.validateAuthResponse(\n as,\n client,\n new URLSearchParams(callbackQuery),\n data.state,\n );\n } catch (error) {\n // A provider `error=` (the person declined) ends the flow; a state/iss mismatch leaves the\n // pending flow to be retried from a genuine browser.\n if (error instanceof oauth.AuthorizationResponseError) {\n await this.#clear();\n return { error: \"Authorization was declined.\" };\n }\n return {\n error: \"This sign-in expired or does not match this browser. Start sign-in again.\",\n };\n }\n const started = Date.now();\n let tokens: oauth.TokenEndpointResponse;\n try {\n const response = await oauth.authorizationCodeGrantRequest(\n as,\n client,\n oauth.None(),\n callback,\n `${data.origin}/.auth/callback`,\n data.verifier,\n this.#tokenOptions(data),\n );\n tokens = await oauth.processAuthorizationCodeResponse(as, client, response);\n } catch (error) {\n await this.#endOnDeadGrant(error);\n return { error: \"Sign-in could not complete. Start sign-in again.\" };\n }\n await this.#activate(data, tokens, started);\n return { next: data.next };\n });\n }\n\n bearer() {\n return this.#serial(() => this.#bearer());\n }\n /** THE ISSUER THIS SESSION IS BOUND TO and the resource its tokens are for — written once at\n * `begin`, never steered by a request: the app's `/api` proxy, its login probe and its logout read\n * them from here, so a browser connected to one issuer can only ever spend its credential there.\n * Null when no session was begun. */\n async host(): Promise<{ issuer: string; resource: string } | null> {\n const data = await this.ctx.storage.get<StoredSession>(\"session\");\n return data ? { issuer: data.issuer, resource: data.resource } : null;\n }\n async scopes() {\n const data = await this.ctx.storage.get<StoredSession>(\"session\");\n return data?.phase === \"active\" ? data.scopes : [];\n }\n /** Public client metadata chosen by the app before consent; never a browser-supplied claim. */\n async client() {\n const data = await this.ctx.storage.get<StoredSession>(\"session\");\n return data?.phase === \"active\" ? data.client : undefined;\n }\n /** A verified 401 means this local credential no longer grants access. */\n discard() {\n return this.#serial(() => this.#clear());\n }\n\n end() {\n return this.#serial(async () => {\n const token = await this.#bearer();\n const data = await this.ctx.storage.get<StoredSession>(\"session\");\n if (token && data) {\n // Probe classifies a revoked/expired credential without interpreting RPC error text.\n const probe = await fetch(\n new Request(data.resource, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token}` },\n body: \"\",\n signal: AbortSignal.timeout(10_000),\n }),\n );\n await probe.body?.cancel();\n if (probe.status !== 401) {\n if (!probe.ok)\n throw new Error(`Sign-out could not reach Iterate (${probe.status}). Try again.`);\n // eslint-disable-next-line iterate/no-capnweb-http-batch -- A bounded logout command returns no live capabilities.\n using api = newHttpBatchRpcSession<IterateApi>(\n new Request(data.resource, {\n headers: { Authorization: `Bearer ${token}` },\n signal: AbortSignal.timeout(10_000),\n }),\n );\n // `authenticate(...)` returns a SessionRpcTarget STUB — its own RPC result to dispose (the\n // capnweb README's `using authedApi = api.authenticate(...)`); disposing only `api` leaves\n // it dangling and workerd warns the result was never disposed.\n using session = api.authenticate({ type: \"from-server-cookie\" });\n await session.logout();\n }\n }\n // Network failures preserve the session and are shown to the caller.\n // Successful logout has written the issuer's durable revocation marker.\n await this.#clear();\n });\n }\n\n async alarm() {\n // Pending flows expire in ten minutes; active grants have the same absolute\n // thirty-day lifetime at the issuer. No refresh token survives this bound.\n await this.#clear();\n }\n async #bearer() {\n const data = await this.ctx.storage.get<StoredSession>(\"session\");\n if (!data || data.phase !== \"active\") return null;\n if (data.until <= Date.now()) {\n await this.#clear();\n return null;\n }\n if (data.expiresAt > Date.now() + 30_000) return data.accessToken;\n const as: oauth.AuthorizationServer = {\n issuer: data.issuer,\n token_endpoint: `${data.issuer}/oauth2/token`,\n };\n const client: oauth.Client = { client_id: data.clientId };\n const started = Date.now();\n let tokens: oauth.TokenEndpointResponse;\n try {\n const response = await oauth.refreshTokenGrantRequest(\n as,\n client,\n oauth.None(),\n data.refreshToken,\n this.#tokenOptions(data),\n );\n tokens = await oauth.processRefreshTokenResponse(as, client, response);\n } catch (error) {\n await this.#endOnDeadGrant(error);\n return null;\n }\n return (await this.#activate(data, tokens, started)).accessToken;\n }\n\n /** The token request's shared options: the audience (RFC 8707 resource), a bounded timeout, and —\n * only for a LOCAL APP (`begin` already admits http there) — oauth4webapi's opt-out of its\n * HTTPS-only default. Keyed on the app's origin, never the issuer's: an issuer a person typed can\n * not talk a deployed app into sending its codes in the clear. */\n #tokenOptions(data: StoredSession): oauth.TokenEndpointRequestOptions {\n const options: oauth.TokenEndpointRequestOptions = {\n additionalParameters: { resource: data.resource },\n signal: AbortSignal.timeout(10_000),\n };\n if (isLocalOrigin(data.origin)) options[oauth.allowInsecureRequests] = true;\n return options;\n }\n\n /** Write the active session from a token response. A refresh reuses the prior refresh token when\n * the issuer does not rotate it. */\n async #activate(\n data: StoredSession,\n tokens: oauth.TokenEndpointResponse,\n started: number,\n ): Promise<Active> {\n const refreshToken =\n tokens.refresh_token || (data.phase === \"active\" ? data.refreshToken : undefined);\n if (!refreshToken) throw new Error(\"Iterate returned no refresh token.\");\n const { origin, issuer, resource, clientId, next } = data;\n const stored: Active = {\n origin,\n issuer,\n resource,\n clientId,\n client: data.client,\n next,\n phase: \"active\",\n // An omitted (or empty) `scope` means unchanged (RFC 6749 §5.1) — keep what the grant already\n // holds rather than silently narrow to `iterate`.\n scopes: tokens.scope\n ? OAuthScopes.parse(tokens.scope.split(\" \").filter(Boolean))\n : data.scopes,\n accessToken: tokens.access_token,\n refreshToken,\n expiresAt: started + (tokens.expires_in ?? 0) * 1000,\n until: data.phase === \"pending\" ? started + 30 * 24 * 3600_000 : data.until,\n };\n await this.ctx.storage.put(\"session\", stored);\n await this.ctx.storage.setAlarm(stored.until);\n return stored;\n }\n\n /** A dead code or refresh token (`invalid_grant`) ends the session; other failures are transient. */\n async #endOnDeadGrant(error: unknown): Promise<void> {\n if (error instanceof oauth.ResponseBodyError && error.error === \"invalid_grant\") {\n await this.#clear();\n return;\n }\n throw error instanceof oauth.ResponseBodyError\n ? new Error(`Iterate token exchange failed (${error.status}). Try again.`)\n : error;\n }\n /** An operation failure must not reset the DO or fail other tabs' requests. */\n #serial<T>(work: () => Promise<T>): Promise<T> {\n return this.ctx\n .blockConcurrencyWhile(() =>\n work().then(\n (value) => ({ value }),\n (error: unknown) => ({ error }),\n ),\n )\n .then((result) => {\n if (\"error\" in result) throw result.error;\n return result.value;\n });\n }\n\n async #clear() {\n await this.ctx.storage.deleteAll();\n await this.ctx.storage.deleteAlarm();\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { type RpcStub } from "capnweb";
2
+ import type { IterateSessionApi } from "./api.ts";
3
+ /** What `authenticate` resolves to: the session as a capnweb stub (pipelined; disposable) and the
4
+ * bootstrap info it answered with. Declared, so the package's declarations stay serializable. */
5
+ export type AuthenticatedApp = {
6
+ api: RpcStub<IterateSessionApi>;
7
+ info: ReturnType<IterateSessionApi["info"]>;
8
+ /** The page names a project this sign-in does not include — its `/projects/<ref>` is absent from
9
+ * `api.projects.list()`: leave for `/.auth/login`, whose page offers to sign in again (the
10
+ * project ticked at consent, or another account) and returns to this very URL. Never settles,
11
+ * like `authenticate`'s own leave for login. */
12
+ signInFor(project: string): Promise<never>;
13
+ };
14
+ export type IterateClient = {
15
+ authenticate(next?: string): Promise<AuthenticatedApp>;
16
+ };
17
+ /** Create once per app — a TanStack route's client-only `beforeLoad`, or a plain page's entry.
18
+ * Every loader and action of the page shares the returned public RPC session.
19
+ *
20
+ * Connecting tries for a while (client/socket.ts: ≈16 s of attempts) before an error reaches the
21
+ * page — a phone waking up or a flapping tunnel is not a reason to show "connection failed". And
22
+ * the `api` the page holds is a proxy to the CURRENT connection: when the socket closes, the next
23
+ * call opens a fresh one and pipelines onto it, so a dropped connection costs a reconnect, not the
24
+ * page. A reconnect the platform refuses (the session ended elsewhere) rejects that call; the
25
+ * page's retry runs `authenticate` again — a fresh one, the socket's close forgot the last — whose
26
+ * probe sends the browser to log in. */
27
+ export declare function createIterateClient(options?: {
28
+ scopes?: string[];
29
+ }): IterateClient;
@@ -0,0 +1,141 @@
1
+ import { OAuthScopes } from "./oauth-scopes.mjs";
2
+ import { newWebSocketRpcSession } from "capnweb";
3
+ //#region src/next/client/socket.ts
4
+ /** Open a WebSocket, trying again for a while when the connection fails: a phone waking up, a
5
+ * tunnel flapping, a cold edge — the things a first attempt trips over. Resolves with the socket
6
+ * once it is OPEN; rejects with the last failure after the last attempt. The delays are the
7
+ * waits BETWEEN attempts (the first is immediate). */
8
+ const RETRY_DELAYS_MS = [
9
+ 250,
10
+ 500,
11
+ 1e3,
12
+ 2e3,
13
+ 4e3,
14
+ 8e3
15
+ ];
16
+ function openSocketWithRetry(url, options = {}) {
17
+ const delays = options.delaysMs || RETRY_DELAYS_MS;
18
+ const Socket = options.WebSocket || WebSocket;
19
+ const sleep = options.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
20
+ const once = () => new Promise((resolve, reject) => {
21
+ const socket = new Socket(url);
22
+ const opened = () => {
23
+ socket.removeEventListener("close", failed);
24
+ resolve(socket);
25
+ };
26
+ const failed = () => {
27
+ socket.removeEventListener("open", opened);
28
+ reject(/* @__PURE__ */ new Error("WebSocket connection failed."));
29
+ };
30
+ socket.addEventListener("open", opened, { once: true });
31
+ socket.addEventListener("close", failed, { once: true });
32
+ });
33
+ return (async () => {
34
+ let attempt = 0;
35
+ for (;;) try {
36
+ return await once();
37
+ } catch (error) {
38
+ if (attempt >= delays.length) throw error;
39
+ await sleep(delays[attempt]);
40
+ attempt += 1;
41
+ }
42
+ })();
43
+ }
44
+ //#endregion
45
+ //#region src/next/app.ts
46
+ /** The browser leaves for the issuer's login (a document navigation) and this never settles — no
47
+ * framework in the loop: a TanStack `beforeLoad` awaiting it ends the way a thrown
48
+ * `redirect({ reloadDocument: true })` did, a plain page simply navigates. */
49
+ function leaveForLogin(login) {
50
+ window.location.assign(login);
51
+ return new Promise(() => {});
52
+ }
53
+ function connectionOn(socket) {
54
+ const iterate = newWebSocketRpcSession(socket);
55
+ return {
56
+ api: iterate.authenticate({ type: "from-server-cookie" }),
57
+ socket,
58
+ dispose: () => iterate[Symbol.dispose]()
59
+ };
60
+ }
61
+ /** Create once per app — a TanStack route's client-only `beforeLoad`, or a plain page's entry.
62
+ * Every loader and action of the page shares the returned public RPC session.
63
+ *
64
+ * Connecting tries for a while (client/socket.ts: ≈16 s of attempts) before an error reaches the
65
+ * page — a phone waking up or a flapping tunnel is not a reason to show "connection failed". And
66
+ * the `api` the page holds is a proxy to the CURRENT connection: when the socket closes, the next
67
+ * call opens a fresh one and pipelines onto it, so a dropped connection costs a reconnect, not the
68
+ * page. A reconnect the platform refuses (the session ended elsewhere) rejects that call; the
69
+ * page's retry runs `authenticate` again — a fresh one, the socket's close forgot the last — whose
70
+ * probe sends the browser to log in. */
71
+ function createIterateClient(options = {}) {
72
+ const scopes = OAuthScopes.parse(options.scopes || []);
73
+ let live = null;
74
+ let connecting;
75
+ const socketUrl = () => {
76
+ const url = new URL("/api", window.location.href);
77
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
78
+ return url;
79
+ };
80
+ /** Adopt a connection as the live one until its socket closes. A close also forgets the settled
81
+ * `authenticate` — the next one probes `/api` again, so a session that ended elsewhere sends the
82
+ * browser to log in instead of a retry that can only fail. */
83
+ function adopt(socket) {
84
+ const connection = connectionOn(socket);
85
+ live = connection;
86
+ const forget = () => {
87
+ if (live !== connection) return;
88
+ live = null;
89
+ connecting = void 0;
90
+ };
91
+ const dispose = () => {
92
+ forget();
93
+ connection.dispose();
94
+ };
95
+ socket.addEventListener("close", () => {
96
+ forget();
97
+ window.removeEventListener("pagehide", dispose);
98
+ }, { once: true });
99
+ window.addEventListener("pagehide", dispose, { once: true });
100
+ return connection;
101
+ }
102
+ const api = new Proxy({}, { get(_target, property) {
103
+ const connection = live || adopt(new WebSocket(socketUrl()));
104
+ return Reflect.get(connection.api, property);
105
+ } });
106
+ const loginUrl = (params) => `/.auth/login?${new URLSearchParams({
107
+ scope: scopes.join(" "),
108
+ ...params
109
+ })}`;
110
+ async function connect(next) {
111
+ const login = loginUrl({ next });
112
+ const probe = await fetch("/api", {
113
+ method: "POST",
114
+ body: "",
115
+ signal: AbortSignal.timeout(1e4)
116
+ });
117
+ await probe.body?.cancel();
118
+ if (probe.status === 401) return leaveForLogin(login);
119
+ if (!probe.ok) throw new Error(`iterate is unavailable (${probe.status}). Please retry.`);
120
+ if (!live) adopt(await openSocketWithRetry(socketUrl()));
121
+ return {
122
+ api,
123
+ info: await api.info(),
124
+ signInFor: (project) => leaveForLogin(loginUrl({
125
+ next: window.location.href,
126
+ project
127
+ }))
128
+ };
129
+ }
130
+ return { authenticate(next = "/") {
131
+ connecting ||= connect(next).catch((error) => {
132
+ connecting = void 0;
133
+ throw error;
134
+ });
135
+ return connecting;
136
+ } };
137
+ }
138
+ //#endregion
139
+ export { createIterateClient };
140
+
141
+ //# sourceMappingURL=app.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.mjs","names":[],"sources":["../../src/next/client/socket.ts","../../src/next/app.ts"],"sourcesContent":["/** Open a WebSocket, trying again for a while when the connection fails: a phone waking up, a\n * tunnel flapping, a cold edge — the things a first attempt trips over. Resolves with the socket\n * once it is OPEN; rejects with the last failure after the last attempt. The delays are the\n * waits BETWEEN attempts (the first is immediate). */\nconst RETRY_DELAYS_MS: readonly number[] = [250, 500, 1_000, 2_000, 4_000, 8_000];\n\nexport function openSocketWithRetry(\n url: string | URL,\n options: {\n delaysMs?: readonly number[];\n /** the constructor to use — a test's fake, `WebSocket` otherwise */\n WebSocket?: typeof WebSocket;\n sleep?: (ms: number) => Promise<void>;\n } = {},\n): Promise<WebSocket> {\n const delays = options.delaysMs || RETRY_DELAYS_MS;\n const Socket = options.WebSocket || WebSocket;\n const sleep = options.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));\n const once = () =>\n new Promise<WebSocket>((resolve, reject) => {\n const socket = new Socket(url);\n const opened = () => {\n socket.removeEventListener(\"close\", failed);\n resolve(socket);\n };\n const failed = () => {\n socket.removeEventListener(\"open\", opened);\n reject(new Error(\"WebSocket connection failed.\"));\n };\n socket.addEventListener(\"open\", opened, { once: true });\n socket.addEventListener(\"close\", failed, { once: true });\n });\n return (async () => {\n let attempt = 0;\n for (;;) {\n try {\n return await once();\n } catch (error) {\n if (attempt >= delays.length) throw error;\n await sleep(delays[attempt]!);\n attempt += 1;\n }\n }\n })();\n}\n","import { newWebSocketRpcSession, type RpcStub } from \"capnweb\";\nimport type { IterateApi, IterateSessionApi } from \"./api.ts\";\nimport { openSocketWithRetry } from \"./client/socket.ts\";\nimport { OAuthScopes } from \"./oauth-scopes.ts\";\n\n/** What `authenticate` resolves to: the session as a capnweb stub (pipelined; disposable) and the\n * bootstrap info it answered with. Declared, so the package's declarations stay serializable. */\nexport type AuthenticatedApp = {\n api: RpcStub<IterateSessionApi>;\n info: ReturnType<IterateSessionApi[\"info\"]>;\n /** The page names a project this sign-in does not include — its `/projects/<ref>` is absent from\n * `api.projects.list()`: leave for `/.auth/login`, whose page offers to sign in again (the\n * project ticked at consent, or another account) and returns to this very URL. Never settles,\n * like `authenticate`'s own leave for login. */\n signInFor(project: string): Promise<never>;\n};\nexport type IterateClient = { authenticate(next?: string): Promise<AuthenticatedApp> };\n\n/** The browser leaves for the issuer's login (a document navigation) and this never settles — no\n * framework in the loop: a TanStack `beforeLoad` awaiting it ends the way a thrown\n * `redirect({ reloadDocument: true })` did, a plain page simply navigates. */\nfunction leaveForLogin(login: string): Promise<never> {\n window.location.assign(login);\n return new Promise<never>(() => {});\n}\n\n/** One socket to `/api` with the session the OAuth gate resolved from the cookie. The `api` is\n * capnweb's pipelined `authenticate(...)` answer: usable before the socket has even opened, calls\n * queue until it has. */\ntype Connection = { api: RpcStub<IterateSessionApi>; socket: WebSocket; dispose(): void };\nfunction connectionOn(socket: WebSocket): Connection {\n const iterate = newWebSocketRpcSession<IterateApi>(socket);\n const api = iterate.authenticate({\n type: \"from-server-cookie\",\n }) as unknown as RpcStub<IterateSessionApi>;\n return { api, socket, dispose: () => iterate[Symbol.dispose]() };\n}\n\n/** Create once per app — a TanStack route's client-only `beforeLoad`, or a plain page's entry.\n * Every loader and action of the page shares the returned public RPC session.\n *\n * Connecting tries for a while (client/socket.ts: ≈16 s of attempts) before an error reaches the\n * page — a phone waking up or a flapping tunnel is not a reason to show \"connection failed\". And\n * the `api` the page holds is a proxy to the CURRENT connection: when the socket closes, the next\n * call opens a fresh one and pipelines onto it, so a dropped connection costs a reconnect, not the\n * page. A reconnect the platform refuses (the session ended elsewhere) rejects that call; the\n * page's retry runs `authenticate` again — a fresh one, the socket's close forgot the last — whose\n * probe sends the browser to log in. */\nexport function createIterateClient(options: { scopes?: string[] } = {}): IterateClient {\n const scopes = OAuthScopes.parse(options.scopes || []);\n let live: Connection | null = null;\n let connecting: Promise<AuthenticatedApp> | undefined;\n const socketUrl = () => {\n const url = new URL(\"/api\", window.location.href);\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n return url;\n };\n /** Adopt a connection as the live one until its socket closes. A close also forgets the settled\n * `authenticate` — the next one probes `/api` again, so a session that ended elsewhere sends the\n * browser to log in instead of a retry that can only fail. */\n function adopt(socket: WebSocket): Connection {\n const connection = connectionOn(socket);\n live = connection;\n const forget = () => {\n if (live !== connection) return;\n live = null;\n connecting = undefined;\n };\n const dispose = () => {\n forget();\n connection.dispose();\n };\n socket.addEventListener(\n \"close\",\n () => {\n forget();\n window.removeEventListener(\"pagehide\", dispose);\n },\n { once: true },\n );\n window.addEventListener(\"pagehide\", dispose, { once: true });\n return connection;\n }\n // Every property read goes to the live connection — or to a fresh one when the last socket\n // closed (a new WebSocket, no wait: capnweb queues the call until it opens). What comes back is\n // capnweb's own stub for that property — a method stub carries its path, so it is returned as\n // is, never bound or otherwise touched (a capnweb stub answers `.bind` with another stub).\n const api = new Proxy({} as RpcStub<IterateSessionApi>, {\n get(_target, property) {\n const connection = live || adopt(new WebSocket(socketUrl()));\n return Reflect.get(connection.api as object, property);\n },\n });\n const loginUrl = (params: Record<string, string>) =>\n `/.auth/login?${new URLSearchParams({ scope: scopes.join(\" \"), ...params })}`;\n async function connect(next: string): Promise<AuthenticatedApp> {\n const login = loginUrl({ next });\n // HTTP distinguishes 401 from outages; a failed WebSocket is not evidence\n // that the visitor needs to log in. Errors reach the route's error boundary.\n const probe = await fetch(\"/api\", {\n method: \"POST\",\n body: \"\",\n signal: AbortSignal.timeout(10_000),\n });\n await probe.body?.cancel();\n if (probe.status === 401) return leaveForLogin(login);\n if (!probe.ok) throw new Error(`iterate is unavailable (${probe.status}). Please retry.`);\n if (!live) adopt(await openSocketWithRetry(socketUrl()));\n // Consent is task-based: the person may have granted fewer scopes than the app asked for\n // (every scope but `iterate` is optional on the consent page). The granted set is\n // `info.scopes` — an app reads it and offers a step-up link (`/.auth/login?scope=…`) for what\n // it lacks; it is never bounced back to consent for a permission the person declined.\n const info = await api.info();\n return {\n api,\n info,\n signInFor: (project) => leaveForLogin(loginUrl({ next: window.location.href, project })),\n };\n }\n return {\n authenticate(next = \"/\") {\n connecting ||= connect(next).catch((error) => {\n connecting = undefined;\n throw error;\n });\n return connecting;\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ import { type PatchOp } from "../lib.ts";
2
+ /** One live-state delta off the wire — the payload of an `events.iterate.com/live-state/changed`
3
+ * ephemeral event, delivered raw to the subscriber. */
4
+ /** `patch: null` = the change was too large to send — the rev moved, re-seed through the door. */
5
+ export type LiveStateDelta = {
6
+ key: string;
7
+ from: number;
8
+ to: number;
9
+ patch: PatchOp[] | null;
10
+ };
11
+ /** What the producer's seed door returns: the current revision paired with the current value. */
12
+ export type LiveStateSeed<S> = {
13
+ rev: number;
14
+ state: S;
15
+ };
16
+ export type LiveStateStore<S> = {
17
+ /** The current value, or undefined until the first seed lands. */
18
+ get(): S | undefined;
19
+ /** The held revision, or null before the first seed. */
20
+ rev(): number | null;
21
+ /** Subscribe to changes (for React's useSyncExternalStore, or a test's await-loop). */
22
+ subscribe(listener: () => void): () => void;
23
+ /** Seed (or re-seed) from the door — the first paint, and the heal after a gap. */
24
+ seed(seed: LiveStateSeed<S>): void;
25
+ /** Reduce one delta in; on a revision gap call `resync` and hold the value until a fresh seed. */
26
+ apply(delta: LiveStateDelta, resync: () => void): void;
27
+ };
28
+ export declare function createLiveStateStore<S>(): LiveStateStore<S>;
29
+ /** The slice of an itx session this needs — a capnweb `IterateContextRpcTarget` proxy satisfies it structurally:
30
+ * `subscribe` hands back a DISPOSABLE handle (disposing it removes the subscription server-side). */
31
+ export type LiveStateItx = {
32
+ subscribe(input: {
33
+ name?: string;
34
+ consumes?: string[];
35
+ target: (events: unknown[], range: unknown) => void;
36
+ }): Promise<{
37
+ [Symbol.dispose](): void;
38
+ }>;
39
+ };
40
+ /** A connected live-state subscription: the store rendering it, and the dispose that removes the
41
+ * server-side subscription (the handle's disposer; the session's end does the same). */
42
+ export type LiveStateConnection<S> = {
43
+ store: LiveStateStore<S>;
44
+ /** Unsubscribe on the server and stop reducing deltas. Safe to call more than once. */
45
+ dispose(): Promise<void>;
46
+ };
47
+ /** Subscribe to a producer's live state and reduce it into a store. `door` reads the seed
48
+ * (`itx.invoke("itx.facets.get('slug').liveSnapshot()")` for a processor, or a mini-app's
49
+ * own `state()` method). Subscribe happens BEFORE the first seed, so a delta racing the seed just
50
+ * triggers one door re-read — never a lost update. Gap heals are SINGLE-FLIGHT (a burst of gapped
51
+ * frames triggers one door read, not one per frame); a failed heal is reported through `onResync`
52
+ * and retried by the next delivered delta (its `from` still mismatches, so it re-triggers). */
53
+ export declare function connectLiveState<S>(itx: LiveStateItx, opts: {
54
+ key: string;
55
+ name?: string;
56
+ door: () => Promise<LiveStateSeed<S>>;
57
+ /** Called after each gap heal attempt: "healed" on a fresh seed, the error when the door read
58
+ * failed (the store keeps its last value; the next delta retries). */
59
+ onResync?: (result: "healed" | Error) => void;
60
+ /** Abort while the FIRST seed is still pending (a component unmounting): the row just configured
61
+ * is recalled and the connect rejects — a door that never answers leaves nothing lent. */
62
+ signal?: AbortSignal;
63
+ }): Promise<LiveStateConnection<S>>;
@@ -0,0 +1,12 @@
1
+ /** The same code/PKCE parameters for browser login and a console-minted token. */
2
+ export declare function authorizationCodeRequest(input: {
3
+ issuer: string;
4
+ clientId: string;
5
+ redirectUri: string;
6
+ resources: string[];
7
+ scopes?: string[];
8
+ }): Promise<{
9
+ url: URL;
10
+ state: string;
11
+ verifier: string;
12
+ }>;
@@ -0,0 +1,109 @@
1
+ import { type LiveStateItx, type LiveStateSeed } from "./live-state.ts";
2
+ export type LiveStateStatus = "connecting" | "live" | "error";
3
+ /** One live state as a component reads it: the latest value (undefined until the first seed lands),
4
+ * the revision it is at, whether its subscription is connecting, live or failed, and the failure. */
5
+ export type LiveStateResult<S = unknown> = {
6
+ value: S | undefined;
7
+ rev: number | null;
8
+ status: LiveStateStatus;
9
+ error?: string;
10
+ };
11
+ /** Subscribe to a producer's live state and render its latest value. Pass a ready `itx` (a capnweb
12
+ * `api.authenticate(credentials).user` or `.projects.get(id)`), the producer's `key`, and a `door`
13
+ * thunk that reads `{rev, state}` (`() => itx.invoke("itx.facets.get('slug').liveSnapshot()")`).
14
+ * Re-subscribes when the session, `key`, or `name` changes; unmount (and every re-subscribe)
15
+ * disposes the previous server-side subscription. */
16
+ export declare function useLiveState<S>(itx: LiveStateItx | undefined, opts: {
17
+ key: string;
18
+ name?: string;
19
+ door: () => Promise<LiveStateSeed<S>>;
20
+ }): LiveStateResult<S>;
21
+ /** One committed event as the hook hands it out: the itx envelope, structurally. */
22
+ export type IterateContextEvent = {
23
+ offset: number;
24
+ type: string;
25
+ createdAt: string;
26
+ payload?: unknown;
27
+ metadata?: Record<string, unknown>;
28
+ idempotencyKey?: string;
29
+ source?: {
30
+ principal?: {
31
+ actor: string;
32
+ email?: string;
33
+ };
34
+ grant?: string;
35
+ processor?: {
36
+ slug: string;
37
+ version: string;
38
+ };
39
+ };
40
+ };
41
+ /** One row of a context's processors table (`itx.processors.list()`), structurally. */
42
+ export type IterateContextProcessorRow = {
43
+ name: string;
44
+ target: string;
45
+ consumes?: string[];
46
+ configuredAtOffset: number;
47
+ hostedFacet?: {
48
+ name: string;
49
+ className: string;
50
+ cacheKey?: string;
51
+ restarts: number;
52
+ };
53
+ };
54
+ /** One presence: who acted on the context and when last, from the log's stamps. */
55
+ export type IterateContextPresence = {
56
+ actor: string;
57
+ email?: string;
58
+ grant?: string;
59
+ lastSeenAt: string;
60
+ };
61
+ /** The slice of a context handle `useIterateContext` reads — a capnweb `IterateContextApi` stub
62
+ * satisfies it structurally. `rpcStubs` is optional: a handle typed without the census (a project
63
+ * context's client type) still gets the log, the table and the actors. `invoke` seeds a named
64
+ * facet's live state (`itx.facets.get('<name>').liveSnapshot()`, as an expression). */
65
+ export type IterateContextHandle = LiveStateItx & {
66
+ readEvents(afterOffset?: number, limit?: number): Promise<{
67
+ events: unknown[];
68
+ atHead: boolean;
69
+ scannedThroughOffset: number;
70
+ }>;
71
+ processors: {
72
+ list(): Promise<IterateContextProcessorRow[]> | IterateContextProcessorRow[];
73
+ };
74
+ rpcStubs?: {
75
+ list(): Promise<string[]> | string[];
76
+ };
77
+ invoke(call: string): Promise<unknown>;
78
+ };
79
+ /** THE ITERATE CONTEXT, live — one hook, one stream subscription. THE LOG: subscribe to every
80
+ * committed event (or `consumes`) BEFORE the catch-up read, so nothing lands between the two;
81
+ * pushes and pages both dedupe into one map by offset; `caughtUp` once the read reached the head;
82
+ * `error` when the connect failed. Off that same log, THE PROCESSORS TABLE, re-read whenever the
83
+ * log grows a row-changing event (a subscription configured, halted or resumed — the table is core
84
+ * state, one call away, no push of its own), and WHO IS HERE: the rpc stubs lent right now
85
+ * (`itx.rpcStubs.list()` — physical, re-read at every new head, since presence changes are
86
+ * ephemeral facts) and, from the log, every principal that acted, newest first. And named facets'
87
+ * LIVE STATE, each seeded through `itx.facets.get('<name>').liveSnapshot()` — one entry per name,
88
+ * always. `liveState` OMITTED opens `core` (the core reduce answers under that name) plus every
89
+ * hosted facet in the processors table the hook holds, following the table as it loads and changes;
90
+ * `liveState` GIVEN is exactly the names to open, no implicit `core`. Re-connects when `itx`
91
+ * changes; unmount disposes every server-side subscription. */
92
+ export declare function useIterateContext(itx: IterateContextHandle | undefined, opts?: {
93
+ consumes?: string[];
94
+ liveState?: string[];
95
+ }): {
96
+ events: IterateContextEvent[];
97
+ caughtUp: boolean;
98
+ error?: string;
99
+ processors: {
100
+ rows: IterateContextProcessorRow[];
101
+ loaded: boolean;
102
+ error?: string;
103
+ };
104
+ presence: {
105
+ actors: IterateContextPresence[];
106
+ rpcStubs: string[];
107
+ };
108
+ liveState: Record<string, LiveStateResult>;
109
+ };