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.
- package/README.md +164 -82
- package/dist/{next/api.d.ts → api.d.ts} +197 -33
- package/dist/{next/app-server.d.ts → app-server.d.ts} +7 -0
- package/dist/{next/app-server.mjs → app-server.mjs} +17 -15
- package/dist/app-server.mjs.map +1 -0
- package/dist/{next/app-session.mjs → app-session.mjs} +12 -15
- package/dist/app-session.mjs.map +1 -0
- package/dist/{next/app.mjs → app.mjs} +53 -14
- package/dist/app.mjs.map +1 -0
- package/dist/{next/client → client}/live-state.d.ts +11 -11
- package/dist/client/oauth.d.ts +17 -0
- package/dist/{next/client → client}/react.d.ts +10 -42
- package/dist/{next/client → client}/socket.d.ts +1 -0
- package/dist/client.mjs +156 -4
- package/dist/client.mjs.map +1 -0
- package/dist/{next/expression.d.ts → expression.d.ts} +11 -69
- package/dist/{next/expression.mjs → expression.mjs} +11 -109
- package/dist/expression.mjs.map +1 -0
- package/dist/lib-BWr-5mFO.mjs +36 -0
- package/dist/lib-BWr-5mFO.mjs.map +1 -0
- package/dist/{next/lib.d.ts → lib.d.ts} +16 -2
- package/dist/{next/lib.mjs → lib.mjs} +32 -3
- package/dist/lib.mjs.map +1 -0
- package/dist/node.d.ts +15 -3
- package/dist/node.mjs +36 -174
- package/dist/node.mjs.map +1 -1
- package/dist/{next/oauth-scopes.mjs → oauth-scopes.mjs} +1 -1
- package/dist/oauth-scopes.mjs.map +1 -0
- package/dist/{next/oauth.mjs → oauth.mjs} +14 -2
- package/dist/oauth.mjs.map +1 -0
- package/dist/principal.d.ts +8 -0
- package/dist/principal.mjs +8 -0
- package/dist/principal.mjs.map +1 -0
- package/dist/project-ingress.d.ts +58 -0
- package/dist/project-ingress.mjs +104 -0
- package/dist/project-ingress.mjs.map +1 -0
- package/dist/{next/react.mjs → react.mjs} +12 -12
- package/dist/react.mjs.map +1 -0
- package/dist/sdk/auth.d.ts +25 -0
- package/dist/sdk/index.d.ts +155 -0
- package/dist/sdk/record-pipelined-steps.d.ts +19 -0
- package/dist/sdk.mjs +245 -2
- package/dist/sdk.mjs.map +1 -0
- package/dist/{next/stream → stream}/processor.d.ts +28 -23
- package/dist/{next/stream → stream}/processor.mjs +48 -25
- package/dist/stream/processor.mjs.map +1 -0
- package/dist/{next/stream → stream}/run.d.ts +9 -6
- package/dist/{next/stream → stream}/run.mjs +13 -8
- package/dist/stream/run.mjs.map +1 -0
- package/dist/stream/test-support.d.ts +45 -0
- package/dist/stream/test-support.mjs +196 -0
- package/dist/stream/test-support.mjs.map +1 -0
- package/package.json +65 -219
- package/THIRD_PARTY_NOTICES.md +0 -55
- package/bin/iterate.js +0 -94
- package/dist/api-url-B6404M82.mjs +0 -17
- package/dist/api-url-B6404M82.mjs.map +0 -1
- package/dist/app-ref-BipL0feU.mjs +0 -35
- package/dist/app-ref-BipL0feU.mjs.map +0 -1
- package/dist/app-ref-C1CrgXqX.mjs +0 -7
- package/dist/app-ref-C1CrgXqX.mjs.map +0 -1
- package/dist/app-ref-DYai_om1.mjs +0 -7
- package/dist/app-ref-DYai_om1.mjs.map +0 -1
- package/dist/cli-D0c-pDL_.mjs +0 -1010
- package/dist/cli-D0c-pDL_.mjs.map +0 -1
- package/dist/client.d.ts +0 -3
- package/dist/cloudflare-BTm90gQ4.mjs +0 -951
- package/dist/cloudflare-BTm90gQ4.mjs.map +0 -1
- package/dist/contract-s4FW4eES.mjs +0 -309
- package/dist/contract-s4FW4eES.mjs.map +0 -1
- package/dist/document-review/index.d.ts +0 -5
- package/dist/document-review/types.d.ts +0 -107
- package/dist/document-review.mjs +0 -7015
- package/dist/document-review.mjs.map +0 -1
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs +0 -205
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +0 -1
- package/dist/idempotency-DleloJNt.mjs +0 -28
- package/dist/idempotency-DleloJNt.mjs.map +0 -1
- package/dist/index.d.mts +0 -5
- package/dist/index.mjs +0 -8
- package/dist/index.mjs.map +0 -1
- package/dist/itx/api-url.d.ts +0 -6
- package/dist/itx/itx-node-client.d.ts +0 -65
- package/dist/itx/itx-session.d.ts +0 -215
- package/dist/itx/owned-rpc-session.d.ts +0 -14
- package/dist/itx/query-client.d.ts +0 -10
- package/dist/itx-api.generated.d.ts +0 -6195
- package/dist/itx-session-sjud8GiT.mjs +0 -534
- package/dist/itx-session-sjud8GiT.mjs.map +0 -1
- package/dist/live-state-BJNqOwFw.mjs +0 -299
- package/dist/live-state-BJNqOwFw.mjs.map +0 -1
- package/dist/next/app-server.mjs.map +0 -1
- package/dist/next/app-session.mjs.map +0 -1
- package/dist/next/app.mjs.map +0 -1
- package/dist/next/client/oauth.d.ts +0 -12
- package/dist/next/client.mjs +0 -156
- package/dist/next/client.mjs.map +0 -1
- package/dist/next/expression.mjs.map +0 -1
- package/dist/next/lib.mjs.map +0 -1
- package/dist/next/oauth-scopes.mjs.map +0 -1
- package/dist/next/oauth.mjs.map +0 -1
- package/dist/next/principal.d.ts +0 -64
- package/dist/next/principal.mjs +0 -98
- package/dist/next/principal.mjs.map +0 -1
- package/dist/next/project-ingress.d.ts +0 -37
- package/dist/next/project-ingress.mjs +0 -75
- package/dist/next/project-ingress.mjs.map +0 -1
- package/dist/next/react.mjs.map +0 -1
- package/dist/next/sdk/auth.d.ts +0 -5
- package/dist/next/sdk/index.d.ts +0 -112
- package/dist/next/sdk.mjs +0 -139
- package/dist/next/sdk.mjs.map +0 -1
- package/dist/next/stream/processor.mjs.map +0 -1
- package/dist/next/stream/run.mjs.map +0 -1
- package/dist/next-node.d.ts +0 -15
- package/dist/next-node.mjs +0 -51
- package/dist/next-node.mjs.map +0 -1
- package/dist/processor-host-capabilities-BMFH3KTM.mjs +0 -56
- package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +0 -1
- package/dist/processors/cloudflare.d.ts +0 -3
- package/dist/processors/durable-object-processor-durability.d.ts +0 -79
- package/dist/processors/event-consumption-metrics.d.ts +0 -82
- package/dist/processors/idempotency.d.ts +0 -13
- package/dist/processors/index.d.ts +0 -12
- package/dist/processors/processor-contracts.d.ts +0 -342
- package/dist/processors/processor-facet.d.ts +0 -186
- package/dist/processors/processor-host-capabilities.d.ts +0 -60
- package/dist/processors/prompt-sections.d.ts +0 -17
- package/dist/processors/rpc-types.d.ts +0 -515
- package/dist/processors/schemas.d.ts +0 -102
- package/dist/processors/stream-handle.d.ts +0 -45
- package/dist/processors/stream-processor-keepalive.d.ts +0 -95
- package/dist/processors/stream-processor-registry.d.ts +0 -233
- package/dist/processors/stream-processor-runner.d.ts +0 -289
- package/dist/processors/stream-processor.d.ts +0 -339
- package/dist/processors/stream-runtime-metrics.d.ts +0 -107
- package/dist/processors/testing.d.ts +0 -302
- package/dist/processors-BoNyeBfQ.mjs +0 -10
- package/dist/processors-BoNyeBfQ.mjs.map +0 -1
- package/dist/processors-cloudflare.mjs +0 -3
- package/dist/processors-testing.mjs +0 -435
- package/dist/processors-testing.mjs.map +0 -1
- package/dist/processors.mjs +0 -52
- package/dist/processors.mjs.map +0 -1
- package/dist/protocol-DnK_f2m6.mjs +0 -251
- package/dist/protocol-DnK_f2m6.mjs.map +0 -1
- package/dist/sdk/capnweb/index.d.ts +0 -2
- package/dist/sdk/capnweb/live-state/compact.d.ts +0 -5
- package/dist/sdk/capnweb/live-state/diff.d.ts +0 -41
- package/dist/sdk/capnweb/live-state/engine.d.ts +0 -44
- package/dist/sdk/capnweb/live-state/index.d.ts +0 -41
- package/dist/sdk/capnweb/live-state/protocol.d.ts +0 -87
- package/dist/sdk/capnweb/live-state/retain.d.ts +0 -23
- package/dist/sdk/capnweb/live-state/store.d.ts +0 -20
- package/dist/sdk/capnweb/live-state/types.d.ts +0 -11
- package/dist/sdk/capnweb/react.d.ts +0 -45
- package/dist/sdk/capnweb/react.mjs +0 -316
- package/dist/sdk/capnweb/react.mjs.map +0 -1
- package/dist/sdk/capnweb.mjs +0 -4
- package/dist/sdk/itx/react.d.ts +0 -191
- package/dist/sdk/itx/react.mjs +0 -383
- package/dist/sdk/itx/react.mjs.map +0 -1
- package/dist/sdk-DMB-IM11.mjs +0 -933
- package/dist/sdk-DMB-IM11.mjs.map +0 -1
- package/dist/sdk.d.ts +0 -339
- package/dist/serve-itx.d.ts +0 -46
- package/dist/starter-apps/flake-dashboard/app-ref.d.ts +0 -31
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs +0 -1055
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/flake-dashboard/contract.d.ts +0 -4839
- package/dist/starter-apps/flake-dashboard/contract.mjs +0 -2
- package/dist/starter-apps/flake-dashboard/index.d.ts +0 -17
- package/dist/starter-apps/flake-dashboard/index.mjs +0 -56
- package/dist/starter-apps/flake-dashboard/index.mjs.map +0 -1
- package/dist/starter-apps/flake-dashboard/worker.d.ts +0 -4607
- package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +0 -8914
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs +0 -17987
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/github-ai-linter/contract.d.ts +0 -9193
- package/dist/starter-apps/github-ai-linter/index.d.ts +0 -10
- package/dist/starter-apps/github-ai-linter/index.mjs +0 -36
- package/dist/starter-apps/github-ai-linter/index.mjs.map +0 -1
- package/dist/starter-apps/github-ai-linter/prompt.d.ts +0 -13
- package/dist/starter-apps/github-ai-linter/review-bot.d.ts +0 -808
- package/dist/starter-apps/github-ai-linter/rules.d.ts +0 -34
- package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +0 -19
- package/dist/starter-apps/github-ai-linter/worker.d.ts +0 -19
- package/dist/starter-apps/github-ai-linter/worker.mjs +0 -947
- package/dist/starter-apps/github-ai-linter/worker.mjs.map +0 -1
- package/dist/starter-apps/guestbook/app-ref.d.ts +0 -27
- package/dist/starter-apps/guestbook/client.d.ts +0 -7
- package/dist/starter-apps/guestbook/client.mjs +0 -59
- package/dist/starter-apps/guestbook/configured-worker.mjs +0 -205
- package/dist/starter-apps/guestbook/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/guestbook/index.d.ts +0 -9
- package/dist/starter-apps/guestbook/index.mjs +0 -31
- package/dist/starter-apps/guestbook/index.mjs.map +0 -1
- package/dist/starter-apps/guestbook/processor.d.ts +0 -2267
- package/dist/starter-apps/guestbook/worker.d.ts +0 -26
- package/dist/starter-apps/guestbook/worker.mjs +0 -191
- package/dist/starter-apps/guestbook/worker.mjs.map +0 -1
- package/dist/starter-apps/media/configured-worker.mjs +0 -577
- package/dist/starter-apps/media/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/media/index.mjs +0 -36
- package/dist/starter-apps/media/index.mjs.map +0 -1
- package/dist/starter-apps/media/ref.mjs +0 -20
- package/dist/starter-apps/media/ref.mjs.map +0 -1
- package/dist/starter-apps/media/worker.mjs +0 -579
- package/dist/starter-apps/media/worker.mjs.map +0 -1
- package/dist/starter-apps/notes/configured-worker.mjs +0 -6134
- package/dist/starter-apps/notes/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/notes/index.mjs +0 -23
- package/dist/starter-apps/notes/index.mjs.map +0 -1
- package/dist/starter-apps/notes/ref.mjs +0 -21
- package/dist/starter-apps/notes/ref.mjs.map +0 -1
- package/dist/starter-apps/notes/worker.mjs +0 -427
- package/dist/starter-apps/notes/worker.mjs.map +0 -1
- package/dist/starter-apps/todo/client.mjs +0 -59
- package/dist/starter-apps/todo/configured-worker.mjs +0 -2864
- package/dist/starter-apps/todo/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/todo/index.d.ts +0 -8
- package/dist/starter-apps/todo/index.mjs +0 -29
- package/dist/starter-apps/todo/index.mjs.map +0 -1
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs +0 -2082
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +0 -1
- package/dist/usingCtx-mZx5nsAW.mjs +0 -11800
- package/dist/usingCtx-mZx5nsAW.mjs.map +0 -1
- package/dist/worker-ref-DZxPDmb_.mjs +0 -390
- package/dist/worker-ref-DZxPDmb_.mjs.map +0 -1
- package/dist/worker.d.mts +0 -33
- package/dist/worker.mjs +0 -18
- package/dist/worker.mjs.map +0 -1
- package/menubar/Iterate.entitlements +0 -12
- package/menubar/Iterate.swift +0 -914
- package/menubar/IterateIcon.swift +0 -145
- package/menubar/README.md +0 -28
- package/menubar/build-menubar-app.sh +0 -59
- /package/dist/{next/api.mjs → api.mjs} +0 -0
- /package/dist/{next/app-session.d.ts → app-session.d.ts} +0 -0
- /package/dist/{next/app.d.ts → app.d.ts} +0 -0
- /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 "
|
|
3
|
+
import { t as _usingCtx } from "./usingCtx-inzbY1Qz.mjs";
|
|
5
4
|
import { newHttpBatchRpcSession } from "capnweb";
|
|
6
|
-
//#region src/
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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}/
|
|
134
|
-
csp: `default-src 'none'; style-src ${issuer}; img-src ${issuer};
|
|
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:
|
|
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
|
-
`
|
|
161
|
+
`through <strong>${text(new URL(issuer).host)}</strong>.`,
|
|
160
162
|
reaches,
|
|
161
163
|
permissions
|
|
162
|
-
].filter(Boolean).join(" ")
|
|
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 "
|
|
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/
|
|
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: "
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
reject(/* @__PURE__ */ new Error(
|
|
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 =
|
|
35
|
-
for (;;) try {
|
|
66
|
+
for (let attempt = 1;; attempt++) try {
|
|
36
67
|
return await once();
|
|
37
68
|
} catch (error) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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/
|
|
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. */
|
package/dist/app.mjs.map
ADDED
|
@@ -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
|
|
3
|
-
* ephemeral event, delivered raw to the subscriber.
|
|
4
|
-
|
|
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
|
|
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
|
|
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. `
|
|
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
|
|
51
|
-
* frames triggers one
|
|
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
|
-
|
|
57
|
-
/** Called after each gap heal attempt: "healed" on a fresh seed, the error when the
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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. `
|
|
63
|
-
*
|
|
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<
|
|
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:
|
|
64
|
+
events: StreamEvent[];
|
|
97
65
|
caughtUp: boolean;
|
|
98
66
|
error?: string;
|
|
99
67
|
processors: {
|
|
100
|
-
rows:
|
|
68
|
+
rows: SubscriptionListEntry[];
|
|
101
69
|
loaded: boolean;
|
|
102
70
|
error?: string;
|
|
103
71
|
};
|