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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib-BWr-5mFO.mjs","names":[],"sources":["../src/lib.ts"],"sourcesContent":["// lib.ts — the pure helpers every layer shares, one platform-neutral file (it rides the SDK bundle\n// and the Node unit tests, so no cloudflare:workers here):\n// errors — `codedError` / `errorCode` / `reportIssue` / `forwardIssues`: THE machine-readable\n// error channel\n// release — `releaseRpcSessions`: dispose the Workers-RPC values a round trip reached\n// patch — `diff` / `applyPatch` / `jsonEqual`: the live-state delta (an RFC 6902 subset)\n// timeout — `withTimeout`: a promise raced against a deadline (code TIMEOUT)\n// origin — `isSameOriginBrowserRequest`: may a request spend the cookies it carries;\n// `cookieValueOf`: one cookie out of a `Cookie` header;\n// `sameOriginPath`, `isLocalOrigin`, `resolveContextPath`: origins and paths\n\n// ── errors ── THE machine-readable error channel, after cloudflare-os\n// (workshop-shared/src/api.ts: plain Error + a `code` own-property via Object.assign, read with\n// `\"code\" in error`). Why this shape and no other: capnweb coerces custom error NAMES to a\n// builtin whitelist and drops subclass identity, but preserves ALL own enumerable properties\n// across the wire (verified at runtime against @iterate-com/capnweb 0.10.0) — and with\n// enhanced_error_serialization (our compat date) own props survive native Workers-RPC hops too.\n// So: never classify by `name`, `instanceof`, or message regex across a hop; check the code.\n// Human messages stay verbatim and greppable — the code rides beside them, never instead.\n// workerd's own stamped flags (`.retryable`, `.overloaded`, `.durableObjectReset`) ride the same\n// own-property channel; honor them rather than inventing a retry taxonomy.\n\n/** The stable machine-readable codes — SCREAMING_SNAKE, defined once, both ends import this. */\ntype ErrorCode =\n | \"SCHEDULE_LIMIT\" // bounded pending/failed scheduled batches per context\n | \"INVALID_INPUT\"\n | \"IDENTITY_CONFLICT\" // verified login cannot replace another linked identity\n | \"GRANT_NOT_FOUND\" // a caller may revoke only a grant in their own inventory\n | \"NO_ITX_EXPRESSION_MATCH\" // no rewrite rule matches the call (default-deny)\n | \"IDEMPOTENCY_CONFLICT\"\n | \"OFFSET_CONFLICT\" // an input's expected `offset` is not the offset it would land at\n | \"EVENT_TOO_LARGE\" // one event's serialized body is over the append ceiling (stream.ts EVENT_BODY_MAX_CHARS)\n | \"REDUCE_CHECKPOINT_TOO_LARGE\" // a reduce's state would not fit one storage cell (stream/processor.ts)\n | \"EVENT_UNREADABLE\" // a stored row's body is not JSON — `data.offset` names it (stream.ts read)\n | \"STREAM_PAUSED\"\n | \"INVALID_CONTEXT\" // a context name / project id the codec refuses (apps/os context/paths.ts `DurableObjectNameCodec`) — coded, so it survives the hop\n | \"EXPRESSION_TOO_LONG\" // a STRING itx expression over ITX_EXPRESSION_STRING_MAX_CHARS — pass the parsed form\n | \"FACET_SOURCE_TOO_LARGE\" // a facet's literal source over FACET_SOURCE_MAX_CHARS (worker-loader.ts) — refused on entry\n | \"INVALID_CREDENTIALS\" // authenticate(): the admin secret did not match, or the credentials named no known kind\n | \"UNAUTHENTICATED\" // authenticate({ type: \"from-server-cookie\" }): no session cookie on the request, or a cross-origin browser's request\n | \"FORBIDDEN\" // projects.get(project): outside the session's reach (a grant narrowed to other projects, or the user is no member of its org); create on a narrowed grant; grants/consent on a session that carries none; organizations.get outside the session's memberships; `cd` in the global namespace (a global context is reached by identity, never by path); a first-party facet off the context the platform hosts it on, or loaded code in the global namespace (apps/os first-party-facet-placement.ts)\n | \"PROJECT_NAME_TAKEN\" // projects.create({ project }): a project of that slug exists in another org\n | \"RPC_STUB_OFFLINE\" // the rpc stub a row names is neither borrowed nor pager-backed right now — or its lend ended mid-call (recalled, returned, broken; the relay re-codes)\n | \"NOT_A_METHOD\" // the dotted path's terminal segment is not callable on the target\n | \"NO_FACET\" // no facet of that name has been loaded into this context\n | \"FACET_ABORTED\" // the facet instance this call ran on was reset by `itx.facets.abort` (apps/os context/facet-host.ts) — its next call starts it fresh\n | \"FACET_NO_UPGRADE\" // a WebSocket upgrade aimed at a facet: a facet answers RPC and plain HTTP, never a socket — sockets terminate at the edge (apps/os context/facet-host.ts)\n | \"WAIT_TIMEOUT\" // waitForEvent expired with no matching event committed\n | \"TIMEOUT\"; // lib.ts withTimeout: the call did not answer within its deadline\n// (There is no separate boundary-validation library: the append method's own runtime guards\n// throw plain Errors; a client is JUST capnweb, so malformed args surface as ordinary errors.)\n\n/** A plain Error carrying `code` (+ optional `data`) as own enumerable properties. */\nexport function codedError(code: ErrorCode, message: string, data?: unknown): Error {\n return Object.assign(new Error(message), data === undefined ? { code } : { code, data });\n}\n\n/** The code of an error that crossed any number of hops — undefined for uncoded errors. */\nexport function errorCode(error: unknown): ErrorCode | undefined {\n return typeof error === \"object\" && error && \"code\" in error\n ? ((error as { code: unknown }).code as ErrorCode)\n : undefined;\n}\n\n// reportIssue — the ONE exit for unexpected failures (cloudflare-os error-reporting.ts, minus\n// its private Reporter Worker): one bounded console.error line; query event=\"issue\" in Workers\n// Logs. A host forwards issues elsewhere too by `forwardIssues(forward)` (apps/os posthog.ts:\n// PostHog Error Tracking) — capture sites never change. Deliberately NO cloudflare:workers\n// import: this file rides the platform-neutral SDK bundle. Reporting must never disturb the\n// caller — armored end to end; worst case it prints nothing.\n\n// Bounds verbatim from cloudflare-os: hostile strings get clipped, never explode a log line.\nconst MAX = { message: 1024, stack: 16_384, string: 256, attributeKeys: 32 } as const;\ntype Scalar = string | number | boolean | null; // attribute values stay queryable scalars\n\n/** One reported issue as `reportIssue` prints it, and the value that was caught. */\nexport type Issue = {\n failureSite: string;\n caught: unknown;\n attributes: Record<string, Scalar>;\n};\nlet forwardIssue: ((issue: Issue) => void) | undefined;\n\n/** Also hand every issue to `forward` (one per isolate; the last call wins). It runs inside\n * `reportIssue`'s armor: a throw is swallowed. */\nexport function forwardIssues(forward: (issue: Issue) => void): void {\n forwardIssue = forward;\n}\n\n/** Print ONE bounded console.error line for an unexpected failure; never throws. */\nexport function reportIssue(\n failureSite: string,\n caught: unknown,\n attributes?: Record<string, Scalar | undefined>,\n): void {\n try {\n const bounded: Record<string, Scalar> = {};\n for (const [key, value] of Object.entries(attributes || {}).slice(0, MAX.attributeKeys)) {\n if (value === undefined) continue;\n bounded[key.slice(0, MAX.string)] =\n typeof value === \"string\" ? value.slice(0, MAX.string) : value;\n }\n const code = errorCode(caught);\n // The thrown value, bounded — name/message/stack read off an Error; an arbitrary object is\n // never walked.\n const error =\n caught instanceof Error\n ? {\n type: (caught.name || \"Error\").slice(0, MAX.string),\n message: caught.message.slice(0, MAX.message),\n ...(caught.stack && { stack: caught.stack.slice(0, MAX.stack) }),\n }\n : typeof caught === \"object\" && caught\n ? { type: \"ObjectThrown\" }\n : { type: `${typeof caught}Thrown`, message: String(caught).slice(0, MAX.message) };\n console.error({\n ...bounded, // fixed keys spread last so an attribute can never shadow them\n event: \"issue\",\n failureSite: failureSite.slice(0, MAX.string),\n code,\n error,\n });\n forwardIssue?.({ failureSite, caught, attributes: bounded });\n } catch {\n // Reporting must never disturb the caller — swallow and move on.\n }\n}\n\n// ── release ── a Workers-RPC value (a stub, a call's promise) keeps its session, and the actor at\n// its far end, open until disposed.\n\n/** Release each of `rpcSessions`, the last first. The answer they served is already in, so a release\n * that throws is reported, never made the call's failure. */\nexport function releaseRpcSessions(rpcSessions: readonly unknown[]): void {\n for (const rpcSession of [...rpcSessions].reverse())\n try {\n // A session-brand value or an RPC result object carries a disposer; a void call's undefined\n // answer, or a plain value, has nothing to release.\n (rpcSession as Partial<Disposable> | undefined)?.[Symbol.dispose]?.();\n } catch (error) {\n reportIssue(\"itx-expression.release-rpc-session\", error);\n }\n}\n\n// ── patch ── the LiveView-style delta that rides every live-state change event. `diff` runs at\n// the PRODUCER (the processor's reduce, the mini-app helper's set) where old and new value are\n// both in hand; the stream forwards the ops verbatim; only CLIENTS apply them. The format is an\n// RFC 6902 subset (add / replace / remove, JSON-Pointer paths) so any off-the-shelf json-patch\n// library applies our patch payloads — we invent no wire format, we just never emit the exotic ops.\n\nexport type PatchOp =\n | { op: \"add\"; path: string; value: unknown }\n | { op: \"replace\"; path: string; value: unknown }\n | { op: \"remove\"; path: string };\n\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === \"object\" && !!v && !Array.isArray(v);\n\nconst escape = (seg: string | number): string =>\n String(seg).replaceAll(\"~\", \"~0\").replaceAll(\"/\", \"~1\");\n\n/** Structural deep-equal over plain JSON values — order-insensitive, unbudgeted (JSON is acyclic).\n * THE one deep-equal: the live-state diff's \"don't emit\" test AND the idempotency-body compare\n * (re-exported through stream/processor.ts). The `Object.hasOwn(b, k)` guard is load-bearing — without\n * it, two objects with the same key COUNT but different key SETS compare equal. */\nexport function jsonEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));\n if (isRecord(a) && isRecord(b)) {\n const ka = Object.keys(a);\n return (\n ka.length === Object.keys(b).length &&\n ka.every((k) => Object.hasOwn(b, k) && jsonEqual(a[k], b[k]))\n );\n }\n return false;\n}\n\n/** Structural diff `a → b`, or undefined when deep-equal (the producer's \"don't emit\" signal).\n * Objects recurse per key; arrays get the chat-log fast paths (pure append → `add …/-` ops,\n * pure tail-truncate → `remove` ops) and are replaced wholesale on any middle divergence —\n * the LiveView trade: optimize the growing log, full-render the rewrite.\n * Both sides are JSON-normalized first — the wire is JSON, so the diff must see exactly what\n * the wire will carry: undefined-valued keys vanish, Dates become their ISO strings, array\n * holes become null. Diffing what you didn't normalize is how a Date change goes silent. */\nexport function diff(a: unknown, b: unknown): PatchOp[] | undefined {\n // stringify can itself return undefined (a bare function, toJSON() → undefined) — parse\n // would then throw SyntaxError('\"undefined\" is not valid JSON'); treat those as absent.\n const json = (v: unknown) => {\n const s = JSON.stringify(v);\n return s ? (JSON.parse(s) as unknown) : undefined;\n };\n return walk(json(a), json(b), \"\");\n}\n\nfunction walk(a: unknown, b: unknown, path: string): PatchOp[] | undefined {\n if (jsonEqual(a, b)) return undefined;\n if (Array.isArray(a) && Array.isArray(b)) {\n const shared = Math.min(a.length, b.length);\n let p = 0;\n while (p < shared && jsonEqual(a[p], b[p])) p++;\n if (p === a.length) return b.slice(p).map((value) => ({ op: \"add\", path: `${path}/-`, value }));\n if (p === b.length)\n return a\n .slice(p)\n .map((_, i) => ({ op: \"remove\", path: `${path}/${a.length - 1 - i}` }) as const);\n return [{ op: \"replace\", path, value: b }];\n }\n if (isRecord(a) && isRecord(b)) {\n const ops: PatchOp[] = [];\n for (const k of Object.keys(a))\n if (!Object.hasOwn(b, k)) ops.push({ op: \"remove\", path: `${path}/${escape(k)}` });\n for (const k of Object.keys(b)) {\n if (!Object.hasOwn(a, k)) ops.push({ op: \"add\", path: `${path}/${escape(k)}`, value: b[k] });\n else ops.push(...(walk(a[k], b[k], `${path}/${escape(k)}`) ?? []));\n }\n return ops.length ? ops : undefined;\n }\n return [{ op: \"replace\", path, value: b }];\n}\n\n/** Apply a patch non-mutatingly (clone-then-mutate). The client half of `diff` — exported\n * through the SDK so subscribers need no third-party json-patch dependency. */\nexport function applyPatch<T>(doc: T, ops: PatchOp[]): T {\n let root: unknown = structuredClone(doc);\n for (const op of ops) {\n if (op.path === \"\") {\n if (op.op === \"remove\") throw new Error(\"applyPatch: cannot remove the document root\");\n root = op.value;\n continue;\n }\n const segs = op.path\n .slice(1)\n .split(\"/\")\n .map((s) => s.replaceAll(\"~1\", \"/\").replaceAll(\"~0\", \"~\"));\n // Patches arrive over the wire. \"__proto__\" is refused outright (assigning it invokes the\n // prototype setter, not a property write); other prototype members (\"constructor\", …) are\n // ordinary keys — traversal below is own-property-only, so the chain is never walked.\n for (const s of segs)\n if (s === \"__proto__\") throw new Error(`applyPatch: refusing __proto__ in path ${op.path}`);\n const last = segs.pop()!;\n let parent: unknown = root;\n for (const s of segs) {\n parent = Array.isArray(parent)\n ? parent[Number(s)]\n : isRecord(parent) && Object.hasOwn(parent, s)\n ? parent[s]\n : undefined;\n if (parent === undefined) throw new Error(`applyPatch: missing path ${op.path}`);\n }\n if (Array.isArray(parent)) {\n if (op.op === \"add\") {\n if (last === \"-\") parent.push(op.value);\n else parent.splice(Number(last), 0, op.value);\n } else if (op.op === \"replace\") parent[Number(last)] = op.value;\n else parent.splice(Number(last), 1);\n } else if (isRecord(parent)) {\n if (op.op === \"remove\") delete parent[last];\n else parent[last] = op.value;\n } else throw new Error(`applyPatch: path ${op.path} traverses a non-container`);\n }\n return root as T;\n}\n\n// ── timeout ── race a promise against a deadline. The timer is CLEARED on every exit: a leaked\n// timer per call would pin a Durable Object awake. A loss rejects with code TIMEOUT so a caller can\n// tell \"took too long\" from the call's own failure (the DO's facet watchdog aborts the facet on it).\n//\n// `what` may be a THUNK: a label whose construction is expensive (the facet watchdog prints the whole\n// pushed batch) is built ONLY when the deadline is actually lost, off the per-call hot path.\n\nexport async function withTimeout<T>(\n promise: Promise<T>,\n ms: number,\n what: string | (() => string),\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () =>\n reject(\n codedError(\n \"TIMEOUT\",\n `${typeof what === \"function\" ? what() : what}: no answer in ${ms / 1000}s`,\n ),\n ),\n ms,\n );\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n// ── origin ── the one check that makes an ambient cookie safe to honour (session.ts\n// `from-server-cookie`, the issuer's form posts in apps/os issuer-pages.ts).\n\n/** Whether `request` may spend the cookies it carries: its `Origin` header is this origin, or absent\n * (a non-browser client — curl, a script). A browser stamps the page's origin on every WebSocket\n * handshake, every cross-site fetch and every form POST, so a foreign origin means a foreign site\n * drove the request with the visitor's cookie riding along. A malformed `Origin` (the literal\n * `null` of a sandboxed document included) is foreign. */\nexport function isSameOriginBrowserRequest(request: Pick<Request, \"url\" | \"headers\">): boolean {\n const origin = request.headers.get(\"origin\");\n // oxlint-disable-next-line iterate/simple-truthiness-check -- security: only a truly absent Origin (non-browser client) is trusted; an empty \"\" Origin header must stay foreign, which a truthiness check would wrongly accept\n if (origin === null) return true;\n try {\n return new URL(origin).origin === new URL(request.url).origin;\n } catch {\n return false;\n }\n}\n\n/** The value of the cookie `name` in a `Cookie` header, or null. */\nexport function cookieValueOf(cookieHeader: string | null, name: string): string | null {\n for (const part of (cookieHeader || \"\").split(\";\")) {\n const separator = part.indexOf(\"=\");\n if (separator < 0) continue;\n if (part.slice(0, separator).trim() === name) return part.slice(separator + 1).trim();\n }\n return null;\n}\n\n/** `next` as a path on `origin`, else \"/\" — a redirect never leaves the host: `//evil.example`,\n * `/\\evil.example` and an absolute URL all resolve to a foreign origin and fall back to \"/\". The\n * issuer's login redirect uses it too (apps/os issuer-pages.ts). */\nexport function sameOriginPath(next: string, origin: string): string {\n try {\n const url = new URL(next, origin);\n return url.origin === origin ? url.pathname + url.search : \"/\";\n } catch {\n return \"/\";\n }\n}\n\n/** Only plain HTTP loopback origins use development login and client registration. */\nexport function isLocalOrigin(origin: string) {\n const url = new URL(origin);\n return (\n url.protocol === \"http:\" &&\n (url.hostname === \"localhost\" ||\n url.hostname.endsWith(\".localhost\") ||\n url.hostname === \"127.0.0.1\")\n );\n}\n\n/** Resolve a `cd` target against a context's own path — the one resolver every `cd` (the edge\n * method, the built-in root, the library's relative handles) shares. Absolute (\"/agents/x\") stands alone; relative\n * (\"agents/x\", \"../inbox\", \".\") joins onto `base`. `.` and `..` resolve; the root cannot be\n * escaped (\"/..\" is \"/\"). The result is canonical: leading slash, no trailing slash but for \"/\". */\nexport function resolveContextPath(basePath: string, contextPath: string): string {\n const segments: string[] = [];\n for (const seg of `${contextPath.startsWith(\"/\") ? \"\" : basePath}/${contextPath}`.split(\"/\")) {\n if (seg === \"\" || seg === \".\") continue;\n if (seg === \"..\") segments.pop();\n else segments.push(seg);\n }\n return `/${segments.join(\"/\")}`;\n}\n"],"mappings":";;AAqDA,SAAgB,WAAW,MAAiB,SAAiB,MAAuB;CAClF,OAAO,OAAO,OAAO,IAAI,MAAM,OAAO,GAAG,SAAS,KAAA,IAAY,EAAE,KAAK,IAAI;EAAE;EAAM;CAAK,CAAC;AACzF;AAoGA,MAAM,YAAY,MAChB,OAAO,MAAM,YAAY,CAAC,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC;;;;;AASlD,SAAgB,UAAU,GAAY,GAAqB;CACzD,IAAI,OAAO,GAAG,GAAG,CAAC,GAAG,OAAO;CAC5B,IAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GACrC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,UAAU,GAAG,EAAE,EAAE,CAAC;CACtE,IAAI,SAAS,CAAC,KAAK,SAAS,CAAC,GAAG;EAC9B,MAAM,KAAK,OAAO,KAAK,CAAC;EACxB,OACE,GAAG,WAAW,OAAO,KAAK,CAAC,CAAC,CAAC,UAC7B,GAAG,OAAO,MAAM,OAAO,OAAO,GAAG,CAAC,KAAK,UAAU,EAAE,IAAI,EAAE,EAAE,CAAC;CAEhE;CACA,OAAO;AACT;AA+FA,eAAsB,YACpB,SACA,IACA,MACY;CACZ,IAAI;CACJ,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CACxB,SACA,IAAI,SAAgB,GAAG,WAAW;GAChC,QAAQ,iBAEJ,OACE,WACE,WACA,GAAG,OAAO,SAAS,aAAa,KAAK,IAAI,KAAK,iBAAiB,KAAK,IAAK,EAC3E,CACF,GACF,EACF;EACF,CAAC,CACH,CAAC;CACH,UAAU;EACR,IAAI,OAAO,aAAa,KAAK;CAC/B;AACF"}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
/** The stable machine-readable codes — SCREAMING_SNAKE, defined once, both ends import this. */
|
|
2
|
-
type ErrorCode = "SCHEDULE_LIMIT" | "INVALID_INPUT" | "IDENTITY_CONFLICT" | "GRANT_NOT_FOUND" | "NO_ITX_EXPRESSION_MATCH" | "IDEMPOTENCY_CONFLICT" | "OFFSET_CONFLICT" | "EVENT_TOO_LARGE" | "REDUCE_CHECKPOINT_TOO_LARGE" | "EVENT_UNREADABLE" | "STREAM_PAUSED" | "INVALID_CONTEXT" | "EXPRESSION_TOO_LONG" | "FACET_SOURCE_TOO_LARGE" | "INVALID_CREDENTIALS" | "UNAUTHENTICATED" | "FORBIDDEN" | "PROJECT_NAME_TAKEN" | "RPC_STUB_OFFLINE" | "NOT_A_METHOD" | "NO_FACET" | "FACET_NO_UPGRADE" | "WAIT_TIMEOUT" | "TIMEOUT";
|
|
2
|
+
type ErrorCode = "SCHEDULE_LIMIT" | "INVALID_INPUT" | "IDENTITY_CONFLICT" | "GRANT_NOT_FOUND" | "NO_ITX_EXPRESSION_MATCH" | "IDEMPOTENCY_CONFLICT" | "OFFSET_CONFLICT" | "EVENT_TOO_LARGE" | "REDUCE_CHECKPOINT_TOO_LARGE" | "EVENT_UNREADABLE" | "STREAM_PAUSED" | "INVALID_CONTEXT" | "EXPRESSION_TOO_LONG" | "FACET_SOURCE_TOO_LARGE" | "INVALID_CREDENTIALS" | "UNAUTHENTICATED" | "FORBIDDEN" | "PROJECT_NAME_TAKEN" | "RPC_STUB_OFFLINE" | "NOT_A_METHOD" | "NO_FACET" | "FACET_ABORTED" | "FACET_NO_UPGRADE" | "WAIT_TIMEOUT" | "TIMEOUT";
|
|
3
3
|
/** A plain Error carrying `code` (+ optional `data`) as own enumerable properties. */
|
|
4
4
|
export declare function codedError(code: ErrorCode, message: string, data?: unknown): Error;
|
|
5
5
|
/** The code of an error that crossed any number of hops — undefined for uncoded errors. */
|
|
6
6
|
export declare function errorCode(error: unknown): ErrorCode | undefined;
|
|
7
7
|
type Scalar = string | number | boolean | null;
|
|
8
|
+
/** One reported issue as `reportIssue` prints it, and the value that was caught. */
|
|
9
|
+
export type Issue = {
|
|
10
|
+
failureSite: string;
|
|
11
|
+
caught: unknown;
|
|
12
|
+
attributes: Record<string, Scalar>;
|
|
13
|
+
};
|
|
14
|
+
/** Also hand every issue to `forward` (one per isolate; the last call wins). It runs inside
|
|
15
|
+
* `reportIssue`'s armor: a throw is swallowed. */
|
|
16
|
+
export declare function forwardIssues(forward: (issue: Issue) => void): void;
|
|
8
17
|
/** Print ONE bounded console.error line for an unexpected failure; never throws. */
|
|
9
18
|
export declare function reportIssue(failureSite: string, caught: unknown, attributes?: Record<string, Scalar | undefined>): void;
|
|
19
|
+
/** Release each of `rpcSessions`, the last first. The answer they served is already in, so a release
|
|
20
|
+
* that throws is reported, never made the call's failure. */
|
|
21
|
+
export declare function releaseRpcSessions(rpcSessions: readonly unknown[]): void;
|
|
10
22
|
export type PatchOp = {
|
|
11
23
|
op: "add";
|
|
12
24
|
path: string;
|
|
@@ -42,9 +54,11 @@ export declare function withTimeout<T>(promise: Promise<T>, ms: number, what: st
|
|
|
42
54
|
* drove the request with the visitor's cookie riding along. A malformed `Origin` (the literal
|
|
43
55
|
* `null` of a sandboxed document included) is foreign. */
|
|
44
56
|
export declare function isSameOriginBrowserRequest(request: Pick<Request, "url" | "headers">): boolean;
|
|
57
|
+
/** The value of the cookie `name` in a `Cookie` header, or null. */
|
|
58
|
+
export declare function cookieValueOf(cookieHeader: string | null, name: string): string | null;
|
|
45
59
|
/** `next` as a path on `origin`, else "/" — a redirect never leaves the host: `//evil.example`,
|
|
46
60
|
* `/\evil.example` and an absolute URL all resolve to a foreign origin and fall back to "/". The
|
|
47
|
-
* issuer's login redirect uses it too (os
|
|
61
|
+
* issuer's login redirect uses it too (apps/os issuer-pages.ts). */
|
|
48
62
|
export declare function sameOriginPath(next: string, origin: string): string;
|
|
49
63
|
/** Only plain HTTP loopback origins use development login and client registration. */
|
|
50
64
|
export declare function isLocalOrigin(origin: string): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region src/
|
|
1
|
+
//#region src/lib.ts
|
|
2
2
|
/** A plain Error carrying `code` (+ optional `data`) as own enumerable properties. */
|
|
3
3
|
function codedError(code, message, data) {
|
|
4
4
|
return Object.assign(new Error(message), data === void 0 ? { code } : {
|
|
@@ -16,6 +16,12 @@ const MAX = {
|
|
|
16
16
|
string: 256,
|
|
17
17
|
attributeKeys: 32
|
|
18
18
|
};
|
|
19
|
+
let forwardIssue;
|
|
20
|
+
/** Also hand every issue to `forward` (one per isolate; the last call wins). It runs inside
|
|
21
|
+
* `reportIssue`'s armor: a throw is swallowed. */
|
|
22
|
+
function forwardIssues(forward) {
|
|
23
|
+
forwardIssue = forward;
|
|
24
|
+
}
|
|
19
25
|
/** Print ONE bounded console.error line for an unexpected failure; never throws. */
|
|
20
26
|
function reportIssue(failureSite, caught, attributes) {
|
|
21
27
|
try {
|
|
@@ -40,8 +46,22 @@ function reportIssue(failureSite, caught, attributes) {
|
|
|
40
46
|
code,
|
|
41
47
|
error
|
|
42
48
|
});
|
|
49
|
+
forwardIssue?.({
|
|
50
|
+
failureSite,
|
|
51
|
+
caught,
|
|
52
|
+
attributes: bounded
|
|
53
|
+
});
|
|
43
54
|
} catch {}
|
|
44
55
|
}
|
|
56
|
+
/** Release each of `rpcSessions`, the last first. The answer they served is already in, so a release
|
|
57
|
+
* that throws is reported, never made the call's failure. */
|
|
58
|
+
function releaseRpcSessions(rpcSessions) {
|
|
59
|
+
for (const rpcSession of [...rpcSessions].reverse()) try {
|
|
60
|
+
rpcSession?.[Symbol.dispose]?.();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
reportIssue("itx-expression.release-rpc-session", error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
45
65
|
const isRecord = (v) => typeof v === "object" && !!v && !Array.isArray(v);
|
|
46
66
|
const escape = (seg) => String(seg).replaceAll("~", "~0").replaceAll("/", "~1");
|
|
47
67
|
/** Structural deep-equal over plain JSON values — order-insensitive, unbudgeted (JSON is acyclic).
|
|
@@ -164,9 +184,18 @@ function isSameOriginBrowserRequest(request) {
|
|
|
164
184
|
return false;
|
|
165
185
|
}
|
|
166
186
|
}
|
|
187
|
+
/** The value of the cookie `name` in a `Cookie` header, or null. */
|
|
188
|
+
function cookieValueOf(cookieHeader, name) {
|
|
189
|
+
for (const part of (cookieHeader || "").split(";")) {
|
|
190
|
+
const separator = part.indexOf("=");
|
|
191
|
+
if (separator < 0) continue;
|
|
192
|
+
if (part.slice(0, separator).trim() === name) return part.slice(separator + 1).trim();
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
167
196
|
/** `next` as a path on `origin`, else "/" — a redirect never leaves the host: `//evil.example`,
|
|
168
197
|
* `/\evil.example` and an absolute URL all resolve to a foreign origin and fall back to "/". The
|
|
169
|
-
* issuer's login redirect uses it too (os
|
|
198
|
+
* issuer's login redirect uses it too (apps/os issuer-pages.ts). */
|
|
170
199
|
function sameOriginPath(next, origin) {
|
|
171
200
|
try {
|
|
172
201
|
const url = new URL(next, origin);
|
|
@@ -194,6 +223,6 @@ function resolveContextPath(basePath, contextPath) {
|
|
|
194
223
|
return `/${segments.join("/")}`;
|
|
195
224
|
}
|
|
196
225
|
//#endregion
|
|
197
|
-
export { applyPatch, codedError, diff, errorCode, isLocalOrigin, isSameOriginBrowserRequest, jsonEqual, reportIssue, resolveContextPath, sameOriginPath, withTimeout };
|
|
226
|
+
export { applyPatch, codedError, cookieValueOf, diff, errorCode, forwardIssues, isLocalOrigin, isSameOriginBrowserRequest, jsonEqual, releaseRpcSessions, reportIssue, resolveContextPath, sameOriginPath, withTimeout };
|
|
198
227
|
|
|
199
228
|
//# sourceMappingURL=lib.mjs.map
|
package/dist/lib.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.mjs","names":[],"sources":["../src/lib.ts"],"sourcesContent":["// lib.ts — the pure helpers every layer shares, one platform-neutral file (it rides the SDK bundle\n// and the Node unit tests, so no cloudflare:workers here):\n// errors — `codedError` / `errorCode` / `reportIssue` / `forwardIssues`: THE machine-readable\n// error channel\n// release — `releaseRpcSessions`: dispose the Workers-RPC values a round trip reached\n// patch — `diff` / `applyPatch` / `jsonEqual`: the live-state delta (an RFC 6902 subset)\n// timeout — `withTimeout`: a promise raced against a deadline (code TIMEOUT)\n// origin — `isSameOriginBrowserRequest`: may a request spend the cookies it carries;\n// `cookieValueOf`: one cookie out of a `Cookie` header;\n// `sameOriginPath`, `isLocalOrigin`, `resolveContextPath`: origins and paths\n\n// ── errors ── THE machine-readable error channel, after cloudflare-os\n// (workshop-shared/src/api.ts: plain Error + a `code` own-property via Object.assign, read with\n// `\"code\" in error`). Why this shape and no other: capnweb coerces custom error NAMES to a\n// builtin whitelist and drops subclass identity, but preserves ALL own enumerable properties\n// across the wire (verified at runtime against @iterate-com/capnweb 0.10.0) — and with\n// enhanced_error_serialization (our compat date) own props survive native Workers-RPC hops too.\n// So: never classify by `name`, `instanceof`, or message regex across a hop; check the code.\n// Human messages stay verbatim and greppable — the code rides beside them, never instead.\n// workerd's own stamped flags (`.retryable`, `.overloaded`, `.durableObjectReset`) ride the same\n// own-property channel; honor them rather than inventing a retry taxonomy.\n\n/** The stable machine-readable codes — SCREAMING_SNAKE, defined once, both ends import this. */\ntype ErrorCode =\n | \"SCHEDULE_LIMIT\" // bounded pending/failed scheduled batches per context\n | \"INVALID_INPUT\"\n | \"IDENTITY_CONFLICT\" // verified login cannot replace another linked identity\n | \"GRANT_NOT_FOUND\" // a caller may revoke only a grant in their own inventory\n | \"NO_ITX_EXPRESSION_MATCH\" // no rewrite rule matches the call (default-deny)\n | \"IDEMPOTENCY_CONFLICT\"\n | \"OFFSET_CONFLICT\" // an input's expected `offset` is not the offset it would land at\n | \"EVENT_TOO_LARGE\" // one event's serialized body is over the append ceiling (stream.ts EVENT_BODY_MAX_CHARS)\n | \"REDUCE_CHECKPOINT_TOO_LARGE\" // a reduce's state would not fit one storage cell (stream/processor.ts)\n | \"EVENT_UNREADABLE\" // a stored row's body is not JSON — `data.offset` names it (stream.ts read)\n | \"STREAM_PAUSED\"\n | \"INVALID_CONTEXT\" // a context name / project id the codec refuses (apps/os context/paths.ts `DurableObjectNameCodec`) — coded, so it survives the hop\n | \"EXPRESSION_TOO_LONG\" // a STRING itx expression over ITX_EXPRESSION_STRING_MAX_CHARS — pass the parsed form\n | \"FACET_SOURCE_TOO_LARGE\" // a facet's literal source over FACET_SOURCE_MAX_CHARS (worker-loader.ts) — refused on entry\n | \"INVALID_CREDENTIALS\" // authenticate(): the admin secret did not match, or the credentials named no known kind\n | \"UNAUTHENTICATED\" // authenticate({ type: \"from-server-cookie\" }): no session cookie on the request, or a cross-origin browser's request\n | \"FORBIDDEN\" // projects.get(project): outside the session's reach (a grant narrowed to other projects, or the user is no member of its org); create on a narrowed grant; grants/consent on a session that carries none; organizations.get outside the session's memberships; `cd` in the global namespace (a global context is reached by identity, never by path); a first-party facet off the context the platform hosts it on, or loaded code in the global namespace (apps/os first-party-facet-placement.ts)\n | \"PROJECT_NAME_TAKEN\" // projects.create({ project }): a project of that slug exists in another org\n | \"RPC_STUB_OFFLINE\" // the rpc stub a row names is neither borrowed nor pager-backed right now — or its lend ended mid-call (recalled, returned, broken; the relay re-codes)\n | \"NOT_A_METHOD\" // the dotted path's terminal segment is not callable on the target\n | \"NO_FACET\" // no facet of that name has been loaded into this context\n | \"FACET_ABORTED\" // the facet instance this call ran on was reset by `itx.facets.abort` (apps/os context/facet-host.ts) — its next call starts it fresh\n | \"FACET_NO_UPGRADE\" // a WebSocket upgrade aimed at a facet: a facet answers RPC and plain HTTP, never a socket — sockets terminate at the edge (apps/os context/facet-host.ts)\n | \"WAIT_TIMEOUT\" // waitForEvent expired with no matching event committed\n | \"TIMEOUT\"; // lib.ts withTimeout: the call did not answer within its deadline\n// (There is no separate boundary-validation library: the append method's own runtime guards\n// throw plain Errors; a client is JUST capnweb, so malformed args surface as ordinary errors.)\n\n/** A plain Error carrying `code` (+ optional `data`) as own enumerable properties. */\nexport function codedError(code: ErrorCode, message: string, data?: unknown): Error {\n return Object.assign(new Error(message), data === undefined ? { code } : { code, data });\n}\n\n/** The code of an error that crossed any number of hops — undefined for uncoded errors. */\nexport function errorCode(error: unknown): ErrorCode | undefined {\n return typeof error === \"object\" && error && \"code\" in error\n ? ((error as { code: unknown }).code as ErrorCode)\n : undefined;\n}\n\n// reportIssue — the ONE exit for unexpected failures (cloudflare-os error-reporting.ts, minus\n// its private Reporter Worker): one bounded console.error line; query event=\"issue\" in Workers\n// Logs. A host forwards issues elsewhere too by `forwardIssues(forward)` (apps/os posthog.ts:\n// PostHog Error Tracking) — capture sites never change. Deliberately NO cloudflare:workers\n// import: this file rides the platform-neutral SDK bundle. Reporting must never disturb the\n// caller — armored end to end; worst case it prints nothing.\n\n// Bounds verbatim from cloudflare-os: hostile strings get clipped, never explode a log line.\nconst MAX = { message: 1024, stack: 16_384, string: 256, attributeKeys: 32 } as const;\ntype Scalar = string | number | boolean | null; // attribute values stay queryable scalars\n\n/** One reported issue as `reportIssue` prints it, and the value that was caught. */\nexport type Issue = {\n failureSite: string;\n caught: unknown;\n attributes: Record<string, Scalar>;\n};\nlet forwardIssue: ((issue: Issue) => void) | undefined;\n\n/** Also hand every issue to `forward` (one per isolate; the last call wins). It runs inside\n * `reportIssue`'s armor: a throw is swallowed. */\nexport function forwardIssues(forward: (issue: Issue) => void): void {\n forwardIssue = forward;\n}\n\n/** Print ONE bounded console.error line for an unexpected failure; never throws. */\nexport function reportIssue(\n failureSite: string,\n caught: unknown,\n attributes?: Record<string, Scalar | undefined>,\n): void {\n try {\n const bounded: Record<string, Scalar> = {};\n for (const [key, value] of Object.entries(attributes || {}).slice(0, MAX.attributeKeys)) {\n if (value === undefined) continue;\n bounded[key.slice(0, MAX.string)] =\n typeof value === \"string\" ? value.slice(0, MAX.string) : value;\n }\n const code = errorCode(caught);\n // The thrown value, bounded — name/message/stack read off an Error; an arbitrary object is\n // never walked.\n const error =\n caught instanceof Error\n ? {\n type: (caught.name || \"Error\").slice(0, MAX.string),\n message: caught.message.slice(0, MAX.message),\n ...(caught.stack && { stack: caught.stack.slice(0, MAX.stack) }),\n }\n : typeof caught === \"object\" && caught\n ? { type: \"ObjectThrown\" }\n : { type: `${typeof caught}Thrown`, message: String(caught).slice(0, MAX.message) };\n console.error({\n ...bounded, // fixed keys spread last so an attribute can never shadow them\n event: \"issue\",\n failureSite: failureSite.slice(0, MAX.string),\n code,\n error,\n });\n forwardIssue?.({ failureSite, caught, attributes: bounded });\n } catch {\n // Reporting must never disturb the caller — swallow and move on.\n }\n}\n\n// ── release ── a Workers-RPC value (a stub, a call's promise) keeps its session, and the actor at\n// its far end, open until disposed.\n\n/** Release each of `rpcSessions`, the last first. The answer they served is already in, so a release\n * that throws is reported, never made the call's failure. */\nexport function releaseRpcSessions(rpcSessions: readonly unknown[]): void {\n for (const rpcSession of [...rpcSessions].reverse())\n try {\n // A session-brand value or an RPC result object carries a disposer; a void call's undefined\n // answer, or a plain value, has nothing to release.\n (rpcSession as Partial<Disposable> | undefined)?.[Symbol.dispose]?.();\n } catch (error) {\n reportIssue(\"itx-expression.release-rpc-session\", error);\n }\n}\n\n// ── patch ── the LiveView-style delta that rides every live-state change event. `diff` runs at\n// the PRODUCER (the processor's reduce, the mini-app helper's set) where old and new value are\n// both in hand; the stream forwards the ops verbatim; only CLIENTS apply them. The format is an\n// RFC 6902 subset (add / replace / remove, JSON-Pointer paths) so any off-the-shelf json-patch\n// library applies our patch payloads — we invent no wire format, we just never emit the exotic ops.\n\nexport type PatchOp =\n | { op: \"add\"; path: string; value: unknown }\n | { op: \"replace\"; path: string; value: unknown }\n | { op: \"remove\"; path: string };\n\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === \"object\" && !!v && !Array.isArray(v);\n\nconst escape = (seg: string | number): string =>\n String(seg).replaceAll(\"~\", \"~0\").replaceAll(\"/\", \"~1\");\n\n/** Structural deep-equal over plain JSON values — order-insensitive, unbudgeted (JSON is acyclic).\n * THE one deep-equal: the live-state diff's \"don't emit\" test AND the idempotency-body compare\n * (re-exported through stream/processor.ts). The `Object.hasOwn(b, k)` guard is load-bearing — without\n * it, two objects with the same key COUNT but different key SETS compare equal. */\nexport function jsonEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));\n if (isRecord(a) && isRecord(b)) {\n const ka = Object.keys(a);\n return (\n ka.length === Object.keys(b).length &&\n ka.every((k) => Object.hasOwn(b, k) && jsonEqual(a[k], b[k]))\n );\n }\n return false;\n}\n\n/** Structural diff `a → b`, or undefined when deep-equal (the producer's \"don't emit\" signal).\n * Objects recurse per key; arrays get the chat-log fast paths (pure append → `add …/-` ops,\n * pure tail-truncate → `remove` ops) and are replaced wholesale on any middle divergence —\n * the LiveView trade: optimize the growing log, full-render the rewrite.\n * Both sides are JSON-normalized first — the wire is JSON, so the diff must see exactly what\n * the wire will carry: undefined-valued keys vanish, Dates become their ISO strings, array\n * holes become null. Diffing what you didn't normalize is how a Date change goes silent. */\nexport function diff(a: unknown, b: unknown): PatchOp[] | undefined {\n // stringify can itself return undefined (a bare function, toJSON() → undefined) — parse\n // would then throw SyntaxError('\"undefined\" is not valid JSON'); treat those as absent.\n const json = (v: unknown) => {\n const s = JSON.stringify(v);\n return s ? (JSON.parse(s) as unknown) : undefined;\n };\n return walk(json(a), json(b), \"\");\n}\n\nfunction walk(a: unknown, b: unknown, path: string): PatchOp[] | undefined {\n if (jsonEqual(a, b)) return undefined;\n if (Array.isArray(a) && Array.isArray(b)) {\n const shared = Math.min(a.length, b.length);\n let p = 0;\n while (p < shared && jsonEqual(a[p], b[p])) p++;\n if (p === a.length) return b.slice(p).map((value) => ({ op: \"add\", path: `${path}/-`, value }));\n if (p === b.length)\n return a\n .slice(p)\n .map((_, i) => ({ op: \"remove\", path: `${path}/${a.length - 1 - i}` }) as const);\n return [{ op: \"replace\", path, value: b }];\n }\n if (isRecord(a) && isRecord(b)) {\n const ops: PatchOp[] = [];\n for (const k of Object.keys(a))\n if (!Object.hasOwn(b, k)) ops.push({ op: \"remove\", path: `${path}/${escape(k)}` });\n for (const k of Object.keys(b)) {\n if (!Object.hasOwn(a, k)) ops.push({ op: \"add\", path: `${path}/${escape(k)}`, value: b[k] });\n else ops.push(...(walk(a[k], b[k], `${path}/${escape(k)}`) ?? []));\n }\n return ops.length ? ops : undefined;\n }\n return [{ op: \"replace\", path, value: b }];\n}\n\n/** Apply a patch non-mutatingly (clone-then-mutate). The client half of `diff` — exported\n * through the SDK so subscribers need no third-party json-patch dependency. */\nexport function applyPatch<T>(doc: T, ops: PatchOp[]): T {\n let root: unknown = structuredClone(doc);\n for (const op of ops) {\n if (op.path === \"\") {\n if (op.op === \"remove\") throw new Error(\"applyPatch: cannot remove the document root\");\n root = op.value;\n continue;\n }\n const segs = op.path\n .slice(1)\n .split(\"/\")\n .map((s) => s.replaceAll(\"~1\", \"/\").replaceAll(\"~0\", \"~\"));\n // Patches arrive over the wire. \"__proto__\" is refused outright (assigning it invokes the\n // prototype setter, not a property write); other prototype members (\"constructor\", …) are\n // ordinary keys — traversal below is own-property-only, so the chain is never walked.\n for (const s of segs)\n if (s === \"__proto__\") throw new Error(`applyPatch: refusing __proto__ in path ${op.path}`);\n const last = segs.pop()!;\n let parent: unknown = root;\n for (const s of segs) {\n parent = Array.isArray(parent)\n ? parent[Number(s)]\n : isRecord(parent) && Object.hasOwn(parent, s)\n ? parent[s]\n : undefined;\n if (parent === undefined) throw new Error(`applyPatch: missing path ${op.path}`);\n }\n if (Array.isArray(parent)) {\n if (op.op === \"add\") {\n if (last === \"-\") parent.push(op.value);\n else parent.splice(Number(last), 0, op.value);\n } else if (op.op === \"replace\") parent[Number(last)] = op.value;\n else parent.splice(Number(last), 1);\n } else if (isRecord(parent)) {\n if (op.op === \"remove\") delete parent[last];\n else parent[last] = op.value;\n } else throw new Error(`applyPatch: path ${op.path} traverses a non-container`);\n }\n return root as T;\n}\n\n// ── timeout ── race a promise against a deadline. The timer is CLEARED on every exit: a leaked\n// timer per call would pin a Durable Object awake. A loss rejects with code TIMEOUT so a caller can\n// tell \"took too long\" from the call's own failure (the DO's facet watchdog aborts the facet on it).\n//\n// `what` may be a THUNK: a label whose construction is expensive (the facet watchdog prints the whole\n// pushed batch) is built ONLY when the deadline is actually lost, off the per-call hot path.\n\nexport async function withTimeout<T>(\n promise: Promise<T>,\n ms: number,\n what: string | (() => string),\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () =>\n reject(\n codedError(\n \"TIMEOUT\",\n `${typeof what === \"function\" ? what() : what}: no answer in ${ms / 1000}s`,\n ),\n ),\n ms,\n );\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n// ── origin ── the one check that makes an ambient cookie safe to honour (session.ts\n// `from-server-cookie`, the issuer's form posts in apps/os issuer-pages.ts).\n\n/** Whether `request` may spend the cookies it carries: its `Origin` header is this origin, or absent\n * (a non-browser client — curl, a script). A browser stamps the page's origin on every WebSocket\n * handshake, every cross-site fetch and every form POST, so a foreign origin means a foreign site\n * drove the request with the visitor's cookie riding along. A malformed `Origin` (the literal\n * `null` of a sandboxed document included) is foreign. */\nexport function isSameOriginBrowserRequest(request: Pick<Request, \"url\" | \"headers\">): boolean {\n const origin = request.headers.get(\"origin\");\n // oxlint-disable-next-line iterate/simple-truthiness-check -- security: only a truly absent Origin (non-browser client) is trusted; an empty \"\" Origin header must stay foreign, which a truthiness check would wrongly accept\n if (origin === null) return true;\n try {\n return new URL(origin).origin === new URL(request.url).origin;\n } catch {\n return false;\n }\n}\n\n/** The value of the cookie `name` in a `Cookie` header, or null. */\nexport function cookieValueOf(cookieHeader: string | null, name: string): string | null {\n for (const part of (cookieHeader || \"\").split(\";\")) {\n const separator = part.indexOf(\"=\");\n if (separator < 0) continue;\n if (part.slice(0, separator).trim() === name) return part.slice(separator + 1).trim();\n }\n return null;\n}\n\n/** `next` as a path on `origin`, else \"/\" — a redirect never leaves the host: `//evil.example`,\n * `/\\evil.example` and an absolute URL all resolve to a foreign origin and fall back to \"/\". The\n * issuer's login redirect uses it too (apps/os issuer-pages.ts). */\nexport function sameOriginPath(next: string, origin: string): string {\n try {\n const url = new URL(next, origin);\n return url.origin === origin ? url.pathname + url.search : \"/\";\n } catch {\n return \"/\";\n }\n}\n\n/** Only plain HTTP loopback origins use development login and client registration. */\nexport function isLocalOrigin(origin: string) {\n const url = new URL(origin);\n return (\n url.protocol === \"http:\" &&\n (url.hostname === \"localhost\" ||\n url.hostname.endsWith(\".localhost\") ||\n url.hostname === \"127.0.0.1\")\n );\n}\n\n/** Resolve a `cd` target against a context's own path — the one resolver every `cd` (the edge\n * method, the built-in root, the library's relative handles) shares. Absolute (\"/agents/x\") stands alone; relative\n * (\"agents/x\", \"../inbox\", \".\") joins onto `base`. `.` and `..` resolve; the root cannot be\n * escaped (\"/..\" is \"/\"). The result is canonical: leading slash, no trailing slash but for \"/\". */\nexport function resolveContextPath(basePath: string, contextPath: string): string {\n const segments: string[] = [];\n for (const seg of `${contextPath.startsWith(\"/\") ? \"\" : basePath}/${contextPath}`.split(\"/\")) {\n if (seg === \"\" || seg === \".\") continue;\n if (seg === \"..\") segments.pop();\n else segments.push(seg);\n }\n return `/${segments.join(\"/\")}`;\n}\n"],"mappings":"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"}
|
package/dist/node.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { type RpcStub } from "capnweb";
|
|
2
|
+
import type { IterateApi, SessionCredentials } from "./api.ts";
|
|
3
|
+
/** One connection to an Iterate deployment. Dispose the owner to close every child capability and its socket.
|
|
4
|
+
* Operations are never retried: replaying a script could duplicate its effects. */
|
|
5
|
+
export type IterateConnection = Disposable & {
|
|
6
|
+
session: Awaited<ReturnType<RpcStub<IterateApi>["authenticate"]>>;
|
|
7
|
+
closed: Promise<{
|
|
8
|
+
code: number;
|
|
9
|
+
reason: string;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
export declare function connectIterate(input: {
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
auth: SessionCredentials;
|
|
15
|
+
}): Promise<IterateConnection>;
|
package/dist/node.mjs
CHANGED
|
@@ -1,185 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { newWebSocketRpcSession } from "
|
|
1
|
+
import { r as withTimeout } from "./lib-BWr-5mFO.mjs";
|
|
2
|
+
import { newWebSocketRpcSession } from "capnweb";
|
|
3
3
|
import WebSocket from "ws";
|
|
4
|
-
//#region src/
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} });
|
|
17
|
-
}
|
|
18
|
-
function dup(disposable) {
|
|
19
|
-
if (disposable.dup === void 0) throw new Error("Cannot dup scoped RPC stub because an owned stub does not expose dup()");
|
|
20
|
-
return disposable.dup();
|
|
21
|
-
}
|
|
22
|
-
function disposeAll(...disposables) {
|
|
23
|
-
let firstError;
|
|
24
|
-
for (const disposable of disposables) try {
|
|
25
|
-
disposable[Symbol.dispose]?.();
|
|
26
|
-
} catch (error) {
|
|
27
|
-
firstError ??= error;
|
|
28
|
-
}
|
|
29
|
-
if (firstError !== void 0) throw firstError;
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/itx/itx-node-client.ts
|
|
33
|
-
/** Decode a raw ws frame (outbound string, inbound Buffer/ArrayBuffer) into its parsed JSON value. */
|
|
34
|
-
function parseFrame(data) {
|
|
35
|
-
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString("utf8") : ArrayBuffer.isView(data) ? Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8") : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : void 0;
|
|
36
|
-
return text === void 0 ? data : JSON.parse(text);
|
|
37
|
-
}
|
|
38
|
-
function createSocket(url, headers, onWebSocketMessage, onWebSocketClose) {
|
|
39
|
-
const socket = new WebSocket(url, {
|
|
40
|
-
handshakeTimeout: 15e3,
|
|
41
|
-
headers
|
|
4
|
+
//#region src/node.ts
|
|
5
|
+
async function connectIterate(input) {
|
|
6
|
+
const url = new URL("/api", input.baseUrl);
|
|
7
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error("Iterate URL must use http or https.");
|
|
8
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
9
|
+
const socket = new WebSocket(url.href, { handshakeTimeout: 15e3 });
|
|
10
|
+
const root = newWebSocketRpcSession(socket);
|
|
11
|
+
const closed = new Promise((resolve) => {
|
|
12
|
+
socket.once("close", (code, reason) => resolve({
|
|
13
|
+
code,
|
|
14
|
+
reason: reason.toString()
|
|
15
|
+
}));
|
|
42
16
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
17
|
+
socket.on("error", () => {});
|
|
18
|
+
try {
|
|
19
|
+
const session = await withTimeout(root.authenticate(input.auth), 2e4, "Iterate authentication");
|
|
20
|
+
return {
|
|
21
|
+
session,
|
|
22
|
+
closed,
|
|
23
|
+
[Symbol.dispose]() {
|
|
24
|
+
try {
|
|
25
|
+
session[Symbol.dispose]();
|
|
26
|
+
} finally {
|
|
27
|
+
try {
|
|
28
|
+
root[Symbol.dispose]();
|
|
29
|
+
} finally {
|
|
30
|
+
socket.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
52
34
|
};
|
|
53
|
-
|
|
54
|
-
socket.send = ((data, ...args) => {
|
|
55
|
-
record("out", data);
|
|
56
|
-
return send(data, ...args);
|
|
57
|
-
});
|
|
58
|
-
socket.on("message", (data) => record("in", data));
|
|
59
|
-
}
|
|
60
|
-
return socket;
|
|
61
|
-
}
|
|
62
|
-
function observeSocketClose(socket, onWebSocketClose) {
|
|
63
|
-
if (onWebSocketClose) socket.once("close", (code, reason) => onWebSocketClose({
|
|
64
|
-
code,
|
|
65
|
-
reason: reason.toString()
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
function connectItx(input) {
|
|
69
|
-
return createItxConnection(input, createItxSocket(input, input.onWebSocketClose));
|
|
70
|
-
}
|
|
71
|
-
async function connectItxReady(input, options = {}) {
|
|
72
|
-
const retryOptions = options.retryInitialConnection;
|
|
73
|
-
const delayMs = retryOptions === void 0 ? 0 : initialRetryDelay(retryOptions.delayMs);
|
|
74
|
-
for (const attempt of [1, 2]) {
|
|
75
|
-
const startedAt = /* @__PURE__ */ new Date();
|
|
76
|
-
const startedAtPerformance = performance.now();
|
|
77
|
-
const socket = createItxSocket(input);
|
|
35
|
+
} catch (error) {
|
|
78
36
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return connection;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
if (attempt !== 1 || retryOptions === void 0) throw asError(error);
|
|
85
|
-
await retryOptions.onRetry?.({
|
|
86
|
-
attemptDurationMs: performance.now() - startedAtPerformance,
|
|
87
|
-
delayMs,
|
|
88
|
-
error: asError(error),
|
|
89
|
-
failedAttempt: 1,
|
|
90
|
-
nextAttempt: 2,
|
|
91
|
-
startedAt: startedAt.toISOString()
|
|
92
|
-
});
|
|
93
|
-
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
37
|
+
root[Symbol.dispose]();
|
|
38
|
+
} finally {
|
|
39
|
+
socket.terminate();
|
|
94
40
|
}
|
|
95
|
-
}
|
|
96
|
-
throw new Error("unreachable: initial itx connection retry exhausted");
|
|
97
|
-
}
|
|
98
|
-
function createItxSocket(input, onWebSocketClose) {
|
|
99
|
-
return createSocket(apiWebSocketUrl(input.baseUrl).toString(), input.headers, input.onWebSocketMessage, onWebSocketClose);
|
|
100
|
-
}
|
|
101
|
-
function createItxConnection(input, socket) {
|
|
102
|
-
try {
|
|
103
|
-
const session = newWebSocketRpcSession(socket);
|
|
104
|
-
const socketOwner = createSocketOwner(socket, session);
|
|
105
|
-
if (!("auth" in input)) return createSocketOwnedSession(session, socketOwner);
|
|
106
|
-
const root = session.authenticate(input.auth);
|
|
107
|
-
if (!("projectId" in input)) return withOwnedRpcSession(root, socketOwner);
|
|
108
|
-
const project = root.projects.get(input.projectId);
|
|
109
|
-
if (!("agentPath" in input)) return withOwnedRpcSession(project, root, socketOwner);
|
|
110
|
-
return withOwnedRpcSession(project.agents.get(input.agentPath), project, root, socketOwner);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
socket.close(1e3);
|
|
113
41
|
throw error;
|
|
114
42
|
}
|
|
115
43
|
}
|
|
116
|
-
/**
|
|
117
|
-
* One returned scoped itx handle owns one reference to the physical socket.
|
|
118
|
-
* `withOwnedRpcSession()` duplicates every owned value, so each duplicated
|
|
119
|
-
* handle retains its own reference and the last disposal closes the transport.
|
|
120
|
-
*/
|
|
121
|
-
function createSocketOwner(socket, session) {
|
|
122
|
-
let references = 0;
|
|
123
|
-
const retain = () => {
|
|
124
|
-
references += 1;
|
|
125
|
-
let disposed = false;
|
|
126
|
-
return {
|
|
127
|
-
dup: retain,
|
|
128
|
-
[Symbol.dispose]: () => {
|
|
129
|
-
if (disposed) return;
|
|
130
|
-
disposed = true;
|
|
131
|
-
references -= 1;
|
|
132
|
-
if (references !== 0) return;
|
|
133
|
-
socket.close(1e3);
|
|
134
|
-
session[Symbol.dispose]?.();
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
};
|
|
138
|
-
return retain();
|
|
139
|
-
}
|
|
140
|
-
function createSocketOwnedSession(session, socketOwner) {
|
|
141
|
-
return new Proxy(session, { get(target, key, receiver) {
|
|
142
|
-
if (key === "then") return void 0;
|
|
143
|
-
if (key === Symbol.dispose) return () => socketOwner[Symbol.dispose]?.();
|
|
144
|
-
if (key === "dup") return () => createSocketOwnedSession(session, socketOwner.dup());
|
|
145
|
-
return Reflect.get(target, key, receiver);
|
|
146
|
-
} });
|
|
147
|
-
}
|
|
148
|
-
function waitForOpen(socket) {
|
|
149
|
-
if (socket.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
150
|
-
if (socket.readyState !== WebSocket.CONNECTING) return Promise.reject(/* @__PURE__ */ new Error("itx WebSocket closed before connecting"));
|
|
151
|
-
return new Promise((resolve, reject) => {
|
|
152
|
-
const cleanup = () => {
|
|
153
|
-
socket.off("close", onClose);
|
|
154
|
-
socket.off("error", onError);
|
|
155
|
-
socket.off("open", onOpen);
|
|
156
|
-
};
|
|
157
|
-
const onClose = (code, reason) => {
|
|
158
|
-
cleanup();
|
|
159
|
-
reject(/* @__PURE__ */ new Error(`itx WebSocket closed before connecting: ${code} ${reason.toString()}`));
|
|
160
|
-
};
|
|
161
|
-
const onError = (error) => {
|
|
162
|
-
cleanup();
|
|
163
|
-
reject(error);
|
|
164
|
-
};
|
|
165
|
-
const onOpen = () => {
|
|
166
|
-
cleanup();
|
|
167
|
-
resolve();
|
|
168
|
-
};
|
|
169
|
-
socket.once("close", onClose);
|
|
170
|
-
socket.once("error", onError);
|
|
171
|
-
socket.once("open", onOpen);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
function initialRetryDelay(value) {
|
|
175
|
-
const delayMs = value ?? 250;
|
|
176
|
-
if (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > 5e3) throw new Error(`Initial itx retry delay must be between 0 and 5000ms; received ${delayMs}.`);
|
|
177
|
-
return delayMs;
|
|
178
|
-
}
|
|
179
|
-
function asError(error) {
|
|
180
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
181
|
-
}
|
|
182
44
|
//#endregion
|
|
183
|
-
export {
|
|
45
|
+
export { connectIterate };
|
|
184
46
|
|
|
185
47
|
//# sourceMappingURL=node.mjs.map
|
package/dist/node.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.mjs","names":[],"sources":["../src/itx/owned-rpc-session.ts","../src/itx/itx-node-client.ts"],"sourcesContent":["/**\n * Groups an authenticated child stub with the parent stubs that keep it alive.\n *\n * Cap'n Web callers often want `using project = connectItx({ projectId })`, but\n * that project stub is reached through a root session and authentication stub.\n * This proxy makes disposal and `dup()` preserve the whole ownership chain, so\n * disposing the child also tells the server it can release the parent stubs.\n */\n\ntype DisposableLike = {\n [Symbol.dispose]?(): void;\n dup?(): DisposableLike;\n};\n\nexport function withOwnedRpcSession<T extends object>(stub: T, ...owned: DisposableLike[]): T {\n let disposed = false;\n return new Proxy(stub, {\n get(target, key, receiver) {\n // A scoped handle is a settled ownership boundary. Keeping Cap'n Web's\n // promise assimilation here would let an async function unwrap this\n // proxy and lose its parent and transport disposers before returning it.\n if (key === \"then\") return undefined;\n if (key === Symbol.dispose) {\n return () => {\n if (disposed) return;\n disposed = true;\n disposeAll(target as DisposableLike, ...owned);\n };\n }\n if (key === \"dup\") {\n return () => withOwnedRpcSession(dup(target as DisposableLike), ...owned.map(dup));\n }\n return Reflect.get(target, key, receiver);\n },\n });\n}\n\nfunction dup(disposable: DisposableLike): DisposableLike {\n if (disposable.dup === undefined) {\n throw new Error(\"Cannot dup scoped RPC stub because an owned stub does not expose dup()\");\n }\n return disposable.dup();\n}\n\nfunction disposeAll(...disposables: DisposableLike[]): void {\n let firstError: unknown;\n for (const disposable of disposables) {\n try {\n disposable[Symbol.dispose]?.();\n } catch (error) {\n firstError ??= error;\n }\n }\n if (firstError !== undefined) throw firstError;\n}\n","import WebSocket from \"ws\";\nimport { newWebSocketRpcSession, type RpcStub as CapnRpcStub } from \"@iterate-com/capnweb\";\nimport type {\n Agent,\n ItxAuthCredentials,\n Project,\n Session,\n UnauthenticatedOs,\n} from \"../itx-api.generated.ts\";\nimport { apiWebSocketUrl } from \"./api-url.ts\";\nimport { withOwnedRpcSession } from \"./owned-rpc-session.ts\";\n\nexport type ItxWebSocketMessage = [timestamp: number, direction: \"in\" | \"out\", data: unknown];\n\ntype ConnectItxBaseInput = {\n /** OS deployment base URL, e.g. the config's APP_CONFIG_BASE_URL. */\n baseUrl: string;\n /** Node WebSocket handshake headers, used by CLI/server callers with cookies. */\n headers?: Record<string, string>;\n /** Observe every decoded ws frame (e.g. the e2e suite's frame recorder). */\n onWebSocketMessage?: (message: ItxWebSocketMessage) => void;\n /**\n * Observe the returned connection closing. Failed pre-ready dials use onRetry.\n *\n * A client whose job is to stay connected (a device, a long-lived agent)\n * reconnects from HERE — the moment the transport dies — not lazily when\n * its next call fails. The hook is a passive observer: this client stays\n * vanilla capnweb and never reconnects, pings, or retries by itself; the\n * consumer owns that loop.\n */\n onWebSocketClose?: (close: { code: number; reason: string }) => void;\n};\n\ntype ConnectItxAuthenticatedInput = ConnectItxBaseInput & {\n auth: ItxAuthCredentials;\n};\n\ntype ConnectProjectItxInput = ConnectItxAuthenticatedInput & {\n projectId: string;\n};\n\ntype ConnectAgentItxInput = ConnectItxAuthenticatedInput & {\n agentPath: string;\n projectId: string;\n};\n\nexport type ItxInitialConnectionRetry = {\n attemptDurationMs: number;\n delayMs: number;\n error: Error;\n failedAttempt: 1;\n nextAttempt: 2;\n startedAt: string;\n};\n\nexport type ConnectItxReadyOptions = {\n /**\n * Permit exactly one fresh dial while establishing the initial WebSocket.\n *\n * The retry boundary ends before the RPC session exists, so it can never\n * replay authentication or a caller operation.\n */\n retryInitialConnection?: {\n /** Delay before the one retry. Defaults to 250ms; maximum 5s. */\n delayMs?: number;\n /** Observe the failed first dial before the retry begins. */\n onRetry?: (retry: ItxInitialConnectionRetry) => Promise<void> | void;\n };\n};\n\n/** Decode a raw ws frame (outbound string, inbound Buffer/ArrayBuffer) into its parsed JSON value. */\nfunction parseFrame(data: unknown): unknown {\n const text =\n typeof data === \"string\"\n ? data\n : Buffer.isBuffer(data)\n ? data.toString(\"utf8\")\n : ArrayBuffer.isView(data)\n ? Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString(\"utf8\")\n : data instanceof ArrayBuffer\n ? Buffer.from(data).toString(\"utf8\")\n : undefined;\n return text === undefined ? data : JSON.parse(text);\n}\n\nfunction createSocket(\n url: string,\n headers?: Record<string, string>,\n onWebSocketMessage?: (message: ItxWebSocketMessage) => void,\n onWebSocketClose?: (close: { code: number; reason: string }) => void,\n): WebSocket {\n // 15s: cold deployments answer the upgrade only after the worker chain has\n // loaded, but #1601's route-healing + the preview slot warmup mean the first\n // upgrade lands in a few seconds — 15s is headroom, not a hang budget.\n const socket = new WebSocket(url, { handshakeTimeout: 15_000, headers });\n\n observeSocketClose(socket, onWebSocketClose);\n\n if (onWebSocketMessage) {\n const start = Date.now();\n const record = (direction: \"in\" | \"out\", data: unknown) => {\n onWebSocketMessage([Date.now() - start, direction, parseFrame(data)]);\n };\n const send = socket.send.bind(socket);\n socket.send = ((data: Parameters<WebSocket[\"send\"]>[0], ...args: unknown[]) => {\n record(\"out\", data);\n return send(data, ...(args as []));\n }) as WebSocket[\"send\"];\n socket.on(\"message\", (data) => record(\"in\", data));\n }\n\n return socket;\n}\n\nfunction observeSocketClose(\n socket: WebSocket,\n onWebSocketClose: ConnectItxBaseInput[\"onWebSocketClose\"],\n): void {\n if (onWebSocketClose) {\n socket.once(\"close\", (code, reason) => onWebSocketClose({ code, reason: reason.toString() }));\n }\n}\n\ntype RpcSessionStub<T extends object> = CapnRpcStub<T> & {\n [Symbol.dispose]?(): void;\n dup(): RpcSessionStub<T>;\n};\n\ntype SocketOwner = {\n [Symbol.dispose](): void;\n dup(): SocketOwner;\n};\n\nexport function connectItx(input: ConnectAgentItxInput): CapnRpcStub<Agent>;\nexport function connectItx(input: ConnectProjectItxInput): CapnRpcStub<Project>;\nexport function connectItx(input: ConnectItxAuthenticatedInput): CapnRpcStub<Session>;\nexport function connectItx(input: ConnectItxBaseInput): CapnRpcStub<UnauthenticatedOs>;\nexport function connectItx(\n input:\n | ConnectAgentItxInput\n | ConnectItxAuthenticatedInput\n | ConnectItxBaseInput\n | ConnectProjectItxInput,\n):\n | CapnRpcStub<Agent>\n | CapnRpcStub<Project>\n | CapnRpcStub<Session>\n | CapnRpcStub<UnauthenticatedOs> {\n return createItxConnection(input, createItxSocket(input, input.onWebSocketClose));\n}\n\nexport function connectItxReady(\n input: ConnectAgentItxInput,\n options?: ConnectItxReadyOptions,\n): Promise<CapnRpcStub<Agent>>;\nexport function connectItxReady(\n input: ConnectProjectItxInput,\n options?: ConnectItxReadyOptions,\n): Promise<CapnRpcStub<Project>>;\nexport function connectItxReady(\n input: ConnectItxAuthenticatedInput,\n options?: ConnectItxReadyOptions,\n): Promise<CapnRpcStub<Session>>;\nexport function connectItxReady(\n input: ConnectItxBaseInput,\n options?: ConnectItxReadyOptions,\n): Promise<CapnRpcStub<UnauthenticatedOs>>;\nexport async function connectItxReady(\n input:\n | ConnectAgentItxInput\n | ConnectItxAuthenticatedInput\n | ConnectItxBaseInput\n | ConnectProjectItxInput,\n options: ConnectItxReadyOptions = {},\n): Promise<\n CapnRpcStub<Agent> | CapnRpcStub<Project> | CapnRpcStub<Session> | CapnRpcStub<UnauthenticatedOs>\n> {\n const retryOptions = options.retryInitialConnection;\n const delayMs = retryOptions === undefined ? 0 : initialRetryDelay(retryOptions.delayMs);\n\n for (const attempt of [1, 2] as const) {\n const startedAt = new Date();\n const startedAtPerformance = performance.now();\n const socket = createItxSocket(input);\n try {\n await waitForOpen(socket);\n const connection = createItxConnection(input, socket);\n observeSocketClose(socket, input.onWebSocketClose);\n return connection;\n } catch (error) {\n if (attempt !== 1 || retryOptions === undefined) throw asError(error);\n await retryOptions.onRetry?.({\n attemptDurationMs: performance.now() - startedAtPerformance,\n delayMs,\n error: asError(error),\n failedAttempt: 1,\n nextAttempt: 2,\n startedAt: startedAt.toISOString(),\n });\n if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n\n throw new Error(\"unreachable: initial itx connection retry exhausted\");\n}\n\nfunction createItxSocket(\n input:\n | ConnectAgentItxInput\n | ConnectItxAuthenticatedInput\n | ConnectItxBaseInput\n | ConnectProjectItxInput,\n onWebSocketClose?: ConnectItxBaseInput[\"onWebSocketClose\"],\n): WebSocket {\n return createSocket(\n apiWebSocketUrl(input.baseUrl).toString(),\n input.headers,\n input.onWebSocketMessage,\n onWebSocketClose,\n );\n}\n\nfunction createItxConnection(\n input:\n | ConnectAgentItxInput\n | ConnectItxAuthenticatedInput\n | ConnectItxBaseInput\n | ConnectProjectItxInput,\n socket: WebSocket,\n):\n | CapnRpcStub<Agent>\n | CapnRpcStub<Project>\n | CapnRpcStub<Session>\n | CapnRpcStub<UnauthenticatedOs> {\n try {\n // Node ws implements the WebSocket operations Cap'n Web uses; its DOM type\n // declaration differs, so adapt that library boundary here.\n const session = newWebSocketRpcSession<UnauthenticatedOs>(\n socket as unknown as Parameters<typeof newWebSocketRpcSession>[0],\n );\n const socketOwner = createSocketOwner(socket, session);\n if (!(\"auth\" in input)) {\n return createSocketOwnedSession<UnauthenticatedOs>(session, socketOwner);\n }\n\n // The generated API describes remote return values. Cap'n Web pipelines\n // those calls into stubs, retaining their dispose/dup ownership operations.\n const root = session.authenticate(input.auth) as CapnRpcStub<Session>;\n if (!(\"projectId\" in input)) return withOwnedRpcSession(root, socketOwner);\n\n // Project lookup is another pipelined RPC stub with the same ownership API.\n const project = root.projects.get(input.projectId) as RpcSessionStub<Project>;\n if (!(\"agentPath\" in input)) return withOwnedRpcSession(project, root, socketOwner);\n\n // An \"agent itx\" reached from outside `/api` is just this agent's `Agent`\n // handle. It already carries the agent's own control surface plus the dynamic\n // capability scope chain (agent scope → project scope), so\n // `agent.someProvidedCapability()` resolves whether the capability was mounted\n // on the agent or on the project. Inside a Worker, `env.ITX.get()` returns the\n // richer full itx at the agent path; the external client keeps the narrower,\n // serialization-friendly Agent surface.\n // Agent lookup likewise returns a pipelined stub, rather than a local Agent.\n const agent = project.agents.get(input.agentPath) as RpcSessionStub<Agent>;\n return withOwnedRpcSession(agent, project, root, socketOwner);\n } catch (error) {\n socket.close(1000);\n throw error;\n }\n}\n\n/**\n * One returned scoped itx handle owns one reference to the physical socket.\n * `withOwnedRpcSession()` duplicates every owned value, so each duplicated\n * handle retains its own reference and the last disposal closes the transport.\n */\nfunction createSocketOwner(\n socket: WebSocket,\n session: RpcSessionStub<UnauthenticatedOs>,\n): SocketOwner {\n let references = 0;\n const retain = () => {\n references += 1;\n let disposed = false;\n return {\n dup: retain,\n [Symbol.dispose]: () => {\n if (disposed) return;\n disposed = true;\n references -= 1;\n if (references !== 0) return;\n // Cap'n Web turns disposal of its bootstrap stub into a 3000 abort.\n // Start the normal close first, then dispose the session so local\n // pending imports are rejected without replacing the close code.\n socket.close(1000);\n session[Symbol.dispose]?.();\n },\n };\n };\n return retain();\n}\n\nfunction createSocketOwnedSession<T extends object>(\n session: RpcSessionStub<T>,\n socketOwner: SocketOwner,\n): RpcSessionStub<T> {\n return new Proxy(session, {\n get(target, key, receiver) {\n if (key === \"then\") return undefined;\n if (key === Symbol.dispose) return () => socketOwner[Symbol.dispose]?.();\n if (key === \"dup\") {\n return () => createSocketOwnedSession(session, socketOwner.dup());\n }\n return Reflect.get(target, key, receiver);\n },\n });\n}\n\nfunction waitForOpen(socket: WebSocket): Promise<void> {\n if (socket.readyState === WebSocket.OPEN) return Promise.resolve();\n if (socket.readyState !== WebSocket.CONNECTING) {\n return Promise.reject(new Error(\"itx WebSocket closed before connecting\"));\n }\n return new Promise((resolve, reject) => {\n const cleanup = () => {\n socket.off(\"close\", onClose);\n socket.off(\"error\", onError);\n socket.off(\"open\", onOpen);\n };\n const onClose = (code: number, reason: Buffer) => {\n cleanup();\n reject(new Error(`itx WebSocket closed before connecting: ${code} ${reason.toString()}`));\n };\n const onError = (error: Error) => {\n cleanup();\n reject(error);\n };\n const onOpen = () => {\n cleanup();\n resolve();\n };\n socket.once(\"close\", onClose);\n socket.once(\"error\", onError);\n socket.once(\"open\", onOpen);\n });\n}\n\nfunction initialRetryDelay(value: number | undefined): number {\n const delayMs = value ?? 250;\n if (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > 5_000) {\n throw new Error(`Initial itx retry delay must be between 0 and 5000ms; received ${delayMs}.`);\n }\n return delayMs;\n}\n\nfunction asError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"node.mjs","names":[],"sources":["../src/node.ts"],"sourcesContent":["import { newWebSocketRpcSession, type RpcStub } from \"capnweb\";\nimport WebSocket from \"ws\";\nimport type { IterateApi, SessionCredentials } from \"./api.ts\";\nimport { withTimeout } from \"./lib.ts\";\n\n/** One connection to an Iterate deployment. Dispose the owner to close every child capability and its socket.\n * Operations are never retried: replaying a script could duplicate its effects. */\nexport type IterateConnection = Disposable & {\n session: Awaited<ReturnType<RpcStub<IterateApi>[\"authenticate\"]>>;\n closed: Promise<{ code: number; reason: string }>;\n};\n\n// Explicit return type keeps declaration emit from expanding capnweb's recursive mapped types.\nexport async function connectIterate(input: {\n baseUrl: string;\n auth: SessionCredentials;\n}): Promise<IterateConnection> {\n const url = new URL(\"/api\", input.baseUrl);\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new Error(\"Iterate URL must use http or https.\");\n }\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n const socket = new WebSocket(url.href, { handshakeTimeout: 15_000 });\n // ws implements the DOM event/send/close interface consumed by capnweb.\n const root = newWebSocketRpcSession<IterateApi>(socket as unknown as globalThis.WebSocket);\n const closed = new Promise<{ code: number; reason: string }>((resolve) => {\n socket.once(\"close\", (code, reason) => resolve({ code, reason: reason.toString() }));\n });\n // The transport surfaces failures through RPC and `closed`; ws also emits an EventEmitter error.\n socket.on(\"error\", () => {});\n try {\n const session = await withTimeout(\n root.authenticate(input.auth),\n 20_000,\n \"Iterate authentication\",\n );\n return {\n session,\n closed,\n [Symbol.dispose]() {\n try {\n session[Symbol.dispose]();\n } finally {\n try {\n root[Symbol.dispose]();\n } finally {\n socket.close();\n }\n }\n },\n };\n } catch (error) {\n try {\n root[Symbol.dispose]();\n } finally {\n socket.terminate();\n }\n throw error;\n }\n}\n"],"mappings":";;;;AAaA,eAAsB,eAAe,OAGN;CAC7B,MAAM,MAAM,IAAI,IAAI,QAAQ,MAAM,OAAO;CACzC,IAAI,IAAI,aAAa,YAAY,IAAI,aAAa,SAChD,MAAM,IAAI,MAAM,qCAAqC;CAEvD,IAAI,WAAW,IAAI,aAAa,WAAW,SAAS;CACpD,MAAM,SAAS,IAAI,UAAU,IAAI,MAAM,EAAE,kBAAkB,KAAO,CAAC;CAEnE,MAAM,OAAO,uBAAmC,MAAyC;CACzF,MAAM,SAAS,IAAI,SAA2C,YAAY;EACxE,OAAO,KAAK,UAAU,MAAM,WAAW,QAAQ;GAAE;GAAM,QAAQ,OAAO,SAAS;EAAE,CAAC,CAAC;CACrF,CAAC;CAED,OAAO,GAAG,eAAe,CAAC,CAAC;CAC3B,IAAI;EACF,MAAM,UAAU,MAAM,YACpB,KAAK,aAAa,MAAM,IAAI,GAC5B,KACA,wBACF;EACA,OAAO;GACL;GACA;GACA,CAAC,OAAO,WAAW;IACjB,IAAI;KACF,QAAQ,OAAO,QAAQ,CAAC;IAC1B,UAAU;KACR,IAAI;MACF,KAAK,OAAO,QAAQ,CAAC;KACvB,UAAU;MACR,OAAO,MAAM;KACf;IACF;GACF;EACF;CACF,SAAS,OAAO;EACd,IAAI;GACF,KAAK,OAAO,QAAQ,CAAC;EACvB,UAAU;GACR,OAAO,UAAU;EACnB;EACA,MAAM;CACR;AACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
//#region src/
|
|
2
|
+
//#region src/oauth-scopes.ts
|
|
3
3
|
/** The platform's OAuth scopes — what an app may ask for and what a consent grants:
|
|
4
4
|
* - `iterate` — reach the projects the person grants (every app; implied, always granted)
|
|
5
5
|
* - `account` — manage the person's sessions and personal access tokens
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-scopes.mjs","names":[],"sources":["../src/oauth-scopes.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/** The platform's OAuth scopes — what an app may ask for and what a consent grants:\n * - `iterate` — reach the projects the person grants (every app; implied, always granted)\n * - `account` — manage the person's sessions and personal access tokens\n * - `organizations:write` — the person's organizations: list every one they belong to, create new ones\n * Consent is task-based (the shape Cloudflare's own OAuth consent took in August 2026: a client\n * requests a set, the person may deselect the optional ones, the token carries what was granted):\n * `iterate` is required, every other requested scope is optional on the consent page, and an app\n * reads the granted set from `session.info().scopes` rather than assuming its request. */\nexport const OAuthScope = z.enum([\"iterate\", \"account\", \"organizations:write\"]);\nexport type OAuthScope = z.infer<typeof OAuthScope>;\n\nexport const OAuthScopes = z\n .array(OAuthScope)\n .transform((scopes) => [...new Set([\"iterate\", ...scopes])]);\n\n/** A requested scope as the consent page shows it: its name, what it means to the person, and\n * whether they may untick it (`iterate` never). */\nexport interface ConsentScope {\n name: OAuthScope;\n title: string;\n note: string;\n required: boolean;\n}\n\n/** What each scope means to the person — the consent page's copy, sent with `consent.describe` so\n * the page renders what it is given: a scope added to `OAuthScope` is described here or it does\n * not compile. */\nexport const OAuthScopeDescriptions: Record<OAuthScope, Omit<ConsentScope, \"name\">> = {\n iterate: {\n title: \"Read and make changes in the projects you grant it\",\n note: \"Required — what the app is for.\",\n required: true,\n },\n account: {\n title: \"See and end your sessions, and mint personal access tokens\",\n note: \"Optional.\",\n required: false,\n },\n \"organizations:write\": {\n title: \"See all your organizations and create new ones\",\n note: \"Optional.\",\n required: false,\n },\n};\n"],"mappings":";;;;;;;;;;AAUA,MAAa,aAAa,EAAE,KAAK;CAAC;CAAW;CAAW;AAAqB,CAAC;AAG9E,MAAa,cAAc,EACxB,MAAM,UAAU,CAAC,CACjB,WAAW,WAAW,CAAC,GAAG,IAAI,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC;;;;AAc7D,MAAa,yBAAyE;CACpF,SAAS;EACP,OAAO;EACP,MAAM;EACN,UAAU;CACZ;CACA,SAAS;EACP,OAAO;EACP,MAAM;EACN,UAAU;CACZ;CACA,uBAAuB;EACrB,OAAO;EACP,MAAM;EACN,UAAU;CACZ;AACF"}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { OAuthScopes } from "./oauth-scopes.mjs";
|
|
2
2
|
import * as oauth from "oauth4webapi";
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/client/oauth.ts
|
|
4
|
+
/** The platform's issuer as oauth4webapi's authorization server: its endpoints under `/oauth2`, and
|
|
5
|
+
* the `iss` it adds to every authorization response (RFC 9207), which `validateAuthResponse` then
|
|
6
|
+
* requires. The same description for an app's browser session and the CLI. */
|
|
7
|
+
function authorizationServer(issuer) {
|
|
8
|
+
return {
|
|
9
|
+
issuer,
|
|
10
|
+
authorization_endpoint: `${issuer}/oauth2/auth`,
|
|
11
|
+
token_endpoint: `${issuer}/oauth2/token`,
|
|
12
|
+
registration_endpoint: `${issuer}/oauth2/register`,
|
|
13
|
+
authorization_response_iss_parameter_supported: true
|
|
14
|
+
};
|
|
15
|
+
}
|
|
4
16
|
/** The same code/PKCE parameters for browser login and a console-minted token. */
|
|
5
17
|
async function authorizationCodeRequest(input) {
|
|
6
18
|
const verifier = oauth.generateRandomCodeVerifier();
|
|
@@ -24,6 +36,6 @@ async function authorizationCodeRequest(input) {
|
|
|
24
36
|
};
|
|
25
37
|
}
|
|
26
38
|
//#endregion
|
|
27
|
-
export { authorizationCodeRequest };
|
|
39
|
+
export { authorizationCodeRequest, authorizationServer };
|
|
28
40
|
|
|
29
41
|
//# sourceMappingURL=oauth.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.mjs","names":[],"sources":["../src/client/oauth.ts"],"sourcesContent":["import * as oauth from \"oauth4webapi\";\nimport { OAuthScopes } from \"../oauth-scopes.ts\";\n\n/** The platform's issuer as oauth4webapi's authorization server: its endpoints under `/oauth2`, and\n * the `iss` it adds to every authorization response (RFC 9207), which `validateAuthResponse` then\n * requires. The same description for an app's browser session and the CLI. */\nexport function authorizationServer(issuer: string): oauth.AuthorizationServer {\n return {\n issuer,\n authorization_endpoint: `${issuer}/oauth2/auth`,\n token_endpoint: `${issuer}/oauth2/token`,\n registration_endpoint: `${issuer}/oauth2/register`,\n authorization_response_iss_parameter_supported: true,\n };\n}\n\n/** The same code/PKCE parameters for browser login and a console-minted token. */\nexport async function authorizationCodeRequest(input: {\n issuer: string;\n clientId: string;\n redirectUri: string;\n resources: string[];\n scopes?: string[];\n}) {\n const verifier = oauth.generateRandomCodeVerifier();\n const challenge = await oauth.calculatePKCECodeChallenge(verifier);\n const state = oauth.generateRandomState();\n const url = new URL(\"/oauth2/auth\", input.issuer);\n url.search = new URLSearchParams({\n response_type: \"code\",\n client_id: input.clientId,\n redirect_uri: input.redirectUri,\n scope: OAuthScopes.parse(input.scopes || []).join(\" \"),\n state,\n code_challenge: challenge,\n code_challenge_method: \"S256\",\n }).toString();\n for (const resource of input.resources) url.searchParams.append(\"resource\", resource);\n return { url, state, verifier };\n}\n"],"mappings":";;;;;;AAMA,SAAgB,oBAAoB,QAA2C;CAC7E,OAAO;EACL;EACA,wBAAwB,GAAG,OAAO;EAClC,gBAAgB,GAAG,OAAO;EAC1B,uBAAuB,GAAG,OAAO;EACjC,gDAAgD;CAClD;AACF;;AAGA,eAAsB,yBAAyB,OAM5C;CACD,MAAM,WAAW,MAAM,2BAA2B;CAClD,MAAM,YAAY,MAAM,MAAM,2BAA2B,QAAQ;CACjE,MAAM,QAAQ,MAAM,oBAAoB;CACxC,MAAM,MAAM,IAAI,IAAI,gBAAgB,MAAM,MAAM;CAChD,IAAI,SAAS,IAAI,gBAAgB;EAC/B,eAAe;EACf,WAAW,MAAM;EACjB,cAAc,MAAM;EACpB,OAAO,YAAY,MAAM,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG;EACrD;EACA,gBAAgB;EAChB,uBAAuB;CACzB,CAAC,CAAC,CAAC,SAAS;CACZ,KAAK,MAAM,YAAY,MAAM,WAAW,IAAI,aAAa,OAAO,YAAY,QAAQ;CACpF,OAAO;EAAE;EAAK;EAAO;CAAS;AAChC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Who is acting: a stable actor id (the control plane's user id) and, when known, an email. */
|
|
2
|
+
export type Principal = {
|
|
3
|
+
actor: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
};
|
|
6
|
+
/** The header the edge sets on a Request it forwards on a principal's behalf — the ingress after
|
|
7
|
+
* the cookie check, a session's terminal `fetch` — and strips from every inbound Request. */
|
|
8
|
+
export declare const ITX_PRINCIPAL_HEADER = "x-itx-principal";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/principal.ts
|
|
2
|
+
/** The header the edge sets on a Request it forwards on a principal's behalf — the ingress after
|
|
3
|
+
* the cookie check, a session's terminal `fetch` — and strips from every inbound Request. */
|
|
4
|
+
const ITX_PRINCIPAL_HEADER = "x-itx-principal";
|
|
5
|
+
//#endregion
|
|
6
|
+
export { ITX_PRINCIPAL_HEADER };
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=principal.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"principal.mjs","names":[],"sources":["../src/principal.ts"],"sourcesContent":["// principal.ts — WHO user code is acting for, as the platform tells it: the verified `Principal`\n// (`itx.whoami()`, `source.principal` on an event) and the header the platform stamps it in on a\n// Request it forwards to a project's worker (sdk/auth.ts reads it). How the platform admits and\n// carries a caller, and signs its own tokens, stays in the platform.\n\n/** Who is acting: a stable actor id (the control plane's user id) and, when known, an email. */\nexport type Principal = { actor: string; email?: string };\n\n/** The header the edge sets on a Request it forwards on a principal's behalf — the ingress after\n * the cookie check, a session's terminal `fetch` — and strips from every inbound Request. */\nexport const ITX_PRINCIPAL_HEADER = \"x-itx-principal\";\n"],"mappings":";;;AAUA,MAAa,uBAAuB"}
|