iterate 0.2.7 → 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 +11 -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,479 @@
1
+ import { isSameOriginBrowserRequest, sameOriginPath } from "./lib.mjs";
2
+ import { cookieValueOf } from "./principal.mjs";
3
+ import { OAuthScopes } from "./oauth-scopes.mjs";
4
+ import { t as _usingCtx } from "../usingCtx-inzbY1Qz.mjs";
5
+ import { newHttpBatchRpcSession } from "capnweb";
6
+ //#region src/next/app-server.ts
7
+ /** The port separates local apps sharing localhost's cookie jar. */
8
+ function sessionCookieName(url) {
9
+ return `__Host-itx-session${url.port ? `-${url.port}` : ""}`;
10
+ }
11
+ function appSession(namespace, request) {
12
+ const url = new URL(request.url);
13
+ const id = cookieValueOf(request.headers.get("cookie"), sessionCookieName(url));
14
+ return id && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(id) ? namespace.getByName(`${url.origin}:${id}`) : null;
15
+ }
16
+ /** Start one ordinary app session; the caller publishes its cookie after its own
17
+ * sign-in step succeeds. The issuer uses this same code/PKCE flow internally. */
18
+ async function startAppSession(sessions, host, next) {
19
+ const id = crypto.randomUUID();
20
+ const session = sessions.getByName(`${host.origin}:${id}`);
21
+ return {
22
+ session,
23
+ location: await session.begin(host, next),
24
+ setCookie: `${sessionCookieName(new URL(host.origin))}=${id}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000`
25
+ };
26
+ }
27
+ /** What an issuer a person names must be: an https ORIGIN (a path or query is dropped, not refused),
28
+ * not an IP literal, not loopback, and not under one of this deployment's own zones (`denyZones` —
29
+ * a project host on our wildcard is userspace and could serve a look-alike issuer) — unless it IS
30
+ * the deployment's default issuer, which is always allowed as it is. Pure: the discovery check that
31
+ * the origin really answers as that issuer is `issuerAnswersAt`. */
32
+ function issuerOriginOf(candidate, options) {
33
+ let url;
34
+ try {
35
+ url = new URL(candidate.trim());
36
+ } catch {
37
+ return { error: "Enter the issuer's address, like https://iterate.example.workers.dev." };
38
+ }
39
+ const origin = url.origin;
40
+ if (origin === options.defaultIssuer) return { origin };
41
+ if (url.protocol !== "https:") return { error: "The issuer must be served over https." };
42
+ const host = url.hostname;
43
+ if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.startsWith("[") || host === "localhost" || host.endsWith(".localhost")) return { error: "The issuer must have a public hostname." };
44
+ for (const zone of options.denyZones || []) {
45
+ const denied = zone.toLowerCase().replace(/^\.+/, "");
46
+ if (denied && (host === denied || host.endsWith(`.${denied}`))) return { error: `${host} is not an issuer this app can be connected to.` };
47
+ }
48
+ return { origin };
49
+ }
50
+ /** The issuer must say it is the issuer: its discovery document's `issuer` equals the origin
51
+ * exactly. NOTHING else is read from the document — the endpoints stay hand-built from the origin
52
+ * (app-session.ts: `/oauth2/token`, and `/api` as the resource), so a document can steer nothing.
53
+ * Null when it answers as expected, else the reason. */
54
+ async function issuerAnswersAt(origin) {
55
+ try {
56
+ const response = await fetch(`${origin}/.well-known/oauth-authorization-server`, {
57
+ headers: { accept: "application/json" },
58
+ signal: AbortSignal.timeout(1e4)
59
+ });
60
+ if (!response.ok) return `${new URL(origin).host} does not answer as an iterate platform.`;
61
+ const document = await response.json();
62
+ const issuer = typeof document === "object" && document ? document.issuer : null;
63
+ if (issuer !== origin) return `${new URL(origin).host} does not answer as an iterate platform (its issuer is ${typeof issuer === "string" ? issuer : "missing"}).`;
64
+ return null;
65
+ } catch {
66
+ return `${new URL(origin).host} could not be reached.`;
67
+ }
68
+ }
69
+ /** `next` as a same-origin path with any `issuer` query parameter removed — an issuer rides in
70
+ * through `/.auth/connect` and nowhere else, so a `next` can never carry one back into a login
71
+ * (the Sign-in-again form returns to the very login URL that showed it). Untouched when there is
72
+ * none to remove, so what a page sent stays byte for byte. */
73
+ function nextPathOf(next, origin) {
74
+ const path = sameOriginPath(next || "/", origin);
75
+ const url = new URL(path, origin);
76
+ if (!url.searchParams.has("issuer")) return path;
77
+ url.searchParams.delete("issuer");
78
+ return url.pathname + url.search;
79
+ }
80
+ /** What the app's own sign-in currently is, for the Sign-in-again page to show: who it is signed in
81
+ * as, and the projects it reaches — its grant, the thing that turned out not to include the project
82
+ * the page named. One bounded HTTP-batch read of the platform (the logout path's shape), best
83
+ * effort: a slow or failed read simply leaves those lines off, the page still works. */
84
+ async function grantSummary(resource, bearer) {
85
+ try {
86
+ var _usingCtx$1 = _usingCtx();
87
+ const api = _usingCtx$1.u(newHttpBatchRpcSession(new Request(resource, {
88
+ headers: { Authorization: `Bearer ${bearer}` },
89
+ signal: AbortSignal.timeout(5e3)
90
+ })));
91
+ const session = _usingCtx$1.u(api.authenticate({ type: "from-server-cookie" }));
92
+ const info = session.info();
93
+ const projects = session.projects.list();
94
+ const [{ principal }, list] = await Promise.all([info, projects]);
95
+ return {
96
+ email: principal.email,
97
+ projects: list.map((project) => project.slug)
98
+ };
99
+ } catch (_) {
100
+ _usingCtx$1.e = _;
101
+ } finally {
102
+ _usingCtx$1.d();
103
+ }
104
+ }
105
+ /** 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. */
107
+ const scopeLabels = {
108
+ iterate: "your projects",
109
+ account: "your account",
110
+ "organizations:write": "your organizations"
111
+ };
112
+ const text = (value) => value.replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);
113
+ /** The gate's own pages, dressed without asking anyone: the issuer's stylesheet is loaded only when
114
+ * the issuer is the deployment's own (a stylesheet is a document's dress, and an issuer a person
115
+ * typed does not get to dress a page on this origin). */
116
+ const 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}`;
117
+ /** A page of the gate's own: `head` is what the head carries beyond the title, `body` the card. */
118
+ function gatePage(input) {
119
+ return new Response(`<!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>`, {
120
+ status: input.status ?? 200,
121
+ headers: {
122
+ "Content-Type": "text/html; charset=utf-8",
123
+ "Cache-Control": "no-store",
124
+ "Content-Security-Policy": input.csp,
125
+ "X-Frame-Options": "DENY"
126
+ }
127
+ });
128
+ }
129
+ /** 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) {
132
+ 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
+ mark: `<img class="issuer-mark" src="${issuer}/iterate-logo.svg" alt="" width="56" height="56">`
136
+ };
137
+ return {
138
+ head: `<style>${inlinePageCss}</style>`,
139
+ csp: "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'",
140
+ mark: ""
141
+ };
142
+ }
143
+ /** The sign-in this browser holds does not cover what the page needs: a project the page named
144
+ * (`?project=`, the page's own word — its `/projects/<ref>` was absent from the session's list; the
145
+ * page is not verified, it only offers a sign-out the person must click) or a permission the app
146
+ * asks for. One page for both, on the app's origin because ending its session is a same-origin
147
+ * POST. It shows who the app is signed in as, through which issuer, and the projects that sign-in
148
+ * reaches (its grant), then one button that ends this app's session and comes straight back to
149
+ * `/.auth/login`, which starts a fresh one — the issuer still knows the person, so they land on
150
+ * consent: the project to tick, or Switch account — and returns to the page that sent them. */
151
+ async function signInAgainPage(input) {
152
+ const { url, issuer, resource, defaultIssuer, bearer, scopes, project } = input;
153
+ const lacks = project ? `does not include a project called <code>${text(project)}</code>` : "does not include a permission this app asks for";
154
+ const grant = await grantSummary(resource, bearer).catch(() => null);
155
+ const reaches = grant ? grant.projects.length ? `It reaches ${grant.projects.slice(0, 12).map((slug) => `<code>${text(slug)}</code>`).join(", ")}${grant.projects.length > 12 ? ` and ${grant.projects.length - 12} more` : ""}.` : "It reaches no projects yet." : "";
156
+ const permissions = `Permissions: ${scopes.map((scope) => scopeLabels[scope] || scope).join(", ")}.`;
157
+ const signedIn = [
158
+ grant?.email ? `Signed in as <strong>${text(grant.email)}</strong>` : "Signed in",
159
+ ` through <strong>${text(new URL(issuer).host)}</strong>.`,
160
+ reaches,
161
+ permissions
162
+ ].filter(Boolean).join(" ").replace("</strong> through", "</strong> through");
163
+ const dress = dressOf(issuer, defaultIssuer);
164
+ const back = nextPathOf(url.pathname + url.search, url.origin);
165
+ return gatePage({
166
+ title: "Sign in again",
167
+ head: dress.head,
168
+ csp: dress.csp,
169
+ 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>`
170
+ });
171
+ }
172
+ /** `/.auth/connect` — the one page that may bind this browser to an issuer other than the
173
+ * deployment's own: it names the issuer's host in plain text, says what it will end, and asks. The
174
+ * Continue button is a same-origin POST; the page loads nothing from the issuer it names. */
175
+ function connectPage(input) {
176
+ const { url, origin, next, scope, held, defaultIssuer } = input;
177
+ const host = new URL(origin).host;
178
+ 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);
180
+ return gatePage({
181
+ title: `Connect to ${host}`,
182
+ head: dress.head,
183
+ csp: dress.csp,
184
+ 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>`
185
+ });
186
+ }
187
+ /** A refusal of the gate's own, as a page the person can read. */
188
+ function refusalPage(defaultIssuer, message, status = 400) {
189
+ const dress = dressOf(defaultIssuer, defaultIssuer);
190
+ return gatePage({
191
+ title: "Cannot connect",
192
+ head: dress.head,
193
+ csp: dress.csp,
194
+ status,
195
+ body: `${dress.mark}<h1>Cannot connect</h1><p>${text(message)}</p><p><a class="button" href="/">Back</a></p>`
196
+ });
197
+ }
198
+ /** The same OAuth client and /api proxy on a platform host or a separate app worker. */
199
+ async function appAuth(request, config) {
200
+ const url = new URL(request.url);
201
+ const { issuer, resource, sessions } = config;
202
+ const client = config.client && {
203
+ name: config.client.name,
204
+ logoUri: new URL(config.client.logoUri, url.origin).href
205
+ };
206
+ const session = appSession(sessions, request);
207
+ const clearCookie = `${sessionCookieName(url)}=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0`;
208
+ /** Where THIS browser's credential goes: its session's record, else the deployment's own. A record
209
+ * bound to a CONNECTED issuer whose resource is not that issuer's `/api` is not one this code
210
+ * wrote — it is dropped (the deployment's own issuer may name its resource as it likes). */
211
+ const held = async () => {
212
+ const host = await session?.host();
213
+ if (!host) return null;
214
+ if (host.issuer !== issuer && host.resource !== `${host.issuer}/api`) {
215
+ await session.discard();
216
+ return null;
217
+ }
218
+ return host;
219
+ };
220
+ if (url.pathname === "/.auth/client.json") return Response.json({
221
+ client_id: `${url.origin}/.auth/client.json`,
222
+ client_name: client?.name || url.host,
223
+ client_uri: url.origin,
224
+ logo_uri: client?.logoUri,
225
+ redirect_uris: [`${url.origin}/.auth/callback`],
226
+ token_endpoint_auth_method: "none",
227
+ grant_types: ["authorization_code", "refresh_token"],
228
+ response_types: ["code"]
229
+ }, { headers: { "Cache-Control": "public, max-age=300" } });
230
+ if (url.pathname === "/.auth/session.json") {
231
+ if (request.method !== "GET") return new Response("Method not allowed", { status: 405 });
232
+ const host = await held();
233
+ return Response.json({
234
+ issuer: host?.issuer ?? null,
235
+ defaultIssuer: issuer
236
+ }, { headers: { "Cache-Control": "no-store" } });
237
+ }
238
+ if (url.pathname === "/.auth/connect") {
239
+ if (config.loginPage) return new Response("Not found", { status: 404 });
240
+ if (request.method === "GET") {
241
+ const named = issuerOriginOf(url.searchParams.get("issuer") ?? "", {
242
+ defaultIssuer: issuer,
243
+ denyZones: config.denyZones
244
+ });
245
+ if ("error" in named) return refusalPage(issuer, named.error);
246
+ const next = nextPathOf(url.searchParams.get("next"), url.origin);
247
+ const scope = url.searchParams.get("scope");
248
+ const host = await held();
249
+ if (named.origin === issuer && (!host || host.issuer === issuer)) return new Response(null, {
250
+ status: 303,
251
+ headers: {
252
+ Location: `/.auth/login?${new URLSearchParams({
253
+ next,
254
+ scope: scope || ""
255
+ })}`,
256
+ "Cache-Control": "no-store"
257
+ }
258
+ });
259
+ return connectPage({
260
+ url,
261
+ origin: named.origin,
262
+ next,
263
+ scope,
264
+ held: host,
265
+ defaultIssuer: issuer
266
+ });
267
+ }
268
+ if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
269
+ if (!isSameOriginBrowserRequest(request)) return new Response("Cross-site request refused", { status: 403 });
270
+ const form = await request.formData();
271
+ const named = issuerOriginOf(String(form.get("issuer") ?? ""), {
272
+ defaultIssuer: issuer,
273
+ denyZones: config.denyZones
274
+ });
275
+ if ("error" in named) return refusalPage(issuer, named.error);
276
+ const next = nextPathOf(String(form.get("next") ?? "/"), url.origin);
277
+ const parsed = OAuthScopes.safeParse(String(form.get("scope") ?? "").split(" ").filter(Boolean));
278
+ if (!parsed.success) return refusalPage(issuer, "Unsupported permission.");
279
+ if (named.origin !== issuer) {
280
+ const refused = await issuerAnswersAt(named.origin);
281
+ if (refused) return refusalPage(issuer, refused);
282
+ }
283
+ const host = await held();
284
+ if (host?.issuer === named.origin) return new Response(null, {
285
+ status: 303,
286
+ headers: {
287
+ Location: `/.auth/login?${new URLSearchParams({
288
+ next,
289
+ scope: parsed.data.join(" ")
290
+ })}`,
291
+ "Cache-Control": "no-store"
292
+ }
293
+ });
294
+ if (host) try {
295
+ await session.end();
296
+ } catch {
297
+ await session.discard();
298
+ }
299
+ const { location, setCookie } = await startAppSession(sessions, {
300
+ origin: url.origin,
301
+ client,
302
+ issuer: named.origin,
303
+ resource: `${named.origin}/api`,
304
+ scopes: parsed.data
305
+ }, next);
306
+ return new Response(null, {
307
+ status: 302,
308
+ headers: {
309
+ Location: location,
310
+ "Set-Cookie": setCookie,
311
+ "Cache-Control": "no-store",
312
+ "Referrer-Policy": "no-referrer"
313
+ }
314
+ });
315
+ }
316
+ if (url.pathname === "/.auth/login") {
317
+ if (request.method !== "GET") return new Response("Method not allowed", { status: 405 });
318
+ const next = nextPathOf(url.searchParams.get("next"), url.origin);
319
+ const scope = url.searchParams.get("scope");
320
+ const parsed = OAuthScopes.safeParse(scope ? scope.split(" ").filter(Boolean) : []);
321
+ if (!parsed.success) return new Response("Unsupported permission", { status: 400 });
322
+ const scopes = parsed.data;
323
+ const requested = url.searchParams.get("issuer");
324
+ if (requested && !config.loginPage) {
325
+ const named = issuerOriginOf(requested, {
326
+ defaultIssuer: issuer,
327
+ denyZones: config.denyZones
328
+ });
329
+ if ("error" in named || named.origin !== issuer) return new Response(null, {
330
+ status: 303,
331
+ headers: {
332
+ Location: `/.auth/connect?${new URLSearchParams({
333
+ issuer: requested,
334
+ next,
335
+ scope: scope || ""
336
+ })}`,
337
+ "Cache-Control": "no-store"
338
+ }
339
+ });
340
+ }
341
+ const target = await held() || {
342
+ issuer,
343
+ resource
344
+ };
345
+ const bearer = await session?.bearer();
346
+ if (bearer) {
347
+ const probe = await config.api(new Request(target.resource, {
348
+ method: "POST",
349
+ body: "",
350
+ headers: { Authorization: `Bearer ${bearer}` }
351
+ }));
352
+ await probe.body?.cancel();
353
+ if (probe.status !== 401) {
354
+ if (!probe.ok) throw new Error(`iterate API check failed (${probe.status})`);
355
+ const heldScopes = await session.scopes();
356
+ const project = url.searchParams.get("project");
357
+ if (!project && scopes.every((scope) => heldScopes.includes(scope))) return new Response(null, {
358
+ status: 303,
359
+ headers: {
360
+ Location: next,
361
+ "Cache-Control": "no-store"
362
+ }
363
+ });
364
+ return signInAgainPage({
365
+ url,
366
+ issuer: target.issuer,
367
+ resource: target.resource,
368
+ defaultIssuer: issuer,
369
+ bearer,
370
+ scopes: heldScopes,
371
+ project
372
+ });
373
+ }
374
+ await session.discard();
375
+ }
376
+ if (config.loginPage) return new Response(null, {
377
+ status: 303,
378
+ headers: {
379
+ Location: `${config.loginPage}?${new URLSearchParams({ next })}`,
380
+ "Cache-Control": "no-store"
381
+ }
382
+ });
383
+ const { location, setCookie } = await startAppSession(sessions, {
384
+ origin: url.origin,
385
+ issuer: target.issuer,
386
+ resource: target.resource,
387
+ scopes,
388
+ client
389
+ }, next);
390
+ return new Response(null, {
391
+ status: 302,
392
+ headers: {
393
+ Location: location,
394
+ "Set-Cookie": setCookie,
395
+ "Cache-Control": "no-store",
396
+ "Referrer-Policy": "no-referrer"
397
+ }
398
+ });
399
+ }
400
+ if (url.pathname === "/.auth/callback") {
401
+ if (request.method !== "GET") return new Response("Method not allowed", { status: 405 });
402
+ const result = await session?.complete(url.search);
403
+ if (!result || result.error) return new Response(result?.error || "Sign-in does not match this browser.", {
404
+ status: 400,
405
+ headers: {
406
+ "Cache-Control": "no-store",
407
+ "Referrer-Policy": "no-referrer"
408
+ }
409
+ });
410
+ return new Response(null, {
411
+ status: 303,
412
+ headers: {
413
+ Location: result.next,
414
+ "Cache-Control": "no-store",
415
+ "Referrer-Policy": "no-referrer"
416
+ }
417
+ });
418
+ }
419
+ if (url.pathname === "/.auth/logout") {
420
+ if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
421
+ if (!isSameOriginBrowserRequest(request)) return new Response("Cross-site request refused", { status: 403 });
422
+ const ended = await held();
423
+ try {
424
+ await session?.end();
425
+ } catch {
426
+ console.error("oauth.app_logout_failed", { origin: url.origin });
427
+ return new Response("Sign-out could not complete. Your session is still present; go back and try again.", { status: 503 });
428
+ }
429
+ const next = nextPathOf(url.searchParams.get("next"), url.origin);
430
+ const nextUrl = new URL(next, url.origin);
431
+ const scope = nextUrl.pathname === "/.auth/login" ? nextUrl.searchParams.get("scope") || "" : "";
432
+ const headers = new Headers({
433
+ Location: ended && ended.issuer !== issuer && !config.loginPage ? `/.auth/connect?${new URLSearchParams({
434
+ issuer: ended.issuer,
435
+ next,
436
+ scope
437
+ })}` : next,
438
+ "Set-Cookie": clearCookie,
439
+ "Cache-Control": "no-store"
440
+ });
441
+ return new Response(null, {
442
+ status: 303,
443
+ headers
444
+ });
445
+ }
446
+ if (url.pathname === "/api") {
447
+ let token = request.headers.get("authorization");
448
+ let cookieAuth = false;
449
+ if (!token && request.method !== "OPTIONS") {
450
+ const bearer = session && request.headers.get("origin") === url.origin ? await session.bearer() : null;
451
+ if (bearer) {
452
+ token = `Bearer ${bearer}`;
453
+ cookieAuth = true;
454
+ }
455
+ }
456
+ const target = await held() ?? {
457
+ issuer,
458
+ resource
459
+ };
460
+ const headers = new Headers(request.headers);
461
+ headers.delete("cookie");
462
+ if (request.headers.get("upgrade")?.toLowerCase() === "websocket") headers.delete("origin");
463
+ if (token) headers.set("authorization", token);
464
+ const response = await config.api(new Request(target.resource, new Request(request, { headers })));
465
+ if (cookieAuth && response.status === 401) {
466
+ await session.discard();
467
+ const denied = new Response(response.body, response);
468
+ denied.headers.append("Set-Cookie", clearCookie);
469
+ return denied;
470
+ }
471
+ return response;
472
+ }
473
+ if (url.pathname.startsWith("/.auth/")) return new Response("Not found", { status: 404 });
474
+ return null;
475
+ }
476
+ //#endregion
477
+ export { appAuth, appSession, issuerOriginOf, startAppSession };
478
+
479
+ //# sourceMappingURL=app-server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-server.mjs","names":[],"sources":["../../src/next/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 os-next 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.\n// eslint-disable-next-line iterate/no-capnweb-http-batch -- One bounded read for the Sign-in-again page, no live capabilities.\nimport { newHttpBatchRpcSession } from \"capnweb\";\nimport type { BrowserHost, BrowserSession } from \"./app-session.ts\";\nimport type { IterateApi } from \"./api.ts\";\nimport { cookieValueOf } from \"./principal.ts\";\nimport { isSameOriginBrowserRequest, sameOriginPath } from \"./lib.ts\";\nimport { OAuthScopes } from \"./oauth-scopes.ts\";\n\n/** The port separates local apps sharing localhost's cookie jar. */\nfunction 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. */\nasync 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 // eslint-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: \"from-server-cookie\" });\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 consent page (public/authorize.js) spells the same three at sentence length. */\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. */\nfunction dressOf(\n issuer: string,\n defaultIssuer: string,\n): { head: string; csp: string; mark: string } {\n if (issuer === defaultIssuer)\n return {\n head: `<link rel=\"stylesheet\" href=\"${issuer}/issuer.css\"><link rel=\"icon\" href=\"${issuer}/iterate-logo.svg\" type=\"image/svg+xml\">`,\n csp: `default-src 'none'; style-src ${issuer}; img-src ${issuer}; form-action 'self'; 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'; form-action 'self'; 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 .replace(\"</strong> through\", \"</strong> through\");\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);\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 })` (os-next\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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"}
@@ -0,0 +1,49 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+ export type BrowserHost = {
3
+ origin: string;
4
+ issuer: string;
5
+ resource: string;
6
+ scopes: string[];
7
+ client?: {
8
+ id?: string;
9
+ name: string;
10
+ logoUri: string;
11
+ };
12
+ };
13
+ /** Each app binds this same class. Token exchange and logout use the issuer's
14
+ * public protocol, so separately deployed apps need no platform bindings.
15
+ * The input gate serializes refresh and logout across tabs. */
16
+ export declare class BrowserSession extends DurableObject {
17
+ #private;
18
+ begin(host: BrowserHost, next: string): Promise<string>;
19
+ /** The issuer's redirect back to `/.auth/callback`, its query string. oauth4webapi validates it
20
+ * (the state matches this browser's pending flow, the `iss` matches the issuer, no `error`), then
21
+ * exchanges the code for tokens. */
22
+ complete(callbackQuery: string): Promise<{
23
+ error: string;
24
+ next?: undefined;
25
+ } | {
26
+ next: string;
27
+ error?: undefined;
28
+ }>;
29
+ bearer(): Promise<string | null>;
30
+ /** THE ISSUER THIS SESSION IS BOUND TO and the resource its tokens are for — written once at
31
+ * `begin`, never steered by a request: the app's `/api` proxy, its login probe and its logout read
32
+ * them from here, so a browser connected to one issuer can only ever spend its credential there.
33
+ * Null when no session was begun. */
34
+ host(): Promise<{
35
+ issuer: string;
36
+ resource: string;
37
+ } | null>;
38
+ scopes(): Promise<string[]>;
39
+ /** Public client metadata chosen by the app before consent; never a browser-supplied claim. */
40
+ client(): Promise<{
41
+ id?: string;
42
+ name: string;
43
+ logoUri: string;
44
+ } | undefined>;
45
+ /** A verified 401 means this local credential no longer grants access. */
46
+ discard(): Promise<void>;
47
+ end(): Promise<void>;
48
+ alarm(): Promise<void>;
49
+ }