iterate 0.3.0 → 0.4.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 (241) hide show
  1. package/README.md +164 -82
  2. package/dist/{next/api.d.ts → api.d.ts} +197 -33
  3. package/dist/{next/app-server.d.ts → app-server.d.ts} +7 -0
  4. package/dist/{next/app-server.mjs → app-server.mjs} +17 -15
  5. package/dist/app-server.mjs.map +1 -0
  6. package/dist/{next/app-session.mjs → app-session.mjs} +12 -15
  7. package/dist/app-session.mjs.map +1 -0
  8. package/dist/{next/app.mjs → app.mjs} +53 -14
  9. package/dist/app.mjs.map +1 -0
  10. package/dist/{next/client → client}/live-state.d.ts +11 -11
  11. package/dist/client/oauth.d.ts +17 -0
  12. package/dist/{next/client → client}/react.d.ts +10 -42
  13. package/dist/{next/client → client}/socket.d.ts +1 -0
  14. package/dist/client.mjs +156 -4
  15. package/dist/client.mjs.map +1 -0
  16. package/dist/{next/expression.d.ts → expression.d.ts} +11 -69
  17. package/dist/{next/expression.mjs → expression.mjs} +11 -109
  18. package/dist/expression.mjs.map +1 -0
  19. package/dist/lib-BWr-5mFO.mjs +36 -0
  20. package/dist/lib-BWr-5mFO.mjs.map +1 -0
  21. package/dist/{next/lib.d.ts → lib.d.ts} +16 -2
  22. package/dist/{next/lib.mjs → lib.mjs} +32 -3
  23. package/dist/lib.mjs.map +1 -0
  24. package/dist/node.d.ts +15 -3
  25. package/dist/node.mjs +36 -174
  26. package/dist/node.mjs.map +1 -1
  27. package/dist/{next/oauth-scopes.mjs → oauth-scopes.mjs} +1 -1
  28. package/dist/oauth-scopes.mjs.map +1 -0
  29. package/dist/{next/oauth.mjs → oauth.mjs} +14 -2
  30. package/dist/oauth.mjs.map +1 -0
  31. package/dist/principal.d.ts +8 -0
  32. package/dist/principal.mjs +8 -0
  33. package/dist/principal.mjs.map +1 -0
  34. package/dist/project-ingress.d.ts +58 -0
  35. package/dist/project-ingress.mjs +104 -0
  36. package/dist/project-ingress.mjs.map +1 -0
  37. package/dist/{next/react.mjs → react.mjs} +12 -12
  38. package/dist/react.mjs.map +1 -0
  39. package/dist/sdk/auth.d.ts +25 -0
  40. package/dist/sdk/index.d.ts +155 -0
  41. package/dist/sdk/record-pipelined-steps.d.ts +19 -0
  42. package/dist/sdk.mjs +245 -2
  43. package/dist/sdk.mjs.map +1 -0
  44. package/dist/{next/stream → stream}/processor.d.ts +28 -23
  45. package/dist/{next/stream → stream}/processor.mjs +48 -25
  46. package/dist/stream/processor.mjs.map +1 -0
  47. package/dist/{next/stream → stream}/run.d.ts +9 -6
  48. package/dist/{next/stream → stream}/run.mjs +13 -8
  49. package/dist/stream/run.mjs.map +1 -0
  50. package/dist/stream/test-support.d.ts +45 -0
  51. package/dist/stream/test-support.mjs +196 -0
  52. package/dist/stream/test-support.mjs.map +1 -0
  53. package/package.json +65 -219
  54. package/THIRD_PARTY_NOTICES.md +0 -55
  55. package/bin/iterate.js +0 -94
  56. package/dist/api-url-B6404M82.mjs +0 -17
  57. package/dist/api-url-B6404M82.mjs.map +0 -1
  58. package/dist/app-ref-BipL0feU.mjs +0 -35
  59. package/dist/app-ref-BipL0feU.mjs.map +0 -1
  60. package/dist/app-ref-C1CrgXqX.mjs +0 -7
  61. package/dist/app-ref-C1CrgXqX.mjs.map +0 -1
  62. package/dist/app-ref-DYai_om1.mjs +0 -7
  63. package/dist/app-ref-DYai_om1.mjs.map +0 -1
  64. package/dist/cli-D0c-pDL_.mjs +0 -1010
  65. package/dist/cli-D0c-pDL_.mjs.map +0 -1
  66. package/dist/client.d.ts +0 -3
  67. package/dist/cloudflare-BTm90gQ4.mjs +0 -951
  68. package/dist/cloudflare-BTm90gQ4.mjs.map +0 -1
  69. package/dist/contract-s4FW4eES.mjs +0 -309
  70. package/dist/contract-s4FW4eES.mjs.map +0 -1
  71. package/dist/document-review/index.d.ts +0 -5
  72. package/dist/document-review/types.d.ts +0 -107
  73. package/dist/document-review.mjs +0 -7015
  74. package/dist/document-review.mjs.map +0 -1
  75. package/dist/durable-object-processor-durability-CNsTjAJS.mjs +0 -205
  76. package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +0 -1
  77. package/dist/idempotency-DleloJNt.mjs +0 -28
  78. package/dist/idempotency-DleloJNt.mjs.map +0 -1
  79. package/dist/index.d.mts +0 -5
  80. package/dist/index.mjs +0 -8
  81. package/dist/index.mjs.map +0 -1
  82. package/dist/itx/api-url.d.ts +0 -6
  83. package/dist/itx/itx-node-client.d.ts +0 -65
  84. package/dist/itx/itx-session.d.ts +0 -215
  85. package/dist/itx/owned-rpc-session.d.ts +0 -14
  86. package/dist/itx/query-client.d.ts +0 -10
  87. package/dist/itx-api.generated.d.ts +0 -6195
  88. package/dist/itx-session-sjud8GiT.mjs +0 -534
  89. package/dist/itx-session-sjud8GiT.mjs.map +0 -1
  90. package/dist/live-state-BJNqOwFw.mjs +0 -299
  91. package/dist/live-state-BJNqOwFw.mjs.map +0 -1
  92. package/dist/next/app-server.mjs.map +0 -1
  93. package/dist/next/app-session.mjs.map +0 -1
  94. package/dist/next/app.mjs.map +0 -1
  95. package/dist/next/client/oauth.d.ts +0 -12
  96. package/dist/next/client.mjs +0 -156
  97. package/dist/next/client.mjs.map +0 -1
  98. package/dist/next/expression.mjs.map +0 -1
  99. package/dist/next/lib.mjs.map +0 -1
  100. package/dist/next/oauth-scopes.mjs.map +0 -1
  101. package/dist/next/oauth.mjs.map +0 -1
  102. package/dist/next/principal.d.ts +0 -64
  103. package/dist/next/principal.mjs +0 -98
  104. package/dist/next/principal.mjs.map +0 -1
  105. package/dist/next/project-ingress.d.ts +0 -37
  106. package/dist/next/project-ingress.mjs +0 -75
  107. package/dist/next/project-ingress.mjs.map +0 -1
  108. package/dist/next/react.mjs.map +0 -1
  109. package/dist/next/sdk/auth.d.ts +0 -5
  110. package/dist/next/sdk/index.d.ts +0 -112
  111. package/dist/next/sdk.mjs +0 -139
  112. package/dist/next/sdk.mjs.map +0 -1
  113. package/dist/next/stream/processor.mjs.map +0 -1
  114. package/dist/next/stream/run.mjs.map +0 -1
  115. package/dist/next-node.d.ts +0 -15
  116. package/dist/next-node.mjs +0 -51
  117. package/dist/next-node.mjs.map +0 -1
  118. package/dist/processor-host-capabilities-BMFH3KTM.mjs +0 -56
  119. package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +0 -1
  120. package/dist/processors/cloudflare.d.ts +0 -3
  121. package/dist/processors/durable-object-processor-durability.d.ts +0 -79
  122. package/dist/processors/event-consumption-metrics.d.ts +0 -82
  123. package/dist/processors/idempotency.d.ts +0 -13
  124. package/dist/processors/index.d.ts +0 -12
  125. package/dist/processors/processor-contracts.d.ts +0 -342
  126. package/dist/processors/processor-facet.d.ts +0 -186
  127. package/dist/processors/processor-host-capabilities.d.ts +0 -60
  128. package/dist/processors/prompt-sections.d.ts +0 -17
  129. package/dist/processors/rpc-types.d.ts +0 -515
  130. package/dist/processors/schemas.d.ts +0 -102
  131. package/dist/processors/stream-handle.d.ts +0 -45
  132. package/dist/processors/stream-processor-keepalive.d.ts +0 -95
  133. package/dist/processors/stream-processor-registry.d.ts +0 -233
  134. package/dist/processors/stream-processor-runner.d.ts +0 -289
  135. package/dist/processors/stream-processor.d.ts +0 -339
  136. package/dist/processors/stream-runtime-metrics.d.ts +0 -107
  137. package/dist/processors/testing.d.ts +0 -302
  138. package/dist/processors-BoNyeBfQ.mjs +0 -10
  139. package/dist/processors-BoNyeBfQ.mjs.map +0 -1
  140. package/dist/processors-cloudflare.mjs +0 -3
  141. package/dist/processors-testing.mjs +0 -435
  142. package/dist/processors-testing.mjs.map +0 -1
  143. package/dist/processors.mjs +0 -52
  144. package/dist/processors.mjs.map +0 -1
  145. package/dist/protocol-DnK_f2m6.mjs +0 -251
  146. package/dist/protocol-DnK_f2m6.mjs.map +0 -1
  147. package/dist/sdk/capnweb/index.d.ts +0 -2
  148. package/dist/sdk/capnweb/live-state/compact.d.ts +0 -5
  149. package/dist/sdk/capnweb/live-state/diff.d.ts +0 -41
  150. package/dist/sdk/capnweb/live-state/engine.d.ts +0 -44
  151. package/dist/sdk/capnweb/live-state/index.d.ts +0 -41
  152. package/dist/sdk/capnweb/live-state/protocol.d.ts +0 -87
  153. package/dist/sdk/capnweb/live-state/retain.d.ts +0 -23
  154. package/dist/sdk/capnweb/live-state/store.d.ts +0 -20
  155. package/dist/sdk/capnweb/live-state/types.d.ts +0 -11
  156. package/dist/sdk/capnweb/react.d.ts +0 -45
  157. package/dist/sdk/capnweb/react.mjs +0 -316
  158. package/dist/sdk/capnweb/react.mjs.map +0 -1
  159. package/dist/sdk/capnweb.mjs +0 -4
  160. package/dist/sdk/itx/react.d.ts +0 -191
  161. package/dist/sdk/itx/react.mjs +0 -383
  162. package/dist/sdk/itx/react.mjs.map +0 -1
  163. package/dist/sdk-DMB-IM11.mjs +0 -933
  164. package/dist/sdk-DMB-IM11.mjs.map +0 -1
  165. package/dist/sdk.d.ts +0 -339
  166. package/dist/serve-itx.d.ts +0 -46
  167. package/dist/starter-apps/flake-dashboard/app-ref.d.ts +0 -31
  168. package/dist/starter-apps/flake-dashboard/configured-worker.mjs +0 -1055
  169. package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +0 -1
  170. package/dist/starter-apps/flake-dashboard/contract.d.ts +0 -4839
  171. package/dist/starter-apps/flake-dashboard/contract.mjs +0 -2
  172. package/dist/starter-apps/flake-dashboard/index.d.ts +0 -17
  173. package/dist/starter-apps/flake-dashboard/index.mjs +0 -56
  174. package/dist/starter-apps/flake-dashboard/index.mjs.map +0 -1
  175. package/dist/starter-apps/flake-dashboard/worker.d.ts +0 -4607
  176. package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +0 -8914
  177. package/dist/starter-apps/github-ai-linter/configured-worker.mjs +0 -17987
  178. package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +0 -1
  179. package/dist/starter-apps/github-ai-linter/contract.d.ts +0 -9193
  180. package/dist/starter-apps/github-ai-linter/index.d.ts +0 -10
  181. package/dist/starter-apps/github-ai-linter/index.mjs +0 -36
  182. package/dist/starter-apps/github-ai-linter/index.mjs.map +0 -1
  183. package/dist/starter-apps/github-ai-linter/prompt.d.ts +0 -13
  184. package/dist/starter-apps/github-ai-linter/review-bot.d.ts +0 -808
  185. package/dist/starter-apps/github-ai-linter/rules.d.ts +0 -34
  186. package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +0 -19
  187. package/dist/starter-apps/github-ai-linter/worker.d.ts +0 -19
  188. package/dist/starter-apps/github-ai-linter/worker.mjs +0 -947
  189. package/dist/starter-apps/github-ai-linter/worker.mjs.map +0 -1
  190. package/dist/starter-apps/guestbook/app-ref.d.ts +0 -27
  191. package/dist/starter-apps/guestbook/client.d.ts +0 -7
  192. package/dist/starter-apps/guestbook/client.mjs +0 -59
  193. package/dist/starter-apps/guestbook/configured-worker.mjs +0 -205
  194. package/dist/starter-apps/guestbook/configured-worker.mjs.map +0 -1
  195. package/dist/starter-apps/guestbook/index.d.ts +0 -9
  196. package/dist/starter-apps/guestbook/index.mjs +0 -31
  197. package/dist/starter-apps/guestbook/index.mjs.map +0 -1
  198. package/dist/starter-apps/guestbook/processor.d.ts +0 -2267
  199. package/dist/starter-apps/guestbook/worker.d.ts +0 -26
  200. package/dist/starter-apps/guestbook/worker.mjs +0 -191
  201. package/dist/starter-apps/guestbook/worker.mjs.map +0 -1
  202. package/dist/starter-apps/media/configured-worker.mjs +0 -577
  203. package/dist/starter-apps/media/configured-worker.mjs.map +0 -1
  204. package/dist/starter-apps/media/index.mjs +0 -36
  205. package/dist/starter-apps/media/index.mjs.map +0 -1
  206. package/dist/starter-apps/media/ref.mjs +0 -20
  207. package/dist/starter-apps/media/ref.mjs.map +0 -1
  208. package/dist/starter-apps/media/worker.mjs +0 -579
  209. package/dist/starter-apps/media/worker.mjs.map +0 -1
  210. package/dist/starter-apps/notes/configured-worker.mjs +0 -6134
  211. package/dist/starter-apps/notes/configured-worker.mjs.map +0 -1
  212. package/dist/starter-apps/notes/index.mjs +0 -23
  213. package/dist/starter-apps/notes/index.mjs.map +0 -1
  214. package/dist/starter-apps/notes/ref.mjs +0 -21
  215. package/dist/starter-apps/notes/ref.mjs.map +0 -1
  216. package/dist/starter-apps/notes/worker.mjs +0 -427
  217. package/dist/starter-apps/notes/worker.mjs.map +0 -1
  218. package/dist/starter-apps/todo/client.mjs +0 -59
  219. package/dist/starter-apps/todo/configured-worker.mjs +0 -2864
  220. package/dist/starter-apps/todo/configured-worker.mjs.map +0 -1
  221. package/dist/starter-apps/todo/index.d.ts +0 -8
  222. package/dist/starter-apps/todo/index.mjs +0 -29
  223. package/dist/starter-apps/todo/index.mjs.map +0 -1
  224. package/dist/stream-processor-keepalive-DAQTP6m3.mjs +0 -2082
  225. package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +0 -1
  226. package/dist/usingCtx-mZx5nsAW.mjs +0 -11800
  227. package/dist/usingCtx-mZx5nsAW.mjs.map +0 -1
  228. package/dist/worker-ref-DZxPDmb_.mjs +0 -390
  229. package/dist/worker-ref-DZxPDmb_.mjs.map +0 -1
  230. package/dist/worker.d.mts +0 -33
  231. package/dist/worker.mjs +0 -18
  232. package/dist/worker.mjs.map +0 -1
  233. package/menubar/Iterate.entitlements +0 -12
  234. package/menubar/Iterate.swift +0 -914
  235. package/menubar/IterateIcon.swift +0 -145
  236. package/menubar/README.md +0 -28
  237. package/menubar/build-menubar-app.sh +0 -59
  238. /package/dist/{next/api.mjs → api.mjs} +0 -0
  239. /package/dist/{next/app-session.d.ts → app-session.d.ts} +0 -0
  240. /package/dist/{next/app.d.ts → app.d.ts} +0 -0
  241. /package/dist/{next/oauth-scopes.d.ts → oauth-scopes.d.ts} +0 -0
@@ -1,4 +1,6 @@
1
1
  import type { BrowserHost, BrowserSession } from "./app-session.ts";
2
+ /** The port separates local apps sharing localhost's cookie jar. */
3
+ export declare function sessionCookieName(url: URL): string;
2
4
  export declare function appSession(namespace: DurableObjectNamespace<BrowserSession>, request: Request): DurableObjectStub<BrowserSession> | null;
3
5
  /** Start one ordinary app session; the caller publishes its cookie after its own
4
6
  * sign-in step succeeds. The issuer uses this same code/PKCE flow internally. */
@@ -20,6 +22,11 @@ export declare function issuerOriginOf(candidate: string, options: {
20
22
  } | {
21
23
  error: string;
22
24
  };
25
+ /** The issuer must say it is the issuer: its discovery document's `issuer` equals the origin
26
+ * exactly. NOTHING else is read from the document — the endpoints stay hand-built from the origin
27
+ * (app-session.ts: `/oauth2/token`, and `/api` as the resource), so a document can steer nothing.
28
+ * Null when it answers as expected, else the reason. */
29
+ export declare function issuerAnswersAt(origin: string): Promise<string | null>;
23
30
  type AppAuth = {
24
31
  /** Public app branding; relative logo paths resolve against this app's origin. */
25
32
  client?: {
@@ -1,9 +1,8 @@
1
- import { isSameOriginBrowserRequest, sameOriginPath } from "./lib.mjs";
2
- import { cookieValueOf } from "./principal.mjs";
1
+ import { cookieValueOf, isSameOriginBrowserRequest, sameOriginPath } from "./lib.mjs";
3
2
  import { OAuthScopes } from "./oauth-scopes.mjs";
4
- import { t as _usingCtx } from "../usingCtx-inzbY1Qz.mjs";
3
+ import { t as _usingCtx } from "./usingCtx-inzbY1Qz.mjs";
5
4
  import { newHttpBatchRpcSession } from "capnweb";
6
- //#region src/next/app-server.ts
5
+ //#region src/app-server.ts
7
6
  /** The port separates local apps sharing localhost's cookie jar. */
8
7
  function sessionCookieName(url) {
9
8
  return `__Host-itx-session${url.port ? `-${url.port}` : ""}`;
@@ -88,7 +87,7 @@ async function grantSummary(resource, bearer) {
88
87
  headers: { Authorization: `Bearer ${bearer}` },
89
88
  signal: AbortSignal.timeout(5e3)
90
89
  })));
91
- const session = _usingCtx$1.u(api.authenticate({ type: "from-server-cookie" }));
90
+ const session = _usingCtx$1.u(api.authenticate({ type: "bearer" }));
92
91
  const info = session.info();
93
92
  const projects = session.projects.list();
94
93
  const [{ principal }, list] = await Promise.all([info, projects]);
@@ -103,7 +102,7 @@ async function grantSummary(resource, bearer) {
103
102
  }
104
103
  }
105
104
  /** What each scope is, to the person, in one or two words — the Sign-in-again page's permissions
106
- * line. The consent page (public/authorize.js) spells the same three at sentence length. */
105
+ * line. The issuer's consent page spells the same three at sentence length (oauth-scopes.ts). */
107
106
  const scopeLabels = {
108
107
  iterate: "your projects",
109
108
  account: "your account",
@@ -127,16 +126,19 @@ function gatePage(input) {
127
126
  });
128
127
  }
129
128
  /** The head and the CSP of a gate page: the deployment's own issuer dresses it (its stylesheet, its
130
- * logo); any other issuer gets the inline dress and nothing of its own on this origin. */
131
- function dressOf(issuer, defaultIssuer) {
129
+ * logo); any other issuer gets the inline dress and nothing of its own on this origin. `formIssuer`
130
+ * is the issuer the page's form ends up at: Chromium applies `form-action` to a submission's WHOLE
131
+ * redirect chain, and the gate's POSTs 303 onward to that issuer's authorize endpoint. */
132
+ function dressOf(issuer, defaultIssuer, formIssuer = issuer) {
133
+ const formAction = `form-action 'self' ${formIssuer}`;
132
134
  if (issuer === defaultIssuer) return {
133
- head: `<link rel="stylesheet" href="${issuer}/issuer.css"><link rel="icon" href="${issuer}/iterate-logo.svg" type="image/svg+xml">`,
134
- csp: `default-src 'none'; style-src ${issuer}; img-src ${issuer}; form-action 'self'; frame-ancestors 'none'`,
135
+ head: `<link rel="stylesheet" href="${issuer}/issuer.css"><link rel="icon" href="${issuer}/favicon.svg" type="image/svg+xml">`,
136
+ csp: `default-src 'none'; style-src ${issuer}; img-src ${issuer}; ${formAction}; frame-ancestors 'none'`,
135
137
  mark: `<img class="issuer-mark" src="${issuer}/iterate-logo.svg" alt="" width="56" height="56">`
136
138
  };
137
139
  return {
138
140
  head: `<style>${inlinePageCss}</style>`,
139
- csp: "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'",
141
+ csp: `default-src 'none'; style-src 'unsafe-inline'; ${formAction}; frame-ancestors 'none'`,
140
142
  mark: ""
141
143
  };
142
144
  }
@@ -156,10 +158,10 @@ async function signInAgainPage(input) {
156
158
  const permissions = `Permissions: ${scopes.map((scope) => scopeLabels[scope] || scope).join(", ")}.`;
157
159
  const signedIn = [
158
160
  grant?.email ? `Signed in as <strong>${text(grant.email)}</strong>` : "Signed in",
159
- ` through <strong>${text(new URL(issuer).host)}</strong>.`,
161
+ `through <strong>${text(new URL(issuer).host)}</strong>.`,
160
162
  reaches,
161
163
  permissions
162
- ].filter(Boolean).join(" ").replace("</strong> through", "</strong> through");
164
+ ].filter(Boolean).join(" ");
163
165
  const dress = dressOf(issuer, defaultIssuer);
164
166
  const back = nextPathOf(url.pathname + url.search, url.origin);
165
167
  return gatePage({
@@ -176,7 +178,7 @@ function connectPage(input) {
176
178
  const { url, origin, next, scope, held, defaultIssuer } = input;
177
179
  const host = new URL(origin).host;
178
180
  const ends = held && held.issuer !== origin ? `<p class="muted">This ends your current sign-in through <strong>${text(new URL(held.issuer).host)}</strong>.</p>` : "";
179
- const dress = dressOf(defaultIssuer, defaultIssuer);
181
+ const dress = dressOf(defaultIssuer, defaultIssuer, origin);
180
182
  return gatePage({
181
183
  title: `Connect to ${host}`,
182
184
  head: dress.head,
@@ -474,6 +476,6 @@ async function appAuth(request, config) {
474
476
  return null;
475
477
  }
476
478
  //#endregion
477
- export { appAuth, appSession, issuerOriginOf, startAppSession };
479
+ export { appAuth, appSession, issuerAnswersAt, issuerOriginOf, sessionCookieName, startAppSession };
478
480
 
479
481
  //# sourceMappingURL=app-server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-server.mjs","names":[],"sources":["../src/app-server.ts"],"sourcesContent":["// app-server.ts — THE BROWSER-AUTH GATE every app worker runs first: the OAuth client (one CIMD\n// client id per app origin, PKCE, the `__Host-` cookie), the `/api` proxy, and the few pages the gate\n// serves itself. A browser session is an ordinary OAuth client of ONE issuer — by default the\n// deployment's own (`config.issuer`), or any Iterate issuer a person CONNECTS the app to on purpose (a\n// self-hosted platform). Connecting is deliberate: a same-origin POST from `/.auth/connect`, a page\n// that names the issuer and asks — never a link, because a link would be a forced login: it could\n// bind a browser to an issuer of the linker's choosing, and the app would then show that issuer's\n// world as the person's. Once a session exists, its record is the ONLY source of where the credential\n// goes (`BrowserSession.host()`): the login probe, the `/api` proxy and logout all read it there.\nimport { newHttpBatchRpcSession } from \"capnweb\";\nimport type { BrowserHost, BrowserSession } from \"./app-session.ts\";\nimport type { IterateApi } from \"./api.ts\";\nimport { cookieValueOf, isSameOriginBrowserRequest, sameOriginPath } from \"./lib.ts\";\nimport { OAuthScopes } from \"./oauth-scopes.ts\";\n\n/** The port separates local apps sharing localhost's cookie jar. */\nexport function sessionCookieName(url: URL) {\n return `__Host-itx-session${url.port ? `-${url.port}` : \"\"}`;\n}\nexport function appSession(namespace: DurableObjectNamespace<BrowserSession>, request: Request) {\n const url = new URL(request.url);\n const id = cookieValueOf(request.headers.get(\"cookie\"), sessionCookieName(url));\n return id && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(id)\n ? namespace.getByName(`${url.origin}:${id}`)\n : null;\n}\n\n/** Start one ordinary app session; the caller publishes its cookie after its own\n * sign-in step succeeds. The issuer uses this same code/PKCE flow internally. */\nexport async function startAppSession(\n sessions: DurableObjectNamespace<BrowserSession>,\n host: BrowserHost,\n next: string,\n) {\n const id = crypto.randomUUID();\n const session = sessions.getByName(`${host.origin}:${id}`);\n const location = await session.begin(host, next);\n const setCookie = `${sessionCookieName(new URL(host.origin))}=${id}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000`;\n return { session, location, setCookie };\n}\n\n/** What an issuer a person names must be: an https ORIGIN (a path or query is dropped, not refused),\n * not an IP literal, not loopback, and not under one of this deployment's own zones (`denyZones` —\n * a project host on our wildcard is userspace and could serve a look-alike issuer) — unless it IS\n * the deployment's default issuer, which is always allowed as it is. Pure: the discovery check that\n * the origin really answers as that issuer is `issuerAnswersAt`. */\nexport function issuerOriginOf(\n candidate: string,\n options: { defaultIssuer: string; denyZones?: readonly string[] },\n): { origin: string } | { error: string } {\n let url: URL;\n try {\n url = new URL(candidate.trim());\n } catch {\n return { error: \"Enter the issuer's address, like https://iterate.example.workers.dev.\" };\n }\n const origin = url.origin;\n if (origin === options.defaultIssuer) return { origin };\n if (url.protocol !== \"https:\") return { error: \"The issuer must be served over https.\" };\n const host = url.hostname;\n if (\n /^\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(host) ||\n host.startsWith(\"[\") ||\n host === \"localhost\" ||\n host.endsWith(\".localhost\")\n )\n return { error: \"The issuer must have a public hostname.\" };\n for (const zone of options.denyZones || []) {\n const denied = zone.toLowerCase().replace(/^\\.+/, \"\");\n if (denied && (host === denied || host.endsWith(`.${denied}`)))\n return { error: `${host} is not an issuer this app can be connected to.` };\n }\n return { origin };\n}\n\n/** The issuer must say it is the issuer: its discovery document's `issuer` equals the origin\n * exactly. NOTHING else is read from the document — the endpoints stay hand-built from the origin\n * (app-session.ts: `/oauth2/token`, and `/api` as the resource), so a document can steer nothing.\n * Null when it answers as expected, else the reason. */\nexport async function issuerAnswersAt(origin: string): Promise<string | null> {\n try {\n const response = await fetch(`${origin}/.well-known/oauth-authorization-server`, {\n headers: { accept: \"application/json\" },\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) return `${new URL(origin).host} does not answer as an iterate platform.`;\n const document: unknown = await response.json();\n const issuer =\n typeof document === \"object\" && document ? (document as { issuer?: unknown }).issuer : null;\n if (issuer !== origin)\n return `${new URL(origin).host} does not answer as an iterate platform (its issuer is ${typeof issuer === \"string\" ? issuer : \"missing\"}).`;\n return null;\n } catch {\n return `${new URL(origin).host} could not be reached.`;\n }\n}\n\n/** `next` as a same-origin path with any `issuer` query parameter removed — an issuer rides in\n * through `/.auth/connect` and nowhere else, so a `next` can never carry one back into a login\n * (the Sign-in-again form returns to the very login URL that showed it). Untouched when there is\n * none to remove, so what a page sent stays byte for byte. */\nfunction nextPathOf(next: string | null, origin: string): string {\n const path = sameOriginPath(next || \"/\", origin);\n const url = new URL(path, origin);\n if (!url.searchParams.has(\"issuer\")) return path;\n url.searchParams.delete(\"issuer\");\n return url.pathname + url.search;\n}\n\n/** What the app's own sign-in currently is, for the Sign-in-again page to show: who it is signed in\n * as, and the projects it reaches — its grant, the thing that turned out not to include the project\n * the page named. One bounded HTTP-batch read of the platform (the logout path's shape), best\n * effort: a slow or failed read simply leaves those lines off, the page still works. */\nasync function grantSummary(resource: string, bearer: string) {\n // oxlint-disable-next-line iterate/no-capnweb-http-batch -- One bounded read, no live capabilities.\n using api = newHttpBatchRpcSession<IterateApi>(\n new Request(resource, {\n headers: { Authorization: `Bearer ${bearer}` },\n signal: AbortSignal.timeout(5_000),\n }),\n );\n using session = api.authenticate({ type: \"bearer\" });\n const info = session.info();\n const projects = session.projects.list();\n const [{ principal }, list] = await Promise.all([info, projects]);\n return { email: principal.email, projects: list.map((project) => project.slug) };\n}\n\n/** What each scope is, to the person, in one or two words — the Sign-in-again page's permissions\n * line. The issuer's consent page spells the same three at sentence length (oauth-scopes.ts). */\nconst scopeLabels: Record<string, string> = {\n iterate: \"your projects\",\n account: \"your account\",\n \"organizations:write\": \"your organizations\",\n};\n\nconst text = (value: string) => value.replace(/[&<>\"]/g, (c) => `&#${c.charCodeAt(0)};`);\n\n/** The gate's own pages, dressed without asking anyone: the issuer's stylesheet is loaded only when\n * the issuer is the deployment's own (a stylesheet is a document's dress, and an issuer a person\n * typed does not get to dress a page on this origin). */\nconst inlinePageCss = `body{margin:0;min-height:100svh;display:grid;place-items:center;font:15px/1.5 ui-sans-serif,system-ui,sans-serif;color:#18181b;background:#fafafa}main{width:min(28rem,calc(100% - 2rem));padding:2rem;background:#fff;border:1px solid #e4e4e7;border-radius:12px}h1{font-size:1.25rem;margin:0 0 .75rem}p{margin:0 0 .75rem}.muted{color:#71717a}code{font:.9em ui-monospace,SFMono-Regular,Menlo,monospace}button,.button{display:inline-block;padding:.55rem 1rem;border:0;border-radius:8px;background:#18181b;color:#fff;font:inherit;cursor:pointer;text-decoration:none}a.quiet{color:#71717a;margin-left:1rem}`;\n\n/** A page of the gate's own: `head` is what the head carries beyond the title, `body` the card. */\nfunction gatePage(input: {\n title: string;\n head: string;\n body: string;\n csp: string;\n status?: number;\n}): Response {\n return new Response(\n `<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>${text(input.title)}</title>${input.head}</head><body><main class=\"issuer-card\">${input.body}</main></body></html>`,\n {\n status: input.status ?? 200,\n headers: {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n \"Content-Security-Policy\": input.csp,\n \"X-Frame-Options\": \"DENY\",\n },\n },\n );\n}\n\n/** The head and the CSP of a gate page: the deployment's own issuer dresses it (its stylesheet, its\n * logo); any other issuer gets the inline dress and nothing of its own on this origin. `formIssuer`\n * is the issuer the page's form ends up at: Chromium applies `form-action` to a submission's WHOLE\n * redirect chain, and the gate's POSTs 303 onward to that issuer's authorize endpoint. */\nfunction dressOf(\n issuer: string,\n defaultIssuer: string,\n formIssuer: string = issuer,\n): { head: string; csp: string; mark: string } {\n const formAction = `form-action 'self' ${formIssuer}`;\n if (issuer === defaultIssuer)\n return {\n head: `<link rel=\"stylesheet\" href=\"${issuer}/issuer.css\"><link rel=\"icon\" href=\"${issuer}/favicon.svg\" type=\"image/svg+xml\">`,\n csp: `default-src 'none'; style-src ${issuer}; img-src ${issuer}; ${formAction}; frame-ancestors 'none'`,\n mark: `<img class=\"issuer-mark\" src=\"${issuer}/iterate-logo.svg\" alt=\"\" width=\"56\" height=\"56\">`,\n };\n return {\n head: `<style>${inlinePageCss}</style>`,\n csp: `default-src 'none'; style-src 'unsafe-inline'; ${formAction}; frame-ancestors 'none'`,\n mark: \"\",\n };\n}\n\n/** The sign-in this browser holds does not cover what the page needs: a project the page named\n * (`?project=`, the page's own word — its `/projects/<ref>` was absent from the session's list; the\n * page is not verified, it only offers a sign-out the person must click) or a permission the app\n * asks for. One page for both, on the app's origin because ending its session is a same-origin\n * POST. It shows who the app is signed in as, through which issuer, and the projects that sign-in\n * reaches (its grant), then one button that ends this app's session and comes straight back to\n * `/.auth/login`, which starts a fresh one — the issuer still knows the person, so they land on\n * consent: the project to tick, or Switch account — and returns to the page that sent them. */\nasync function signInAgainPage(input: {\n url: URL;\n issuer: string;\n resource: string;\n defaultIssuer: string;\n bearer: string;\n scopes: string[];\n project: string | null;\n}) {\n const { url, issuer, resource, defaultIssuer, bearer, scopes, project } = input;\n const lacks = project\n ? `does not include a project called <code>${text(project)}</code>`\n : \"does not include a permission this app asks for\";\n const grant = await grantSummary(resource, bearer).catch(() => null);\n const reaches = grant\n ? grant.projects.length\n ? `It reaches ${grant.projects\n .slice(0, 12)\n .map((slug) => `<code>${text(slug)}</code>`)\n .join(\n \", \",\n )}${grant.projects.length > 12 ? ` and ${grant.projects.length - 12} more` : \"\"}.`\n : \"It reaches no projects yet.\"\n : \"\";\n const permissions = `Permissions: ${scopes.map((scope) => scopeLabels[scope] || scope).join(\", \")}.`;\n const signedIn = [\n grant?.email ? `Signed in as <strong>${text(grant.email)}</strong>` : \"Signed in\",\n `through <strong>${text(new URL(issuer).host)}</strong>.`,\n reaches,\n permissions,\n ]\n .filter(Boolean)\n .join(\" \");\n const dress = dressOf(issuer, defaultIssuer);\n const back = nextPathOf(url.pathname + url.search, url.origin);\n return gatePage({\n title: \"Sign in again\",\n head: dress.head,\n csp: dress.csp,\n body: `${dress.mark}<h1>Sign in again</h1><p>Your sign-in to <strong>${text(url.host)}</strong> ${lacks}.</p><p class=\"muted\">${signedIn}</p><p class=\"muted\">Signing in again brings you back here. At consent, tick the project — or switch account.</p><form method=\"post\" action=\"/.auth/logout?next=${encodeURIComponent(back)}\"><button class=\"primary\" type=\"submit\">Sign in again</button></form>`,\n });\n}\n\n/** `/.auth/connect` — the one page that may bind this browser to an issuer other than the\n * deployment's own: it names the issuer's host in plain text, says what it will end, and asks. The\n * Continue button is a same-origin POST; the page loads nothing from the issuer it names. */\nfunction connectPage(input: {\n url: URL;\n origin: string;\n next: string;\n scope: string | null;\n held: { issuer: string } | null;\n defaultIssuer: string;\n}): Response {\n const { url, origin, next, scope, held, defaultIssuer } = input;\n const host = new URL(origin).host;\n const ends =\n held && held.issuer !== origin\n ? `<p class=\"muted\">This ends your current sign-in through <strong>${text(new URL(held.issuer).host)}</strong>.</p>`\n : \"\";\n const dress = dressOf(defaultIssuer, defaultIssuer, origin);\n return gatePage({\n title: `Connect to ${host}`,\n head: dress.head,\n csp: dress.csp,\n body: `${dress.mark}<h1>Connect ${text(url.host)} to <code>${text(host)}</code>?</h1><p>This app will sign you in through <strong>${text(host)}</strong> and show what that platform holds. Continue only if that is your own iterate platform.</p>${ends}<form method=\"post\" action=\"/.auth/connect\"><input type=\"hidden\" name=\"issuer\" value=\"${text(origin)}\"><input type=\"hidden\" name=\"next\" value=\"${text(next)}\">${scope ? `<input type=\"hidden\" name=\"scope\" value=\"${text(scope)}\">` : \"\"}<button class=\"primary\" type=\"submit\">Continue</button><a class=\"quiet\" href=\"/\">Cancel</a></form>`,\n });\n}\n\n/** A refusal of the gate's own, as a page the person can read. */\nfunction refusalPage(defaultIssuer: string, message: string, status = 400): Response {\n const dress = dressOf(defaultIssuer, defaultIssuer);\n return gatePage({\n title: \"Cannot connect\",\n head: dress.head,\n csp: dress.csp,\n status,\n body: `${dress.mark}<h1>Cannot connect</h1><p>${text(message)}</p><p><a class=\"button\" href=\"/\">Back</a></p>`,\n });\n}\n\ntype AppAuth = {\n /** Public app branding; relative logo paths resolve against this app's origin. */\n client?: { name: string; logoUri: string };\n sessions: DurableObjectNamespace<BrowserSession>;\n /** The deployment's own issuer — where a browser with no session signs in. */\n issuer: string;\n /** Its `/api`. */\n resource: string;\n /** Platform dispatches in process to avoid /api recursion; other apps pass fetch. */\n api: (request: Request) => Promise<Response> | Response;\n /** The issuer proves identity before establishing its own ordinary app session. */\n loginPage?: string;\n /** Zones no connectable issuer may live under (`issuerOriginOf`): this deployment's own, whose\n * project hosts are userspace. The default issuer is exempt. */\n denyZones?: readonly string[];\n};\n\n/** The same OAuth client and /api proxy on a platform host or a separate app worker. */\nexport async function appAuth(request: Request, config: AppAuth): Promise<Response | null> {\n const url = new URL(request.url);\n const { issuer, resource, sessions } = config;\n const client = config.client && {\n name: config.client.name,\n logoUri: new URL(config.client.logoUri, url.origin).href,\n };\n const session = appSession(sessions, request);\n const clearCookie = `${sessionCookieName(url)}=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0`;\n /** Where THIS browser's credential goes: its session's record, else the deployment's own. A record\n * bound to a CONNECTED issuer whose resource is not that issuer's `/api` is not one this code\n * wrote — it is dropped (the deployment's own issuer may name its resource as it likes). */\n const held = async (): Promise<{ issuer: string; resource: string } | null> => {\n const host = await session?.host();\n if (!host) return null;\n if (host.issuer !== issuer && host.resource !== `${host.issuer}/api`) {\n await session!.discard();\n return null;\n }\n return host;\n };\n if (url.pathname === \"/.auth/client.json\")\n return Response.json(\n {\n client_id: `${url.origin}/.auth/client.json`,\n client_name: client?.name || url.host,\n client_uri: url.origin,\n logo_uri: client?.logoUri,\n redirect_uris: [`${url.origin}/.auth/callback`],\n token_endpoint_auth_method: \"none\",\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n },\n { headers: { \"Cache-Control\": \"public, max-age=300\" } },\n );\n\n if (url.pathname === \"/.auth/session.json\") {\n // which issuer this browser is connected to — the app's shell says so when it is not the\n // deployment's own; no credential, no identity, nothing a page on another origin could use\n if (request.method !== \"GET\") return new Response(\"Method not allowed\", { status: 405 });\n const host = await held();\n return Response.json(\n { issuer: host?.issuer ?? null, defaultIssuer: issuer },\n { headers: { \"Cache-Control\": \"no-store\" } },\n );\n }\n\n if (url.pathname === \"/.auth/connect\") {\n if (config.loginPage) return new Response(\"Not found\", { status: 404 });\n if (request.method === \"GET\") {\n const named = issuerOriginOf(url.searchParams.get(\"issuer\") ?? \"\", {\n defaultIssuer: issuer,\n denyZones: config.denyZones,\n });\n if (\"error\" in named) return refusalPage(issuer, named.error);\n const next = nextPathOf(url.searchParams.get(\"next\"), url.origin);\n const scope = url.searchParams.get(\"scope\");\n const host = await held();\n // the deployment's own issuer needs no asking when this browser is not bound elsewhere\n if (named.origin === issuer && (!host || host.issuer === issuer))\n return new Response(null, {\n status: 303,\n headers: {\n Location: `/.auth/login?${new URLSearchParams({ next, scope: scope || \"\" })}`,\n \"Cache-Control\": \"no-store\",\n },\n });\n return connectPage({\n url,\n origin: named.origin,\n next,\n scope,\n held: host,\n defaultIssuer: issuer,\n });\n }\n if (request.method !== \"POST\") return new Response(\"Method not allowed\", { status: 405 });\n if (!isSameOriginBrowserRequest(request))\n return new Response(\"Cross-site request refused\", { status: 403 });\n const form = await request.formData();\n const named = issuerOriginOf(String(form.get(\"issuer\") ?? \"\"), {\n defaultIssuer: issuer,\n denyZones: config.denyZones,\n });\n if (\"error\" in named) return refusalPage(issuer, named.error);\n const next = nextPathOf(String(form.get(\"next\") ?? \"/\"), url.origin);\n const parsed = OAuthScopes.safeParse(\n String(form.get(\"scope\") ?? \"\")\n .split(\" \")\n .filter(Boolean),\n );\n if (!parsed.success) return refusalPage(issuer, \"Unsupported permission.\");\n if (named.origin !== issuer) {\n const refused = await issuerAnswersAt(named.origin);\n if (refused) return refusalPage(issuer, refused);\n }\n const host = await held();\n if (host?.issuer === named.origin)\n return new Response(null, {\n status: 303,\n headers: {\n Location: `/.auth/login?${new URLSearchParams({ next, scope: parsed.data.join(\" \") })}`,\n \"Cache-Control\": \"no-store\",\n },\n });\n if (host) {\n // a deliberate switch: end the sign-in at the issuer it was with (best effort — that issuer\n // may be gone), then forget it here either way\n try {\n await session!.end();\n } catch {\n await session!.discard();\n }\n }\n const { location, setCookie } = await startAppSession(\n sessions,\n {\n origin: url.origin,\n client,\n issuer: named.origin,\n resource: `${named.origin}/api`,\n scopes: parsed.data,\n },\n next,\n );\n return new Response(null, {\n status: 302,\n headers: {\n Location: location,\n \"Set-Cookie\": setCookie,\n \"Cache-Control\": \"no-store\",\n \"Referrer-Policy\": \"no-referrer\",\n },\n });\n }\n\n if (url.pathname === \"/.auth/login\") {\n if (request.method !== \"GET\") return new Response(\"Method not allowed\", { status: 405 });\n const next = nextPathOf(url.searchParams.get(\"next\"), url.origin);\n const scope = url.searchParams.get(\"scope\");\n const parsed = OAuthScopes.safeParse(scope ? scope.split(\" \").filter(Boolean) : []);\n if (!parsed.success) return new Response(\"Unsupported permission\", { status: 400 });\n const scopes = parsed.data;\n // A link may NAME an issuer; only the connect page's POST may bind one. So a login link that\n // names one lands on that page (which also says no to a bad one) — a click is always required.\n const requested = url.searchParams.get(\"issuer\");\n if (requested && !config.loginPage) {\n const named = issuerOriginOf(requested, {\n defaultIssuer: issuer,\n denyZones: config.denyZones,\n });\n if (\"error\" in named || named.origin !== issuer)\n return new Response(null, {\n status: 303,\n headers: {\n Location: `/.auth/connect?${new URLSearchParams({ issuer: requested, next, scope: scope || \"\" })}`,\n \"Cache-Control\": \"no-store\",\n },\n });\n }\n const host = await held();\n const target = host || { issuer, resource };\n const bearer = await session?.bearer();\n if (bearer) {\n const probe = await config.api(\n new Request(target.resource, {\n method: \"POST\",\n body: \"\",\n headers: { Authorization: `Bearer ${bearer}` },\n }),\n );\n await probe.body?.cancel();\n if (probe.status !== 401) {\n if (!probe.ok) throw new Error(`iterate API check failed (${probe.status})`);\n const heldScopes = await session!.scopes();\n const project = url.searchParams.get(\"project\");\n if (!project && scopes.every((scope) => heldScopes.includes(scope)))\n return new Response(null, {\n status: 303,\n headers: { Location: next, \"Cache-Control\": \"no-store\" },\n });\n // Only a deliberate POST can replace a valid grant. GET never signs out.\n return signInAgainPage({\n url,\n issuer: target.issuer,\n resource: target.resource,\n defaultIssuer: issuer,\n bearer,\n scopes: heldScopes,\n project,\n });\n }\n await session!.discard();\n }\n if (config.loginPage)\n return new Response(null, {\n status: 303,\n headers: {\n Location: `${config.loginPage}?${new URLSearchParams({ next })}`,\n \"Cache-Control\": \"no-store\",\n },\n });\n // The fresh session goes where this browser was CONNECTED (an expired grant at a self-host\n // signs in again at that self-host, not at the deployment's own issuer); a browser with no\n // record signs in at the default.\n const { location, setCookie } = await startAppSession(\n sessions,\n { origin: url.origin, issuer: target.issuer, resource: target.resource, scopes, client },\n next,\n );\n return new Response(null, {\n status: 302,\n headers: {\n Location: location,\n \"Set-Cookie\": setCookie,\n \"Cache-Control\": \"no-store\",\n \"Referrer-Policy\": \"no-referrer\",\n },\n });\n }\n if (url.pathname === \"/.auth/callback\") {\n if (request.method !== \"GET\") return new Response(\"Method not allowed\", { status: 405 });\n const result = await session?.complete(url.search);\n if (!result || result.error)\n return new Response(result?.error || \"Sign-in does not match this browser.\", {\n status: 400,\n headers: { \"Cache-Control\": \"no-store\", \"Referrer-Policy\": \"no-referrer\" },\n });\n return new Response(null, {\n status: 303,\n headers: {\n Location: result.next!,\n \"Cache-Control\": \"no-store\",\n \"Referrer-Policy\": \"no-referrer\",\n },\n });\n }\n if (url.pathname === \"/.auth/logout\") {\n if (request.method !== \"POST\") return new Response(\"Method not allowed\", { status: 405 });\n if (!isSameOriginBrowserRequest(request))\n return new Response(\"Cross-site request refused\", { status: 403 });\n // Where the ended session was bound: a sign-out from a CONNECTED issuer returns through the\n // connect page for that issuer (one click, the host named), so \"Sign in again\" at a self-host\n // stays at the self-host instead of silently binding the browser back to the default.\n const ended = await held();\n try {\n await session?.end();\n } catch {\n console.error(\"oauth.app_logout_failed\", { origin: url.origin });\n return new Response(\n \"Sign-out could not complete. Your session is still present; go back and try again.\",\n { status: 503 },\n );\n }\n const next = nextPathOf(url.searchParams.get(\"next\"), url.origin);\n // The scopes the next sign-in must hold ride along: the Sign-in-again form's `next` IS the login\n // URL that asked for them (`/.auth/login?…&scope=…`), and the connect page starts the grant from\n // its own `scope` — without this the reconnected grant would hold `iterate` alone and the login\n // would send the person straight back to Sign in again.\n const nextUrl = new URL(next, url.origin);\n const scope =\n nextUrl.pathname === \"/.auth/login\" ? nextUrl.searchParams.get(\"scope\") || \"\" : \"\";\n const headers = new Headers({\n Location:\n ended && ended.issuer !== issuer && !config.loginPage\n ? `/.auth/connect?${new URLSearchParams({ issuer: ended.issuer, next, scope })}`\n : next,\n \"Set-Cookie\": clearCookie,\n \"Cache-Control\": \"no-store\",\n });\n return new Response(null, { status: 303, headers });\n }\n if (url.pathname === \"/api\") {\n let token = request.headers.get(\"authorization\");\n let cookieAuth = false;\n if (!token && request.method !== \"OPTIONS\") {\n // THE COOKIE'S AUTHORITY IS SAME-ORIGIN ONLY (CSRF): a page on another origin that happens to\n // carry this app's cookie gets nothing from it — its request goes on BARE, and a WebSocket then\n // authenticates in-band with its own token, `authenticate({ type: \"bearer\", token })` (apps/os\n // api.ts), or holds no session at all.\n const bearer =\n session && request.headers.get(\"origin\") === url.origin ? await session.bearer() : null;\n if (bearer) {\n token = `Bearer ${bearer}`;\n cookieAuth = true;\n }\n }\n // where the credential goes is the session's record, never anything on the request\n const target = (await held()) ?? { issuer, resource };\n const headers = new Headers(request.headers);\n headers.delete(\"cookie\");\n // The same-origin gate above already ran. Provider CORS reconstruction loses 101.\n if (request.headers.get(\"upgrade\")?.toLowerCase() === \"websocket\") headers.delete(\"origin\");\n if (token) headers.set(\"authorization\", token);\n const response = await config.api(\n new Request(target.resource, new Request(request, { headers })),\n );\n if (cookieAuth && response.status === 401) {\n await session!.discard();\n const denied = new Response(response.body, response);\n denied.headers.append(\"Set-Cookie\", clearCookie);\n return denied;\n }\n return response;\n }\n if (url.pathname.startsWith(\"/.auth/\")) return new Response(\"Not found\", { status: 404 });\n return null;\n}\n"],"mappings":"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"}
@@ -1,12 +1,12 @@
1
1
  import { isLocalOrigin } from "./lib.mjs";
2
2
  import { OAuthScopes } from "./oauth-scopes.mjs";
3
- import { t as _usingCtx } from "../usingCtx-inzbY1Qz.mjs";
4
- import { authorizationCodeRequest } from "./oauth.mjs";
3
+ import { t as _usingCtx } from "./usingCtx-inzbY1Qz.mjs";
4
+ import { authorizationCodeRequest, authorizationServer } from "./oauth.mjs";
5
5
  import { DurableObject } from "cloudflare:workers";
6
6
  import { z } from "zod";
7
7
  import { newHttpBatchRpcSession } from "capnweb";
8
8
  import * as oauth from "oauth4webapi";
9
- //#region src/next/app-session.ts
9
+ //#region src/app-session.ts
10
10
  /** Each app binds this same class. Token exchange and logout use the issuer's
11
11
  * public protocol, so separately deployed apps need no platform bindings.
12
12
  * The input gate serializes refresh and logout across tabs. */
@@ -64,11 +64,7 @@ var BrowserSession = class extends DurableObject {
64
64
  return this.#serial(async () => {
65
65
  const data = await this.ctx.storage.get("session");
66
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
- };
67
+ const as = authorizationServer(data.issuer);
72
68
  const client = { client_id: data.clientId };
73
69
  let callback;
74
70
  try {
@@ -139,7 +135,7 @@ var BrowserSession = class extends DurableObject {
139
135
  headers: { Authorization: `Bearer ${token}` },
140
136
  signal: AbortSignal.timeout(1e4)
141
137
  })));
142
- await _usingCtx$1.u(api.authenticate({ type: "from-server-cookie" })).logout();
138
+ await _usingCtx$1.u(api.authenticate({ type: "bearer" })).logout();
143
139
  } catch (_) {
144
140
  _usingCtx$1.e = _;
145
141
  } finally {
@@ -160,10 +156,7 @@ var BrowserSession = class extends DurableObject {
160
156
  return null;
161
157
  }
162
158
  if (data.expiresAt > Date.now() + 3e4) return data.accessToken;
163
- const as = {
164
- issuer: data.issuer,
165
- token_endpoint: `${data.issuer}/oauth2/token`
166
- };
159
+ const as = authorizationServer(data.issuer);
167
160
  const client = { client_id: data.clientId };
168
161
  const started = Date.now();
169
162
  let tokens;
@@ -212,13 +205,17 @@ var BrowserSession = class extends DurableObject {
212
205
  await this.ctx.storage.setAlarm(stored.until);
213
206
  return stored;
214
207
  }
215
- /** A dead code or refresh token (`invalid_grant`) ends the session; other failures are transient. */
208
+ /** A dead code or refresh token (`invalid_grant`) ends the session; other failures are transient.
209
+ * A token endpoint that answered anything but its token — an OAuth error body, or a response
210
+ * oauth4webapi could not read as one (an uncaught 500) — throws one message naming its status,
211
+ * which reads the same on the far side of the Durable Object's RPC. */
216
212
  async #endOnDeadGrant(error) {
217
213
  if (error instanceof oauth.ResponseBodyError && error.error === "invalid_grant") {
218
214
  await this.#clear();
219
215
  return;
220
216
  }
221
- throw error instanceof oauth.ResponseBodyError ? /* @__PURE__ */ new Error(`Iterate token exchange failed (${error.status}). Try again.`) : error;
217
+ const status = error instanceof oauth.ResponseBodyError ? error.status : error instanceof oauth.OperationProcessingError && error.cause instanceof Response ? error.cause.status : null;
218
+ throw status === null ? error : /* @__PURE__ */ new Error(`Iterate token exchange failed (${status}). Try again.`);
222
219
  }
223
220
  /** An operation failure must not reset the DO or fail other tabs' requests. */
224
221
  #serial(work) {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-session.mjs","names":["#serial","#clear","#tokenOptions","#endOnDeadGrant","#activate","#bearer"],"sources":["../src/app-session.ts"],"sourcesContent":["import { DurableObject } from \"cloudflare:workers\";\nimport { newHttpBatchRpcSession } from \"capnweb\";\nimport * as oauth from \"oauth4webapi\";\nimport { z } from \"zod\";\nimport { authorizationCodeRequest, authorizationServer } 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 = authorizationServer(data.issuer);\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 // oxlint-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: \"bearer\" });\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 (which also ends one unused for a week: its\n // refresh then answers `invalid_grant`). 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 = authorizationServer(data.issuer);\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 * A token endpoint that answered anything but its token — an OAuth error body, or a response\n * oauth4webapi could not read as one (an uncaught 500) — throws one message naming its status,\n * which reads the same on the far side of the Durable Object's RPC. */\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 const status =\n error instanceof oauth.ResponseBodyError\n ? error.status\n : error instanceof oauth.OperationProcessingError && error.cause instanceof Response\n ? error.cause.status\n : null;\n throw status === null\n ? error\n : new Error(`Iterate token exchange failed (${status}). Try again.`);\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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"}
@@ -1,10 +1,18 @@
1
1
  import { OAuthScopes } from "./oauth-scopes.mjs";
2
2
  import { newWebSocketRpcSession } from "capnweb";
3
- //#region src/next/client/socket.ts
3
+ //#region src/client/socket.ts
4
4
  /** Open a WebSocket, trying again for a while when the connection fails: a phone waking up, a
5
5
  * tunnel flapping, a cold edge — the things a first attempt trips over. Resolves with the socket
6
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). */
7
+ * waits BETWEEN attempts (the first is immediate).
8
+ *
9
+ * Every failed attempt is explained, never swallowed (docs/engineering-invariants.md): the error
10
+ * names the close code and reason the socket closed with before it opened, and each attempt that
11
+ * is tried again logs a `client.platform-failure-socket-open` warn first. The upgrade's HTTP status
12
+ * is not among them: the WebSocket API never exposes the handshake's response (a refused upgrade
13
+ * is a close 1006, by design — https://websockets.spec.whatwg.org/#feedback-from-the-protocol),
14
+ * so a non-standard `ErrorEvent.error` (Node's undici names "non-101 status code" there) is added
15
+ * when the runtime gives one. */
8
16
  const RETRY_DELAYS_MS = [
9
17
  250,
10
18
  500,
@@ -13,36 +21,67 @@ const RETRY_DELAYS_MS = [
13
21
  4e3,
14
22
  8e3
15
23
  ];
24
+ /** How long one attempt may take to open. A handshake that neither opens nor closes is a failed
25
+ * attempt too, closed and tried again like any other: without a bound the page waits on it for as
26
+ * long as the browser does, behind a spinner that never ends (a Dash spec sat 30 s after its
27
+ * sign-in's `POST /api` answered, with no `/api` upgrade ever reaching the Worker, 2026-09-24).
28
+ * A healthy upgrade opens in well under a second. */
29
+ const HANDSHAKE_TIMEOUT_MS = 1e4;
16
30
  function openSocketWithRetry(url, options = {}) {
17
31
  const delays = options.delaysMs || RETRY_DELAYS_MS;
32
+ const handshakeTimeoutMs = options.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS;
18
33
  const Socket = options.WebSocket || WebSocket;
19
34
  const sleep = options.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
20
35
  const once = () => new Promise((resolve, reject) => {
21
36
  const socket = new Socket(url);
22
- const opened = () => {
37
+ let cause = "";
38
+ const timeout = setTimeout(() => {
39
+ settle();
40
+ socket.close();
41
+ reject(/* @__PURE__ */ new Error(`WebSocket did not open in ${handshakeTimeoutMs} ms`));
42
+ }, handshakeTimeoutMs);
43
+ function settle() {
44
+ clearTimeout(timeout);
45
+ socket.removeEventListener("open", opened);
23
46
  socket.removeEventListener("close", failed);
47
+ socket.removeEventListener("error", errored);
48
+ }
49
+ function errored(event) {
50
+ const error = event.error;
51
+ if (error instanceof Error) cause = ` (${error.message})`;
52
+ }
53
+ function opened() {
54
+ settle();
24
55
  resolve(socket);
25
- };
26
- const failed = () => {
27
- socket.removeEventListener("open", opened);
28
- reject(/* @__PURE__ */ new Error("WebSocket connection failed."));
29
- };
56
+ }
57
+ function failed(event) {
58
+ settle();
59
+ reject(/* @__PURE__ */ new Error(`WebSocket connection failed: closed ${event.code}${event.reason ? ` "${event.reason}"` : ""} before it opened${cause}`));
60
+ }
30
61
  socket.addEventListener("open", opened, { once: true });
62
+ socket.addEventListener("error", errored);
31
63
  socket.addEventListener("close", failed, { once: true });
32
64
  });
33
65
  return (async () => {
34
- let attempt = 0;
35
- for (;;) try {
66
+ for (let attempt = 1;; attempt++) try {
36
67
  return await once();
37
68
  } catch (error) {
38
- if (attempt >= delays.length) throw error;
39
- await sleep(delays[attempt]);
40
- attempt += 1;
69
+ const delay = delays[attempt - 1];
70
+ if (delay === void 0) throw error;
71
+ console.warn({
72
+ event: "client.platform-failure-socket-open",
73
+ url: String(url),
74
+ attempt,
75
+ attempts: delays.length + 1,
76
+ retryInMs: delay,
77
+ message: error.message
78
+ });
79
+ await sleep(delay);
41
80
  }
42
81
  })();
43
82
  }
44
83
  //#endregion
45
- //#region src/next/app.ts
84
+ //#region src/app.ts
46
85
  /** The browser leaves for the issuer's login (a document navigation) and this never settles — no
47
86
  * framework in the loop: a TanStack `beforeLoad` awaiting it ends the way a thrown
48
87
  * `redirect({ reloadDocument: true })` did, a plain page simply navigates. */
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.mjs","names":[],"sources":["../src/client/socket.ts","../src/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).\n *\n * Every failed attempt is explained, never swallowed (docs/engineering-invariants.md): the error\n * names the close code and reason the socket closed with before it opened, and each attempt that\n * is tried again logs a `client.platform-failure-socket-open` warn first. The upgrade's HTTP status\n * is not among them: the WebSocket API never exposes the handshake's response (a refused upgrade\n * is a close 1006, by design — https://websockets.spec.whatwg.org/#feedback-from-the-protocol),\n * so a non-standard `ErrorEvent.error` (Node's undici names \"non-101 status code\" there) is added\n * when the runtime gives one. */\nconst RETRY_DELAYS_MS: readonly number[] = [250, 500, 1_000, 2_000, 4_000, 8_000];\n\n/** How long one attempt may take to open. A handshake that neither opens nor closes is a failed\n * attempt too, closed and tried again like any other: without a bound the page waits on it for as\n * long as the browser does, behind a spinner that never ends (a Dash spec sat 30 s after its\n * sign-in's `POST /api` answered, with no `/api` upgrade ever reaching the Worker, 2026-09-24).\n * A healthy upgrade opens in well under a second. */\nconst HANDSHAKE_TIMEOUT_MS = 10_000;\n\nexport function openSocketWithRetry(\n url: string | URL,\n options: {\n delaysMs?: readonly number[];\n handshakeTimeoutMs?: 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 handshakeTimeoutMs = options.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_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 let cause = \"\";\n const timeout = setTimeout(() => {\n settle();\n socket.close();\n reject(new Error(`WebSocket did not open in ${handshakeTimeoutMs} ms`));\n }, handshakeTimeoutMs);\n function settle() {\n clearTimeout(timeout);\n socket.removeEventListener(\"open\", opened);\n socket.removeEventListener(\"close\", failed);\n socket.removeEventListener(\"error\", errored);\n }\n function errored(event: Event) {\n const error = (event as Partial<ErrorEvent>).error;\n if (error instanceof Error) cause = ` (${error.message})`;\n }\n function opened() {\n settle();\n resolve(socket);\n }\n function failed(event: CloseEvent) {\n settle();\n reject(\n new Error(\n `WebSocket connection failed: closed ${event.code}${event.reason ? ` \"${event.reason}\"` : \"\"} before it opened${cause}`,\n ),\n );\n }\n socket.addEventListener(\"open\", opened, { once: true });\n socket.addEventListener(\"error\", errored);\n socket.addEventListener(\"close\", failed, { once: true });\n });\n return (async () => {\n for (let attempt = 1; ; attempt++) {\n try {\n return await once();\n } catch (error) {\n const delay = delays[attempt - 1];\n if (delay === undefined) throw error;\n console.warn({\n event: \"client.platform-failure-socket-open\",\n url: String(url),\n attempt,\n attempts: delays.length + 1,\n retryInMs: delay,\n message: (error as Error).message,\n });\n await sleep(delay);\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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"}
@@ -1,14 +1,14 @@
1
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. */
2
+ /** One live-state delta off the wire — the payload of an `events.iterate.com/itx/live-state-changed`
3
+ * ephemeral event, delivered raw to the subscriber. `patch: null` = the change was too large to
4
+ * send — the rev moved, re-read the seed. */
5
5
  export type LiveStateDelta = {
6
6
  key: string;
7
7
  from: number;
8
8
  to: number;
9
9
  patch: PatchOp[] | null;
10
10
  };
11
- /** What the producer's seed door returns: the current revision paired with the current value. */
11
+ /** What the producer's seed read returns: the current revision paired with the current value. */
12
12
  export type LiveStateSeed<S> = {
13
13
  rev: number;
14
14
  state: S;
@@ -20,7 +20,7 @@ export type LiveStateStore<S> = {
20
20
  rev(): number | null;
21
21
  /** Subscribe to changes (for React's useSyncExternalStore, or a test's await-loop). */
22
22
  subscribe(listener: () => void): () => void;
23
- /** Seed (or re-seed) from the door — the first paint, and the heal after a gap. */
23
+ /** Seed (or re-seed) from a seed read — the first paint, and the heal after a gap. */
24
24
  seed(seed: LiveStateSeed<S>): void;
25
25
  /** Reduce one delta in; on a revision gap call `resync` and hold the value until a fresh seed. */
26
26
  apply(delta: LiveStateDelta, resync: () => void): void;
@@ -44,20 +44,20 @@ export type LiveStateConnection<S> = {
44
44
  /** Unsubscribe on the server and stop reducing deltas. Safe to call more than once. */
45
45
  dispose(): Promise<void>;
46
46
  };
47
- /** Subscribe to a producer's live state and reduce it into a store. `door` reads the seed
47
+ /** Subscribe to a producer's live state and reduce it into a store. `readSeed` reads the seed
48
48
  * (`itx.invoke("itx.facets.get('slug').liveSnapshot()")` for a processor, or a mini-app's
49
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`
50
+ * triggers one seed re-read — never a lost update. Gap heals are SINGLE-FLIGHT (a burst of gapped
51
+ * frames triggers one seed read, not one per frame); a failed heal is reported through `onResync`
52
52
  * and retried by the next delivered delta (its `from` still mismatches, so it re-triggers). */
53
53
  export declare function connectLiveState<S>(itx: LiveStateItx, opts: {
54
54
  key: string;
55
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
56
+ readSeed: () => Promise<LiveStateSeed<S>>;
57
+ /** Called after each gap heal attempt: "healed" on a fresh seed, the error when the seed read
58
58
  * failed (the store keeps its last value; the next delta retries). */
59
59
  onResync?: (result: "healed" | Error) => void;
60
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. */
61
+ * is recalled and the connect rejects — a seed read that never answers leaves nothing lent. */
62
62
  signal?: AbortSignal;
63
63
  }): Promise<LiveStateConnection<S>>;
@@ -0,0 +1,17 @@
1
+ import * as oauth from "oauth4webapi";
2
+ /** The platform's issuer as oauth4webapi's authorization server: its endpoints under `/oauth2`, and
3
+ * the `iss` it adds to every authorization response (RFC 9207), which `validateAuthResponse` then
4
+ * requires. The same description for an app's browser session and the CLI. */
5
+ export declare function authorizationServer(issuer: string): oauth.AuthorizationServer;
6
+ /** The same code/PKCE parameters for browser login and a console-minted token. */
7
+ export declare function authorizationCodeRequest(input: {
8
+ issuer: string;
9
+ clientId: string;
10
+ redirectUri: string;
11
+ resources: string[];
12
+ scopes?: string[];
13
+ }): Promise<{
14
+ url: URL;
15
+ state: string;
16
+ verifier: string;
17
+ }>;
@@ -1,3 +1,5 @@
1
+ import type { SubscriptionListEntry } from "../api.ts";
2
+ import type { StreamEvent } from "../stream/processor.ts";
1
3
  import { type LiveStateItx, type LiveStateSeed } from "./live-state.ts";
2
4
  export type LiveStateStatus = "connecting" | "live" | "error";
3
5
  /** One live state as a component reads it: the latest value (undefined until the first seed lands),
@@ -9,48 +11,15 @@ export type LiveStateResult<S = unknown> = {
9
11
  error?: string;
10
12
  };
11
13
  /** 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`
14
+ * `api.authenticate(credentials).user` or `.projects.get(id)`), the producer's `key`, and a `readSeed`
13
15
  * thunk that reads `{rev, state}` (`() => itx.invoke("itx.facets.get('slug').liveSnapshot()")`).
14
16
  * Re-subscribes when the session, `key`, or `name` changes; unmount (and every re-subscribe)
15
17
  * disposes the previous server-side subscription. */
16
18
  export declare function useLiveState<S>(itx: LiveStateItx | undefined, opts: {
17
19
  key: string;
18
20
  name?: string;
19
- door: () => Promise<LiveStateSeed<S>>;
21
+ readSeed: () => Promise<LiveStateSeed<S>>;
20
22
  }): 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
23
  /** One presence: who acted on the context and when last, from the log's stamps. */
55
24
  export type IterateContextPresence = {
56
25
  actor: string;
@@ -59,9 +28,8 @@ export type IterateContextPresence = {
59
28
  lastSeenAt: string;
60
29
  };
61
30
  /** 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). */
31
+ * satisfies it structurally. `invoke` seeds a named facet's live state
32
+ * (`itx.facets.get('<name>').liveSnapshot()`, as an expression). */
65
33
  export type IterateContextHandle = LiveStateItx & {
66
34
  readEvents(afterOffset?: number, limit?: number): Promise<{
67
35
  events: unknown[];
@@ -69,9 +37,9 @@ export type IterateContextHandle = LiveStateItx & {
69
37
  scannedThroughOffset: number;
70
38
  }>;
71
39
  processors: {
72
- list(): Promise<IterateContextProcessorRow[]> | IterateContextProcessorRow[];
40
+ list(): Promise<SubscriptionListEntry[]> | SubscriptionListEntry[];
73
41
  };
74
- rpcStubs?: {
42
+ rpcStubs: {
75
43
  list(): Promise<string[]> | string[];
76
44
  };
77
45
  invoke(call: string): Promise<unknown>;
@@ -93,11 +61,11 @@ export declare function useIterateContext(itx: IterateContextHandle | undefined,
93
61
  consumes?: string[];
94
62
  liveState?: string[];
95
63
  }): {
96
- events: IterateContextEvent[];
64
+ events: StreamEvent[];
97
65
  caughtUp: boolean;
98
66
  error?: string;
99
67
  processors: {
100
- rows: IterateContextProcessorRow[];
68
+ rows: SubscriptionListEntry[];
101
69
  loaded: boolean;
102
70
  error?: string;
103
71
  };