iterate 0.2.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +173 -81
  2. package/dist/api.d.ts +643 -0
  3. package/dist/api.mjs +0 -0
  4. package/dist/app-server.d.ts +51 -0
  5. package/dist/app-server.mjs +481 -0
  6. package/dist/app-server.mjs.map +1 -0
  7. package/dist/app-session.d.ts +49 -0
  8. package/dist/app-session.mjs +235 -0
  9. package/dist/app-session.mjs.map +1 -0
  10. package/dist/app.d.ts +29 -0
  11. package/dist/app.mjs +180 -0
  12. package/dist/app.mjs.map +1 -0
  13. package/dist/client/live-state.d.ts +63 -0
  14. package/dist/client/oauth.d.ts +17 -0
  15. package/dist/client/react.d.ts +77 -0
  16. package/dist/client/socket.d.ts +7 -0
  17. package/dist/client.mjs +156 -0
  18. package/dist/client.mjs.map +1 -0
  19. package/dist/expression.d.ts +88 -0
  20. package/dist/expression.mjs +301 -0
  21. package/dist/expression.mjs.map +1 -0
  22. package/dist/lib-BWr-5mFO.mjs +36 -0
  23. package/dist/lib-BWr-5mFO.mjs.map +1 -0
  24. package/dist/lib.d.ts +70 -0
  25. package/dist/lib.mjs +228 -0
  26. package/dist/lib.mjs.map +1 -0
  27. package/dist/node.d.ts +15 -0
  28. package/dist/node.mjs +47 -0
  29. package/dist/node.mjs.map +1 -0
  30. package/dist/oauth-scopes.d.ts +32 -0
  31. package/dist/oauth-scopes.mjs +40 -0
  32. package/dist/oauth-scopes.mjs.map +1 -0
  33. package/dist/oauth.mjs +41 -0
  34. package/dist/oauth.mjs.map +1 -0
  35. package/dist/principal.d.ts +8 -0
  36. package/dist/principal.mjs +8 -0
  37. package/dist/principal.mjs.map +1 -0
  38. package/dist/project-ingress.d.ts +58 -0
  39. package/dist/project-ingress.mjs +104 -0
  40. package/dist/project-ingress.mjs.map +1 -0
  41. package/dist/react.mjs +285 -0
  42. package/dist/react.mjs.map +1 -0
  43. package/dist/sdk/auth.d.ts +25 -0
  44. package/dist/sdk/index.d.ts +155 -0
  45. package/dist/sdk/record-pipelined-steps.d.ts +19 -0
  46. package/dist/sdk.mjs +245 -0
  47. package/dist/sdk.mjs.map +1 -0
  48. package/dist/stream/processor.d.ts +383 -0
  49. package/dist/stream/processor.mjs +605 -0
  50. package/dist/stream/processor.mjs.map +1 -0
  51. package/dist/stream/run.d.ts +61 -0
  52. package/dist/stream/run.mjs +45 -0
  53. package/dist/stream/run.mjs.map +1 -0
  54. package/dist/stream/test-support.d.ts +45 -0
  55. package/dist/stream/test-support.mjs +196 -0
  56. package/dist/stream/test-support.mjs.map +1 -0
  57. package/dist/usingCtx-inzbY1Qz.mjs +57 -0
  58. package/package.json +93 -30
  59. package/bin/iterate.js +0 -86
  60. package/dist/cli-DMS4kJph.mjs +0 -868
  61. package/dist/cli-DMS4kJph.mjs.map +0 -1
  62. package/dist/config-DtnR7Lv7.mjs +0 -170
  63. package/dist/config-DtnR7Lv7.mjs.map +0 -1
  64. package/dist/index.d.mts +0 -5
  65. package/dist/index.d.mts.map +0 -1
  66. package/dist/index.mjs +0 -8
  67. package/dist/index.mjs.map +0 -1
  68. package/dist/stream-tui/agent-chat-terminal.d.mts +0 -1
  69. package/dist/stream-tui/agent-chat-terminal.mjs +0 -933
  70. package/dist/stream-tui/agent-chat-terminal.mjs.map +0 -1
  71. package/dist/worker.d.mts +0 -33
  72. package/dist/worker.mjs +0 -18
  73. package/dist/worker.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.mjs","names":[],"sources":["../src/client/react.tsx"],"sourcesContent":["/** @jsxImportSource react */\n// client/react.tsx — the React binding for live state, shared by every UI. `useLiveState` subscribes a component to a producer's live\n// state (a processor slug, a mini-app key), seeds from it, and re-renders on every synced\n// delta via `useSyncExternalStore` over the LiveStateStore. The transport and the store\n// (client/live-state.ts) stay framework-free, so this is the ONE file that imports React.\n//\n// Kept to the one shape a UI or test needs — no reconnect/backoff/ping-watchdog (that policy belongs\n// to whoever owns the capnweb session; here the caller passes a ready `itx`).\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { SubscriptionListEntry } from \"../api.ts\";\nimport type { StreamEvent } from \"../stream/processor.ts\";\nimport {\n connectLiveState,\n type LiveStateItx,\n type LiveStateSeed,\n type LiveStateStore,\n} from \"./live-state.ts\";\n\nexport type LiveStateStatus = \"connecting\" | \"live\" | \"error\";\n\n/** One live state as a component reads it: the latest value (undefined until the first seed lands),\n * the revision it is at, whether its subscription is connecting, live or failed, and the failure. */\nexport type LiveStateResult<S = unknown> = {\n value: S | undefined;\n rev: number | null;\n status: LiveStateStatus;\n error?: string;\n};\n\n/** Subscribe to a producer's live state and render its latest value. Pass a ready `itx` (a capnweb\n * `api.authenticate(credentials).user` or `.projects.get(id)`), the producer's `key`, and a `readSeed`\n * thunk that reads `{rev, state}` (`() => itx.invoke(\"itx.facets.get('slug').liveSnapshot()\")`).\n * Re-subscribes when the session, `key`, or `name` changes; unmount (and every re-subscribe)\n * disposes the previous server-side subscription. */\nexport function useLiveState<S>(\n itx: LiveStateItx | undefined,\n opts: { key: string; name?: string; readSeed: () => Promise<LiveStateSeed<S>> },\n): LiveStateResult<S> {\n const [store, setStore] = useState<LiveStateStore<S> | undefined>();\n const [status, setStatus] = useState<LiveStateStatus>(\"connecting\");\n const [error, setError] = useState<string | undefined>();\n // The readSeed thunk is a fresh arrow every render; hold the latest so the effect need not re-run per\n // render. The effect SNAPSHOTS it at connect time, so an old subscription's gap heal can never\n // read a NEWER key's seed (cross-key contamination after a key/session switch).\n const readSeedRef = useRef(opts.readSeed);\n readSeedRef.current = opts.readSeed;\n\n useEffect(() => {\n setStore(undefined);\n setStatus(\"connecting\");\n setError(undefined);\n if (!itx) return;\n const readSeed = readSeedRef.current; // pinned to THIS key/session for the connection's whole life\n let disposed = false;\n let dispose: (() => Promise<void>) | undefined;\n const unmounted = new AbortController(); // an unmount while the first seed is pending recalls the row\n connectLiveState<S>(itx, {\n key: opts.key,\n name: opts.name,\n readSeed,\n signal: unmounted.signal,\n onResync: (r) => {\n if (disposed) return;\n if (r === \"healed\") {\n setStatus(\"live\");\n setError(undefined);\n } else {\n // the store keeps its last value; the next delta retries the heal\n setStatus(\"error\");\n setError(r.message);\n }\n },\n }).then(\n (conn) => {\n dispose = conn.dispose;\n if (disposed) {\n void conn.dispose(); // unmounted while connecting — still tear the mount down\n return;\n }\n setStore(conn.store);\n setStatus(\"live\");\n },\n (e: unknown) => {\n if (disposed) return;\n setError(e instanceof Error ? e.message : String(e));\n setStatus(\"error\");\n },\n );\n return () => {\n disposed = true;\n unmounted.abort();\n void dispose?.();\n };\n }, [itx, opts.key, opts.name]);\n\n const subscribe = useCallback(\n (cb: () => void) => (store ? store.subscribe(cb) : () => {}),\n [store],\n );\n const value = useSyncExternalStore(\n subscribe,\n () => store?.get(),\n () => undefined,\n );\n return { value, rev: store?.rev() ?? null, status, error };\n}\n\n// ── the iterate context ── the data half of a general-purpose context view (packages/ui\n// `components/context-view`, the rendering half): every committed event of a context, live; the rows\n// of its processors table; who is here; named facets' live state. ONE hook here, pure components\n// there, so the UI kit stays free of the SDK and any app — the dash, the agents app — composes the two.\n\n/** One presence: who acted on the context and when last, from the log's stamps. */\nexport type IterateContextPresence = {\n actor: string;\n email?: string;\n grant?: string;\n lastSeenAt: string;\n};\n\n/** The slice of a context handle `useIterateContext` reads — a capnweb `IterateContextApi` stub\n * satisfies it structurally. `invoke` seeds a named facet's live state\n * (`itx.facets.get('<name>').liveSnapshot()`, as an expression). */\nexport type IterateContextHandle = LiveStateItx & {\n readEvents(\n afterOffset?: number,\n limit?: number,\n ): Promise<{ events: unknown[]; atHead: boolean; scannedThroughOffset: number }>;\n processors: { list(): Promise<SubscriptionListEntry[]> | SubscriptionListEntry[] };\n rpcStubs: { list(): Promise<string[]> | string[] };\n invoke(call: string): Promise<unknown>;\n};\n\n/** A wire event (a capnweb proxy value or a plain object) as a `StreamEvent`, or null when\n * it is not a committed row. Structural, not a schema: the transport validated it; this only refuses\n * a shape the view cannot place (no offset, type or time). */\nfunction toStreamEvent(raw: unknown): StreamEvent | null {\n const value = JSON.parse(JSON.stringify(raw)) as Record<string, unknown> | null;\n if (\n !value ||\n typeof value.offset !== \"number\" ||\n typeof value.type !== \"string\" ||\n typeof value.createdAt !== \"string\"\n )\n return null;\n return value as unknown as StreamEvent; // the three fields checked are all the hook indexes by\n}\n\n/** A named live state before its first seed lands — and before the effect that opens it has run. */\nconst LIVE_STATE_CONNECTING: LiveStateResult = {\n value: undefined,\n rev: null,\n status: \"connecting\",\n};\n\n/** THE ITERATE CONTEXT, live — one hook, one stream subscription. THE LOG: subscribe to every\n * committed event (or `consumes`) BEFORE the catch-up read, so nothing lands between the two;\n * pushes and pages both dedupe into one map by offset; `caughtUp` once the read reached the head;\n * `error` when the connect failed. Off that same log, THE PROCESSORS TABLE, re-read whenever the\n * log grows a row-changing event (a subscription configured, halted or resumed — the table is core\n * state, one call away, no push of its own), and WHO IS HERE: the rpc stubs lent right now\n * (`itx.rpcStubs.list()` — physical, re-read at every new head, since presence changes are\n * ephemeral facts) and, from the log, every principal that acted, newest first. And named facets'\n * LIVE STATE, each seeded through `itx.facets.get('<name>').liveSnapshot()` — one entry per name,\n * always. `liveState` OMITTED opens `core` (the core reduce answers under that name) plus every\n * hosted facet in the processors table the hook holds, following the table as it loads and changes;\n * `liveState` GIVEN is exactly the names to open, no implicit `core`. Re-connects when `itx`\n * changes; unmount disposes every server-side subscription. */\nexport function useIterateContext(\n itx: IterateContextHandle | undefined,\n opts: { consumes?: string[]; liveState?: string[] } = {},\n): {\n events: StreamEvent[];\n caughtUp: boolean;\n error?: string;\n processors: { rows: SubscriptionListEntry[]; loaded: boolean; error?: string };\n presence: { actors: IterateContextPresence[]; rpcStubs: string[] };\n liveState: Record<string, LiveStateResult>;\n} {\n // ── the log ──\n const [events, setEvents] = useState<Map<number, StreamEvent>>(() => new Map());\n const [caughtUp, setCaughtUp] = useState(false);\n const [error, setError] = useState<string | undefined>();\n const consumesKey = JSON.stringify(opts.consumes || [\"*\"]);\n useEffect(() => {\n setEvents(new Map());\n setCaughtUp(false);\n setError(undefined);\n if (!itx) return;\n let disposed = false;\n const merge = (batch: unknown[]) =>\n setEvents((held) => {\n const next = new Map(held);\n for (const raw of batch) {\n const event = toStreamEvent(raw);\n if (event) next.set(event.offset, event);\n }\n return next;\n });\n let subscription: { [Symbol.dispose](): void } | undefined;\n (async () => {\n const handle = await itx.subscribe({\n consumes: JSON.parse(consumesKey) as string[],\n target: (batch) => !disposed && merge(batch),\n });\n // An unmount while the subscribe was pending ran the cleanup before this handle existed:\n // release it here, or the server keeps delivering to nobody.\n if (disposed) {\n handle[Symbol.dispose]();\n return;\n }\n subscription = handle;\n for (let after = 0; ; ) {\n const page = await itx.readEvents(after, 500);\n if (disposed) return;\n merge(page.events);\n if (page.atHead || page.scannedThroughOffset <= after) break;\n after = page.scannedThroughOffset;\n }\n setCaughtUp(true);\n })().catch((e: unknown) => !disposed && setError(e instanceof Error ? e.message : String(e)));\n return () => {\n disposed = true;\n subscription?.[Symbol.dispose]();\n };\n }, [itx, consumesKey]);\n const sorted = useMemo(() => [...events.values()].sort((a, b) => a.offset - b.offset), [events]);\n\n // ── the processors table ──\n // The table and the last failure remember WHICH itx they came from: a page that swaps contexts\n // (one route, another organization) shows an empty, not-yet-loaded table for the new one rather\n // than the old one's rows or error until the new read lands.\n const [table, setTable] = useState<{\n itx: IterateContextHandle;\n rows: SubscriptionListEntry[];\n }>();\n const [failure, setFailure] = useState<{ itx: IterateContextHandle; message: string }>();\n const tableVersion = sorted.reduce(\n (last, event) =>\n event.type.startsWith(\"events.iterate.com/itx/subscription-\") ? event.offset : last,\n 0,\n );\n useEffect(() => {\n if (!itx) return;\n let disposed = false;\n Promise.resolve(itx.processors.list()).then(\n (list) => {\n if (disposed) return;\n setTable({ itx, rows: list });\n setFailure(undefined); // a read that recovered clears the last failure\n },\n (e: unknown) =>\n !disposed && setFailure({ itx, message: e instanceof Error ? e.message : String(e) }),\n );\n return () => {\n disposed = true;\n };\n }, [itx, tableVersion]);\n const currentTable = itx && table?.itx === itx ? table : undefined;\n\n // ── who is here ──\n const [census, setCensus] = useState<{ itx: IterateContextHandle; rpcStubs: string[] }>();\n const head = sorted.at(-1)?.offset ?? 0;\n useEffect(() => {\n if (!itx) return;\n let disposed = false;\n Promise.resolve(itx.rpcStubs.list()).then(\n (list) => !disposed && setCensus({ itx, rpcStubs: list }),\n () => undefined, // presence is nice to have; a failed census shows nothing\n );\n return () => {\n disposed = true;\n };\n }, [itx, head]);\n // keyed by its itx: a swapped context shows no census until its own lands\n const rpcStubs = itx && census?.itx === itx ? census.rpcStubs : [];\n const actors = useMemo(() => {\n const byActor = new Map<string, IterateContextPresence>();\n for (const event of sorted) {\n const principal = event.source?.principal;\n if (!principal) continue;\n byActor.set(principal.actor, {\n actor: principal.actor,\n email: principal.email,\n grant: event.source?.grant,\n lastSeenAt: event.createdAt,\n });\n }\n return [...byActor.values()].sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));\n }, [sorted]);\n\n // ── named facets' live state ──\n // N subscriptions in ONE effect keyed by the name set — it changes at runtime as the processors\n // table loads (the default set is `core` plus the table's hosted facets) — since hooks cannot run\n // in a loop: client/live-state.ts's store reduces each, and this mirrors every change into React\n // state. The entries remember WHICH itx and name set they came from (as the table does), so a\n // swapped context or a changed set shows fresh connecting entries, never the last one's values.\n const liveStateKey = JSON.stringify(\n opts.liveState || [\n \"core\",\n ...(currentTable?.rows || []).flatMap((row) =>\n row.hostedFacet ? [row.hostedFacet.name] : [],\n ),\n ],\n );\n const [liveStates, setLiveStates] = useState<{\n itx: IterateContextHandle;\n key: string;\n entries: Record<string, LiveStateResult>;\n }>();\n useEffect(() => {\n if (!itx) return;\n const names = JSON.parse(liveStateKey) as string[];\n if (names.length === 0) return;\n let disposed = false;\n const unmounted = new AbortController(); // an unmount while a first seed is pending recalls that row\n const disposers: Array<() => void | Promise<void>> = [];\n const patch = (name: string, change: Partial<LiveStateResult>) =>\n setLiveStates((held) =>\n held && held.itx === itx && held.key === liveStateKey\n ? { ...held, entries: { ...held.entries, [name]: { ...held.entries[name], ...change } } }\n : held,\n );\n setLiveStates({\n itx,\n key: liveStateKey,\n entries: Object.fromEntries(names.map((name) => [name, LIVE_STATE_CONNECTING])),\n });\n for (const name of names) {\n connectLiveState<unknown>(itx, {\n key: name,\n readSeed: async () =>\n // the engine's own `{ rev, state }` seed, as `liveSnapshot()` answers it\n (await itx.invoke(`itx.facets.get('${name}').liveSnapshot()`)) as LiveStateSeed<unknown>,\n signal: unmounted.signal,\n onResync: (result) => {\n if (disposed) return;\n if (result === \"healed\") patch(name, { status: \"live\", error: undefined });\n // the store keeps its last value; the next delta retries the heal\n else patch(name, { status: \"error\", error: result.message });\n },\n }).then(\n (connection) => {\n if (disposed) {\n void connection.dispose(); // unmounted while connecting — still tear the row down\n return;\n }\n disposers.push(connection.dispose);\n disposers.push(\n connection.store.subscribe(() =>\n patch(name, { value: connection.store.get(), rev: connection.store.rev() }),\n ),\n );\n patch(name, {\n value: connection.store.get(),\n rev: connection.store.rev(),\n status: \"live\",\n });\n },\n (e: unknown) => {\n if (disposed) return;\n patch(name, { status: \"error\", error: e instanceof Error ? e.message : String(e) });\n },\n );\n }\n return () => {\n disposed = true;\n unmounted.abort();\n for (const dispose of disposers) void dispose();\n };\n }, [itx, liveStateKey]);\n // One entry per name, always: a name the effect has not reached yet (the render right after the\n // set changed) reads as connecting rather than missing.\n const liveState = useMemo(() => {\n const names = JSON.parse(liveStateKey) as string[];\n const held =\n itx && liveStates?.itx === itx && liveStates.key === liveStateKey ? liveStates.entries : {};\n return Object.fromEntries(names.map((name) => [name, held[name] || LIVE_STATE_CONNECTING]));\n }, [itx, liveStateKey, liveStates]);\n\n return {\n events: sorted,\n caughtUp,\n error,\n processors: {\n rows: currentTable?.rows || [],\n loaded: Boolean(currentTable),\n error: itx && failure?.itx === itx ? failure.message : undefined,\n },\n presence: { actors, rpcStubs },\n liveState,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /** Project ingress strips public identity headers and stamps `x-itx-principal` for a project member
2
+ * only: a visitor signed out, signed in without this project, or riding a session cookie on a
3
+ * cross-site write arrives without one. This guard runs in the config worker, before it proxies an
4
+ * app.
5
+ *
6
+ * Signed out, every request gets `401` with `WWW-Authenticate: Bearer realm="iterate"`: the
7
+ * platform's edge turns that answer into the sign-in for a page load (or into "sign in again with
8
+ * this project" for someone signed in without it), whatever path the app is served under, and hands
9
+ * a fetch, a write or a WebSocket upgrade the 401 itself. Any app can ask for a signed-in visitor
10
+ * the same way:
11
+ *
12
+ * ```js
13
+ * if (!request.headers.get("x-itx-principal"))
14
+ * return new Response("Sign in\n", { status: 401, headers: { "WWW-Authenticate": 'Bearer realm="iterate"' } });
15
+ * ```
16
+ *
17
+ * A write or a WebSocket upgrade must also come from this origin (or carry no Origin, a non-browser
18
+ * client), else 403. The edge already sends such a cookie request on anonymous; this repeats the
19
+ * check where the app runs. The handshake is a GET, but it opens a two-way channel, and the app
20
+ * session cookie is `SameSite=Lax`: every `<routingSlug>--<project>.iterate.app` host is same-site with
21
+ * every other, so a page on another project's host could otherwise open a socket to this app with
22
+ * the visitor's cookie. */
23
+ export declare const auth: {
24
+ require(request: Request): Response | null;
25
+ };
@@ -0,0 +1,155 @@
1
+ import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
2
+ import type { IterateContextApi } from "../api.ts";
3
+ import { type ScannedRange, type StreamProcessor, type StreamEvent, type StreamEventInput } from "../stream/processor.ts";
4
+ import { withItx } from "./record-pipelined-steps.ts";
5
+ export { withItx };
6
+ export { LiveState, StreamProcessor, defineProcessorContract, type ConsumedEvent, type EventCatalog, type EventDefinition, type EmittedEventInput, type EventInput, type LiveStateSink, type ProcessorContract, type ProcessorState, type ProcessorStream, type ProcessEventArgs, type ReduceArgs, type ScannedRange, type StreamEvent, type StreamEventInput, } from "../stream/processor.ts";
7
+ export { z } from "zod";
8
+ export { newHttpBatchRpcSession, newWebSocketRpcSession, newWorkersRpcResponse } from "capnweb";
9
+ export { applyPatch, diff, jsonEqual, type PatchOp } from "../lib.ts";
10
+ /** What the parent mints a facet's class with — the whole identity, and one fact about its feed. */
11
+ export type FacetProps = {
12
+ iterateContextName: string;
13
+ name: string;
14
+ /** Set when, as this facet started, a subscription row of its context pushed it every commit it
15
+ * consumes (`processEventBatch`, the delivery loop's push): a processor's engine then trusts the
16
+ * head a catch-up read until the next push (stream/processor.ts, the read verbs). Absent, only a
17
+ * push is proof, so a processor no row pushes reads its log on every read. */
18
+ fedByPushes?: true;
19
+ };
20
+ /** THE FACET SHELL: a `DurableObject` a context hosts as a facet — `itx.facets.get(name, { source,
21
+ * className })`, a rule naming it, or a processor's row. A caller reaches a facet by itx expression
22
+ * (`itx.facets.get(name).<method>(…)`) only through what its class lists in `publicMethods`: the
23
+ * context refuses any other first step FORBIDDEN before the call reaches the facet
24
+ * (apps/os context/facet-public-methods.ts). The platform's own calls — the delivery loop's push
25
+ * and catch-up, the alarm's revive — never go through the list. A loaded class that does not
26
+ * extend this shell lists nothing, so no caller reaches it by expression. */
27
+ export declare abstract class FacetDurableObject<Env = unknown> extends DurableObject<Env, FacetProps> {
28
+ /** What a caller may reach by itx expression: the FIRST step of `itx.facets.get(name).<step>…`, a
29
+ * method or a property of this class. A subclass lists its own on top of its parent's:
30
+ * `static override publicMethods = [...super.publicMethods, "send"]`. */
31
+ static publicMethods: readonly string[];
32
+ /** This class's `publicMethods`, for the context that loaded it — a static does not cross the
33
+ * isolate. On no list: only the context asks it. */
34
+ listPublicMethods(): readonly string[];
35
+ }
36
+ /** The itx scope `withItx` hands its callback: a context's declared API (api.ts) — a capnweb stub
37
+ * of apps/os's `IterateContextRpcTarget`, which satisfies it. */
38
+ export type ItxScope = IterateContextApi;
39
+ /** What hands the scope over: the loopback entrypoint a loaded worker has as `env.ITX`, or the one a
40
+ * class of the platform's own worker mints from `ctx.exports`. */
41
+ export type ItxEntrypointService = {
42
+ get(): ItxScope;
43
+ };
44
+ /** The least a host needs of its scope: the fixed-point log calls the engine makes. The platform's own
45
+ * facets pass the Workers-RPC STUB of a context (every dotted step pipelined; a property there is a
46
+ * promise), which no plain-promise interface can name — so the constraint is this, not `ItxScope`. */
47
+ export type ProcessorScope = {
48
+ append(...events: StreamEventInput[]): Promise<unknown>;
49
+ readEvents(afterOffset?: number, limit?: number): Promise<unknown>;
50
+ /** The engine's claim on the context's alarm (processor.ts rule 3): "come back by `at`", or null. */
51
+ processors: {
52
+ claim(name: string, at: number | null): Promise<unknown>;
53
+ };
54
+ /** Another context of the project by its dotted surface (`.append`), which the platform's handle
55
+ * and a loaded worker's alike answer — how an entity's processor cross-posts its certificate to
56
+ * `/` (`withItx((itx) => itx.cd("/").append(certificate))`). Through the table like every other
57
+ * word here: a loaded processor's `cd` goes down only (the app wall), the platform's own go
58
+ * anywhere within the project. */
59
+ cd(path: string): {
60
+ append(...events: StreamEventInput[]): Promise<unknown>;
61
+ };
62
+ };
63
+ /** THE SCOPE ACCESSOR a host hands its processor: one pipelined round trip on the context's itx,
64
+ * released after (`StreamProcessorDurableObject.withItx`). A processor that needs an effect —
65
+ * `itx.cfArtifacts.create(path)`, `itx.ai.run(…)` — takes this and nothing else, so a unit test
66
+ * hands it a fake and the e2e lends one by rule on the context. */
67
+ export type WithItx<Scope = ItxScope> = <T>(call: (itx: Scope) => T) => Promise<Awaited<T>>;
68
+ export declare abstract class StreamProcessorDurableObject<State = unknown, Env extends {
69
+ ITX?: ItxEntrypointService;
70
+ } = {
71
+ ITX: ItxEntrypointService;
72
+ }, Scope extends ProcessorScope = ItxScope> extends FacetDurableObject<Env> {
73
+ #private;
74
+ /** The reads a caller reaches on every processor: `fetch`, and the state caught up through the log
75
+ * (`snapshot`, `liveSnapshot`) or awaited (`waitUntilProcessed`). What feeds the processor —
76
+ * `processEventBatch`, `catchUpFromLog`, `revive` — is the platform's, never a caller's. */
77
+ static publicMethods: string[];
78
+ /** The processor this object hosts — `processor = new PresenceProcessor()` at the top of the subclass. */
79
+ abstract readonly processor: StreamProcessor<State>;
80
+ /** After a runtime field on the processor moved OUTSIDE a batch (an RPC method on this object);
81
+ * inside `processEvent` the engine re-projects on its own. */
82
+ protected publishLiveState(): void;
83
+ /** THE push: the context hands over each committed batch with its scanned-range proof. */
84
+ processEventBatch(events: StreamEvent[], range: ScannedRange): Promise<void>;
85
+ /** Catch up from the log (the delivery loop's, when a row is configured or resumed). */
86
+ catchUpFromLog(): Promise<void>;
87
+ /** THE REVIVE: the context's alarm pass calls it for a due claim — catch up, then run the
88
+ * at-head pass, so an attempt the last incarnation was running is started again from state. */
89
+ revive(): Promise<void>;
90
+ /** Caught up through the log, then `{ offset, state }`. */
91
+ snapshot(): Promise<{
92
+ offset: number;
93
+ state: State;
94
+ }>;
95
+ /** The live-state seed read: `{ rev, state: projectLiveState(reduced) }`. */
96
+ liveSnapshot(): Promise<{
97
+ rev: number;
98
+ state: unknown;
99
+ }>;
100
+ /** The barrier: resolves once processed at least through `offset` (default timeout 10s). */
101
+ waitUntilProcessed(input: {
102
+ offset: number;
103
+ timeoutMs?: number;
104
+ }): Promise<void>;
105
+ /** ONE round trip on the itx scope, then RELEASE EVERYTHING IT REACHED: the get, and every call the
106
+ * callback made through it — not only the last. A Workers-RPC value this facet leaves undisposed —
107
+ * the `itx.cd(path)` of `itx.cd(path).append(…)`, the `cfArtifacts.get(p)` of `.remote()`, an
108
+ * answer awaited inside the callback (`const { state } = await context.invoke(…)`), data included —
109
+ * keeps THIS FACET running after its context is evicted, until V8 collects the value, which an
110
+ * idle isolate may not do for many minutes: each new incarnation of the context reattaches to the
111
+ * facet, and the object stays billed (measured 2026-09-23: a new website project's `/` and
112
+ * `/repos/config` billed 60 s of every minute for 30 min with no request). The context's own
113
+ * `invoke` cannot end this from its side: the facet holds the value (context-residency.e2e.test.ts,
114
+ * "… does not outlive …"). Protected: a host with methods of its own (the workspace,
115
+ * src/workspace/durable-object.ts) reaches its context the same way. */
116
+ protected withItx<T>(call: (itx: Scope) => T): Promise<Awaited<T>>;
117
+ }
118
+ export type ConfigEventArgs = {
119
+ event: StreamEvent;
120
+ range: ScannedRange;
121
+ itx: ItxScope;
122
+ };
123
+ export declare abstract class ConfigWorker<Env extends {
124
+ ITX: ItxEntrypointService;
125
+ } = {
126
+ ITX: ItxEntrypointService;
127
+ }> extends WorkerEntrypoint<Env> {
128
+ /** At fetch entry: `const denied = this.auth.require(request); if (denied) return denied;`
129
+ * `x-itx-principal` is on a request only when a project member (or the operator) sent it, safe
130
+ * to act on. A private route written by hand answers the platform's sign-in challenge, which
131
+ * the edge turns into the sign-in for a page load (`auth.require` does the same):
132
+ *
133
+ * ```js
134
+ * if (!request.headers.get("x-itx-principal"))
135
+ * return new Response("Sign in\n", { status: 401, headers: { "WWW-Authenticate": 'Bearer realm="iterate"' } });
136
+ * ``` */
137
+ protected readonly auth: {
138
+ require(request: Request): Response | null;
139
+ };
140
+ /** Process an explicitly subscribed batch with this worker's context scope. */
141
+ processEventBatch(events: StreamEvent[], range: ScannedRange): Promise<void>;
142
+ /** ONE round trip on the itx scope, then release the scope and every call made through it
143
+ * (`StreamProcessorDurableObject.withItx` says why an undisposed step keeps a context billed). */
144
+ protected withItx<T>(call: (itx: ItxScope) => T): Promise<Awaited<T>>;
145
+ /** THE AUTHOR HOOK — one event at a time, in offset order. Append reactions through the itx scope;
146
+ * make them idempotent (a redelivery must be a no-op). Default: ignore the event. */
147
+ processEvent(_args: ConfigEventArgs): void | Promise<void>;
148
+ /** THE WEB ROOT — every Request on a host of the project (the project's configured ingress
149
+ * target). The host's routing slug is in `x-iterate-routing-slug` (`notes` for
150
+ * `notes--<project>.<hostname>`; absent on the apex), written only by the platform: route on it
151
+ * in plain code, answering here (reaching the context through `this.withItx`) or forwarding the
152
+ * Request. Default: not found. */
153
+ fetch(_request: Request): Response | Promise<Response>;
154
+ }
155
+ export { RunContract, RunRequested, RunSettled } from "../stream/run.ts";
@@ -0,0 +1,19 @@
1
+ /** ONE round trip on `entrypoint.get()`, then RELEASE EVERYTHING IT REACHED: the scope and every call
2
+ * `call` made through it or through a handle it awaited, the last first. A release that throws is reported and the rest still run
3
+ * (lib.ts `releaseRpcSessions`), so the call's answer stands. Data it answers stays usable; a stub or
4
+ * handle it answers is released with the rest, so return data.
5
+ *
6
+ * const { projectSlug } = await withItx(this.env.ITX, (itx) => itx.whoami());
7
+ */
8
+ export declare function withItx<Scope, T>(entrypoint: {
9
+ get(): Scope;
10
+ }, call: (itx: Scope) => T): Promise<Awaited<T>>;
11
+ /** `stub` as the caller sees it, except that every CALL made through it — at any depth, on the stub,
12
+ * on a call's result, or on the handle a call's result resolves to once awaited — is pushed onto
13
+ * `steps`, so the caller can dispose each one: a Workers-RPC call's result is a stub-bearing promise
14
+ * that keeps its session open until disposed, awaited or not. Awaiting hands back a handle (a stub
15
+ * is callable, in workerd and capnweb alike) recorded and pushed too, and plain data untouched, so
16
+ * data still copies across RPC. `catch`/`finally` and symbol members (`Symbol.dispose`) are the
17
+ * value's own, bound to it, so disposing behaves exactly as on the bare stub; an argument that is
18
+ * itself a recorded value crosses the wire as the stub it wraps. */
19
+ export declare function recordPipelinedSteps<T>(stub: T, steps: unknown[]): T;
package/dist/sdk.mjs ADDED
@@ -0,0 +1,245 @@
1
+ import { applyPatch, diff, isSameOriginBrowserRequest, jsonEqual, releaseRpcSessions } from "./lib.mjs";
2
+ import { LiveState, ProcessorEngine, ReduceCheckpointTable, StreamProcessor, defineProcessorContract } from "./stream/processor.mjs";
3
+ import { ITX_PRINCIPAL_HEADER } from "./principal.mjs";
4
+ import { RunContract, RunRequested, RunSettled } from "./stream/run.mjs";
5
+ import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
6
+ import { z, z as z$1 } from "zod";
7
+ import { newHttpBatchRpcSession, newWebSocketRpcSession, newWorkersRpcResponse } from "capnweb";
8
+ //#region src/sdk/auth.ts
9
+ const Principal = z$1.object({
10
+ actor: z$1.string().min(1),
11
+ email: z$1.string().optional()
12
+ });
13
+ /** Project ingress strips public identity headers and stamps `x-itx-principal` for a project member
14
+ * only: a visitor signed out, signed in without this project, or riding a session cookie on a
15
+ * cross-site write arrives without one. This guard runs in the config worker, before it proxies an
16
+ * app.
17
+ *
18
+ * Signed out, every request gets `401` with `WWW-Authenticate: Bearer realm="iterate"`: the
19
+ * platform's edge turns that answer into the sign-in for a page load (or into "sign in again with
20
+ * this project" for someone signed in without it), whatever path the app is served under, and hands
21
+ * a fetch, a write or a WebSocket upgrade the 401 itself. Any app can ask for a signed-in visitor
22
+ * the same way:
23
+ *
24
+ * ```js
25
+ * if (!request.headers.get("x-itx-principal"))
26
+ * return new Response("Sign in\n", { status: 401, headers: { "WWW-Authenticate": 'Bearer realm="iterate"' } });
27
+ * ```
28
+ *
29
+ * A write or a WebSocket upgrade must also come from this origin (or carry no Origin, a non-browser
30
+ * client), else 403. The edge already sends such a cookie request on anonymous; this repeats the
31
+ * check where the app runs. The handshake is a GET, but it opens a two-way channel, and the app
32
+ * session cookie is `SameSite=Lax`: every `<routingSlug>--<project>.iterate.app` host is same-site with
33
+ * every other, so a page on another project's host could otherwise open a socket to this app with
34
+ * the visitor's cookie. */
35
+ const auth = { require(request) {
36
+ const isWebSocketUpgrade = request.headers.get("upgrade")?.toLowerCase() === "websocket";
37
+ if (!([
38
+ "GET",
39
+ "HEAD",
40
+ "OPTIONS"
41
+ ].includes(request.method) && !isWebSocketUpgrade) && !isSameOriginBrowserRequest(request)) return new Response("Cross-site request refused", { status: 403 });
42
+ const principal = request.headers.get(ITX_PRINCIPAL_HEADER);
43
+ if (principal) {
44
+ Principal.parse(JSON.parse(principal));
45
+ return null;
46
+ }
47
+ return new Response("Sign in\n", {
48
+ status: 401,
49
+ headers: {
50
+ "WWW-Authenticate": "Bearer realm=\"iterate\"",
51
+ "Cache-Control": "no-store"
52
+ }
53
+ });
54
+ } };
55
+ //#endregion
56
+ //#region src/sdk/record-pipelined-steps.ts
57
+ /** ONE round trip on `entrypoint.get()`, then RELEASE EVERYTHING IT REACHED: the scope and every call
58
+ * `call` made through it or through a handle it awaited, the last first. A release that throws is reported and the rest still run
59
+ * (lib.ts `releaseRpcSessions`), so the call's answer stands. Data it answers stays usable; a stub or
60
+ * handle it answers is released with the rest, so return data.
61
+ *
62
+ * const { projectSlug } = await withItx(this.env.ITX, (itx) => itx.whoami());
63
+ */
64
+ async function withItx(entrypoint, call) {
65
+ const steps = [];
66
+ const itx = entrypoint.get();
67
+ try {
68
+ return await call(recordPipelinedSteps(itx, steps));
69
+ } finally {
70
+ releaseRpcSessions([itx, ...steps]);
71
+ }
72
+ }
73
+ /** `stub` as the caller sees it, except that every CALL made through it — at any depth, on the stub,
74
+ * on a call's result, or on the handle a call's result resolves to once awaited — is pushed onto
75
+ * `steps`, so the caller can dispose each one: a Workers-RPC call's result is a stub-bearing promise
76
+ * that keeps its session open until disposed, awaited or not. Awaiting hands back a handle (a stub
77
+ * is callable, in workerd and capnweb alike) recorded and pushed too, and plain data untouched, so
78
+ * data still copies across RPC. `catch`/`finally` and symbol members (`Symbol.dispose`) are the
79
+ * value's own, bound to it, so disposing behaves exactly as on the bare stub; an argument that is
80
+ * itself a recorded value crosses the wire as the stub it wraps. */
81
+ function recordPipelinedSteps(stub, steps) {
82
+ const wrapped = /* @__PURE__ */ new WeakMap();
83
+ const record = (value, receiver) => {
84
+ if (!value || typeof value !== "object" && typeof value !== "function") return value;
85
+ const proxy = new Proxy(value, {
86
+ get(target, key) {
87
+ const member = Reflect.get(target, key);
88
+ if (key === "then" && typeof member === "function") return (onFulfilled, onRejected) => Reflect.apply(member, target, [typeof onFulfilled === "function" ? (answer) => {
89
+ if (typeof answer !== "function") return onFulfilled(answer);
90
+ steps.push(answer);
91
+ return onFulfilled(record(answer, void 0));
92
+ } : onFulfilled, onRejected]);
93
+ if (typeof key === "symbol" || key === "then" || key === "catch" || key === "finally") return typeof member === "function" ? member.bind(target) : member;
94
+ return record(member, target);
95
+ },
96
+ apply(target, _proxyReceiver, args) {
97
+ const result = Reflect.apply(target, receiver, args.map((arg) => wrapped.get(Object(arg)) ?? arg));
98
+ steps.push(result);
99
+ return record(result, void 0);
100
+ }
101
+ });
102
+ wrapped.set(proxy, value);
103
+ return proxy;
104
+ };
105
+ return record(stub, void 0);
106
+ }
107
+ //#endregion
108
+ //#region src/sdk/index.ts
109
+ /** THE FACET SHELL: a `DurableObject` a context hosts as a facet — `itx.facets.get(name, { source,
110
+ * className })`, a rule naming it, or a processor's row. A caller reaches a facet by itx expression
111
+ * (`itx.facets.get(name).<method>(…)`) only through what its class lists in `publicMethods`: the
112
+ * context refuses any other first step FORBIDDEN before the call reaches the facet
113
+ * (apps/os context/facet-public-methods.ts). The platform's own calls — the delivery loop's push
114
+ * and catch-up, the alarm's revive — never go through the list. A loaded class that does not
115
+ * extend this shell lists nothing, so no caller reaches it by expression. */
116
+ var FacetDurableObject = class extends DurableObject {
117
+ /** What a caller may reach by itx expression: the FIRST step of `itx.facets.get(name).<step>…`, a
118
+ * method or a property of this class. A subclass lists its own on top of its parent's:
119
+ * `static override publicMethods = [...super.publicMethods, "send"]`. */
120
+ static publicMethods = ["fetch"];
121
+ /** This class's `publicMethods`, for the context that loaded it — a static does not cross the
122
+ * isolate. On no list: only the context asks it. */
123
+ listPublicMethods() {
124
+ return this.constructor.publicMethods;
125
+ }
126
+ };
127
+ var StreamProcessorDurableObject = class extends FacetDurableObject {
128
+ /** The reads a caller reaches on every processor: `fetch`, and the state caught up through the log
129
+ * (`snapshot`, `liveSnapshot`) or awaited (`waitUntilProcessed`). What feeds the processor —
130
+ * `processEventBatch`, `catchUpFromLog`, `revive` — is the platform's, never a caller's. */
131
+ static publicMethods = [
132
+ ...super.publicMethods,
133
+ "snapshot",
134
+ "liveSnapshot",
135
+ "waitUntilProcessed"
136
+ ];
137
+ /** After a runtime field on the processor moved OUTSIDE a batch (an RPC method on this object);
138
+ * inside `processEvent` the engine re-projects on its own. */
139
+ publishLiveState() {
140
+ this.#engine.publishLiveState();
141
+ }
142
+ /** THE push: the context hands over each committed batch with its scanned-range proof. */
143
+ processEventBatch(events, range) {
144
+ return this.#engine.processEventBatch(events, range);
145
+ }
146
+ /** Catch up from the log (the delivery loop's, when a row is configured or resumed). */
147
+ catchUpFromLog() {
148
+ return this.#engine.catchUpFromLog();
149
+ }
150
+ /** THE REVIVE: the context's alarm pass calls it for a due claim — catch up, then run the
151
+ * at-head pass, so an attempt the last incarnation was running is started again from state. */
152
+ revive() {
153
+ return this.#engine.revive();
154
+ }
155
+ /** Caught up through the log, then `{ offset, state }`. */
156
+ snapshot() {
157
+ return this.#engine.snapshot();
158
+ }
159
+ /** The live-state seed read: `{ rev, state: projectLiveState(reduced) }`. */
160
+ liveSnapshot() {
161
+ return this.#engine.liveSnapshot();
162
+ }
163
+ /** The barrier: resolves once processed at least through `offset` (default timeout 10s). */
164
+ waitUntilProcessed(input) {
165
+ return this.#engine.waitUntilProcessed(input);
166
+ }
167
+ /** The loopback to this facet's context: a LOADED class gets it as `env.ITX` (the loader bakes the
168
+ * stub in, worker-loader.ts); a class of THIS worker hosted through `ctx.exports` has the
169
+ * worker's real env and mints the same stub itself from its props — `ctx.exports` is populated
170
+ * inside a facet (__workers-tests__/facet-props.test.ts). */
171
+ #itxEntrypoint() {
172
+ return this.env.ITX ?? this.ctx.exports.ItxEntrypoint({ props: {
173
+ iterateContextName: this.ctx.props.iterateContextName,
174
+ platform: true
175
+ } });
176
+ }
177
+ #engineBuiltOnFirstUse;
178
+ get #engine() {
179
+ return this.#engineBuiltOnFirstUse ??= new ProcessorEngine(this.processor, {
180
+ stream: {
181
+ append: (...events) => this.withItx((itx) => itx.append(...events)),
182
+ read: (after, limit) => this.withItx((itx) => itx.readEvents(after, limit)),
183
+ claim: (at) => this.withItx((itx) => itx.processors.claim(this.ctx.props.name, at))
184
+ },
185
+ storage: new ReduceCheckpointTable(this.ctx.storage.sql),
186
+ fedByPushes: this.ctx.props.fedByPushes === true
187
+ });
188
+ }
189
+ /** ONE round trip on the itx scope, then RELEASE EVERYTHING IT REACHED: the get, and every call the
190
+ * callback made through it — not only the last. A Workers-RPC value this facet leaves undisposed —
191
+ * the `itx.cd(path)` of `itx.cd(path).append(…)`, the `cfArtifacts.get(p)` of `.remote()`, an
192
+ * answer awaited inside the callback (`const { state } = await context.invoke(…)`), data included —
193
+ * keeps THIS FACET running after its context is evicted, until V8 collects the value, which an
194
+ * idle isolate may not do for many minutes: each new incarnation of the context reattaches to the
195
+ * facet, and the object stays billed (measured 2026-09-23: a new website project's `/` and
196
+ * `/repos/config` billed 60 s of every minute for 30 min with no request). The context's own
197
+ * `invoke` cannot end this from its side: the facet holds the value (context-residency.e2e.test.ts,
198
+ * "… does not outlive …"). Protected: a host with methods of its own (the workspace,
199
+ * src/workspace/durable-object.ts) reaches its context the same way. */
200
+ withItx(call) {
201
+ return withItx(this.#itxEntrypoint(), call);
202
+ }
203
+ };
204
+ var ConfigWorker = class extends WorkerEntrypoint {
205
+ /** At fetch entry: `const denied = this.auth.require(request); if (denied) return denied;`
206
+ * `x-itx-principal` is on a request only when a project member (or the operator) sent it, safe
207
+ * to act on. A private route written by hand answers the platform's sign-in challenge, which
208
+ * the edge turns into the sign-in for a page load (`auth.require` does the same):
209
+ *
210
+ * ```js
211
+ * if (!request.headers.get("x-itx-principal"))
212
+ * return new Response("Sign in\n", { status: 401, headers: { "WWW-Authenticate": 'Bearer realm="iterate"' } });
213
+ * ``` */
214
+ auth = auth;
215
+ /** Process an explicitly subscribed batch with this worker's context scope. */
216
+ async processEventBatch(events, range) {
217
+ await this.withItx(async (itx) => {
218
+ for (const event of events) await this.processEvent({
219
+ event,
220
+ range,
221
+ itx
222
+ });
223
+ });
224
+ }
225
+ /** ONE round trip on the itx scope, then release the scope and every call made through it
226
+ * (`StreamProcessorDurableObject.withItx` says why an undisposed step keeps a context billed). */
227
+ withItx(call) {
228
+ return withItx(this.env.ITX, call);
229
+ }
230
+ /** THE AUTHOR HOOK — one event at a time, in offset order. Append reactions through the itx scope;
231
+ * make them idempotent (a redelivery must be a no-op). Default: ignore the event. */
232
+ processEvent(_args) {}
233
+ /** THE WEB ROOT — every Request on a host of the project (the project's configured ingress
234
+ * target). The host's routing slug is in `x-iterate-routing-slug` (`notes` for
235
+ * `notes--<project>.<hostname>`; absent on the apex), written only by the platform: route on it
236
+ * in plain code, answering here (reaching the context through `this.withItx`) or forwarding the
237
+ * Request. Default: not found. */
238
+ fetch(_request) {
239
+ return new Response("Not found\n", { status: 404 });
240
+ }
241
+ };
242
+ //#endregion
243
+ export { ConfigWorker, FacetDurableObject, LiveState, RunContract, RunRequested, RunSettled, StreamProcessor, StreamProcessorDurableObject, applyPatch, defineProcessorContract, diff, jsonEqual, newHttpBatchRpcSession, newWebSocketRpcSession, newWorkersRpcResponse, withItx, z };
244
+
245
+ //# sourceMappingURL=sdk.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk.mjs","names":["z","#engine","#engineBuiltOnFirstUse","#itxEntrypoint"],"sources":["../src/sdk/auth.ts","../src/sdk/record-pipelined-steps.ts","../src/sdk/index.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { isSameOriginBrowserRequest } from \"../lib.ts\";\nimport { ITX_PRINCIPAL_HEADER } from \"../principal.ts\";\n\nconst Principal = z.object({ actor: z.string().min(1), email: z.string().optional() });\n\n/** Project ingress strips public identity headers and stamps `x-itx-principal` for a project member\n * only: a visitor signed out, signed in without this project, or riding a session cookie on a\n * cross-site write arrives without one. This guard runs in the config worker, before it proxies an\n * app.\n *\n * Signed out, every request gets `401` with `WWW-Authenticate: Bearer realm=\"iterate\"`: the\n * platform's edge turns that answer into the sign-in for a page load (or into \"sign in again with\n * this project\" for someone signed in without it), whatever path the app is served under, and hands\n * a fetch, a write or a WebSocket upgrade the 401 itself. Any app can ask for a signed-in visitor\n * the same way:\n *\n * ```js\n * if (!request.headers.get(\"x-itx-principal\"))\n * return new Response(\"Sign in\\n\", { status: 401, headers: { \"WWW-Authenticate\": 'Bearer realm=\"iterate\"' } });\n * ```\n *\n * A write or a WebSocket upgrade must also come from this origin (or carry no Origin, a non-browser\n * client), else 403. The edge already sends such a cookie request on anonymous; this repeats the\n * check where the app runs. The handshake is a GET, but it opens a two-way channel, and the app\n * session cookie is `SameSite=Lax`: every `<routingSlug>--<project>.iterate.app` host is same-site with\n * every other, so a page on another project's host could otherwise open a socket to this app with\n * the visitor's cookie. */\nexport const auth = {\n require(request: Request): Response | null {\n const isWebSocketUpgrade = request.headers.get(\"upgrade\")?.toLowerCase() === \"websocket\";\n const isRead = [\"GET\", \"HEAD\", \"OPTIONS\"].includes(request.method) && !isWebSocketUpgrade;\n if (!isRead && !isSameOriginBrowserRequest(request))\n return new Response(\"Cross-site request refused\", { status: 403 });\n const principal = request.headers.get(ITX_PRINCIPAL_HEADER);\n if (principal) {\n Principal.parse(JSON.parse(principal)); // Platform-owned stamp; malformed means a defect.\n return null;\n }\n return new Response(\"Sign in\\n\", {\n status: 401,\n headers: { \"WWW-Authenticate\": 'Bearer realm=\"iterate\"', \"Cache-Control\": \"no-store\" },\n });\n },\n};\n","// sdk/record-pipelined-steps.ts — `withItx`, THE one way code reaches its context: ONE round trip on\n// `env.ITX`, then RELEASE a Workers-RPC round trip completely — the scope and every call it made, not\n// only the last. Loaded code imports it from \"./processor.js\" (`withItx(this.env.ITX, (itx) => …)`); the\n// SDK's hosts (`StreamProcessorDurableObject.withItx`, `ConfigWorker.withItx`) delegate to it. No\n// workerd import, so the unit tests run it in node (record-pipelined-steps.test.ts) and the platform\n// bundles it alone for a script's isolate (apps/os `runScriptModule`); on native RpcPromises it is\n// proven by every apps/os e2e row that reaches a facet, and pinned by\n// apps/os/e2e/context-residency.e2e.test.ts (\"… does not outlive …\": a facet that kept one value from\n// its context stayed running, billed). Lint refuses the raw `env.ITX.get()` (iterate/no-raw-itx-get).\n\nimport { releaseRpcSessions } from \"../lib.ts\";\n\n/** ONE round trip on `entrypoint.get()`, then RELEASE EVERYTHING IT REACHED: the scope and every call\n * `call` made through it or through a handle it awaited, the last first. A release that throws is reported and the rest still run\n * (lib.ts `releaseRpcSessions`), so the call's answer stands. Data it answers stays usable; a stub or\n * handle it answers is released with the rest, so return data.\n *\n * const { projectSlug } = await withItx(this.env.ITX, (itx) => itx.whoami());\n */\nexport async function withItx<Scope, T>(\n entrypoint: { get(): Scope },\n call: (itx: Scope) => T,\n): Promise<Awaited<T>> {\n const steps: unknown[] = [];\n const itx = entrypoint.get();\n try {\n return await call(recordPipelinedSteps(itx, steps));\n } finally {\n releaseRpcSessions([itx, ...steps]);\n }\n}\n\n/** `stub` as the caller sees it, except that every CALL made through it — at any depth, on the stub,\n * on a call's result, or on the handle a call's result resolves to once awaited — is pushed onto\n * `steps`, so the caller can dispose each one: a Workers-RPC call's result is a stub-bearing promise\n * that keeps its session open until disposed, awaited or not. Awaiting hands back a handle (a stub\n * is callable, in workerd and capnweb alike) recorded and pushed too, and plain data untouched, so\n * data still copies across RPC. `catch`/`finally` and symbol members (`Symbol.dispose`) are the\n * value's own, bound to it, so disposing behaves exactly as on the bare stub; an argument that is\n * itself a recorded value crosses the wire as the stub it wraps. */\nexport function recordPipelinedSteps<T>(stub: T, steps: unknown[]): T {\n const wrapped = new WeakMap<object, object>();\n const record = (value: unknown, receiver: unknown): unknown => {\n // oxlint-disable-next-line iterate/simple-truthiness-check -- a Proxy target must be an object or a function: a call may answer any value, and only those two can be wrapped\n if (!value || (typeof value !== \"object\" && typeof value !== \"function\")) return value;\n const proxy = new Proxy(value, {\n get(target, key) {\n const member: unknown = Reflect.get(target, key);\n if (key === \"then\" && typeof member === \"function\")\n // `const repo = await itx.repos.get(p); await repo.whoami()`: disposing the step releases\n // `repo` (workerd disposes a promise's result with it), never `whoami`'s call, and an\n // awaited property (`await itx.repos`) is no step at all.\n return (onFulfilled?: unknown, onRejected?: unknown) =>\n Reflect.apply(member, target, [\n typeof onFulfilled === \"function\"\n ? (answer: unknown) => {\n if (typeof answer !== \"function\") return onFulfilled(answer);\n steps.push(answer);\n return onFulfilled(record(answer, undefined));\n }\n : onFulfilled,\n onRejected,\n ]);\n if (typeof key === \"symbol\" || key === \"then\" || key === \"catch\" || key === \"finally\")\n return typeof member === \"function\" ? member.bind(target) : member;\n return record(member, target);\n },\n apply(target, _proxyReceiver, args: unknown[]) {\n // Only a callable target reaches this trap; the call runs on the unwrapped receiver, as\n // `stub.method(…)` would have.\n const result: unknown = Reflect.apply(\n target as (...args: unknown[]) => unknown,\n receiver,\n // `Object(arg)` is a fresh wrapper for a primitive, so only a recorded value is found.\n args.map((arg) => wrapped.get(Object(arg)) ?? arg),\n );\n steps.push(result);\n return record(result, undefined);\n },\n });\n wrapped.set(proxy, value);\n return proxy;\n };\n // The proxy answers every member the stub does (it forwards each one), so it is the stub's type.\n return record(stub, undefined) as T;\n}\n","// sdk/index.ts — THE userspace SDK surface, bundled (zod included — the owner's call) into every\n// loaded isolate as `processor.js` (apps/os/scripts/build.ts bundles it):\n//\n// import { StreamProcessor, StreamProcessorDurableObject, defineProcessorContract, z } from \"./processor.js\";\n//\n// The workerd HOSTS live here too (this file imports cloudflare:workers; the Node unit tests never import it):\n// FacetDurableObject — the `DurableObject` shell a context hosts as a facet: its class lists\n// the methods a caller reaches by itx expression (`publicMethods`)\n// StreamProcessorDurableObject — the facet shell that hosts ONE `StreamProcessor`\n// ConfigWorker — the stateless `WorkerEntrypoint` a project's one event handler extends\n\nimport { DurableObject, WorkerEntrypoint } from \"cloudflare:workers\";\nimport type { IterateContextApi, StreamPage } from \"../api.ts\";\nimport {\n ProcessorEngine,\n type ScannedRange,\n type StreamProcessor,\n ReduceCheckpointTable,\n type StreamEvent,\n type StreamEventInput,\n} from \"../stream/processor.ts\";\nimport { auth } from \"./auth.ts\";\nimport { withItx } from \"./record-pipelined-steps.ts\";\n// THE ONE WAY code reaches its context: `withItx(this.env.ITX, (itx) => …)` — one round trip, then\n// everything it reached released (record-pipelined-steps.ts). A host's `this.withItx(fn)` is the same\n// function. Never `env.ITX.get()` alone: whatever it hands out keeps this isolate, and the object\n// hosting it, running and billed after the context is evicted (lint: iterate/no-raw-itx-get).\nexport { withItx };\nexport {\n // LIVE STATE for a mini-app DO that is NOT a processor (a processor's base owns one internally):\n // `new LiveState({ append: (e) => withItx(this.env.ITX, (itx) => itx.append(e)) }, \"chat\", {…})` — a\n // field initializer cannot await — then `set` to mutate and `snapshot()` as the client's seed read\n // (stream/processor.ts).\n LiveState,\n StreamProcessor,\n defineProcessorContract,\n type ConsumedEvent,\n type EventCatalog,\n type EventDefinition,\n type EmittedEventInput,\n type EventInput,\n type LiveStateSink,\n type ProcessorContract,\n type ProcessorState,\n type ProcessorStream,\n type ProcessEventArgs,\n type ReduceArgs,\n type ScannedRange,\n type StreamEvent,\n type StreamEventInput,\n} from \"../stream/processor.ts\";\nexport { z } from \"zod\";\n// capnweb's CLIENT constructors, so userspace can dial a remote capnweb API from inside its isolate\n// through the context's own egress, and `newWorkersRpcResponse`, the SERVER half, so a loaded worker\n// can serve a capnweb API over its `fetch`. The HTTP batch is exported ON PURPOSE beside the\n// WebSocket session: a stateless entrypoint answering one method with one remote call has no session\n// to hold across calls, and a one-shot POST is the honest shape (the lint rule targets long-lived workers).\n// oxlint-disable-next-line iterate/no-capnweb-http-batch -- userspace one-shot remote calls; see above\nexport { newHttpBatchRpcSession, newWebSocketRpcSession, newWorkersRpcResponse } from \"capnweb\";\nexport { applyPatch, diff, jsonEqual, type PatchOp } from \"../lib.ts\";\n// ── StreamProcessorDurableObject ── THE SDK HOST: the `DurableObject` shell that hosts ONE\n// `StreamProcessor` as a facet of its context. An author writes the pure processor and its host,\n// one line long:\n//\n// export class PresenceDurableObject extends StreamProcessorDurableObject {\n// processor = new PresenceProcessor();\n// }\n//\n// hosted through the ordinary `itx.facets.get('presence', { source, className: 'PresenceDurableObject' })`\n// — a processor is a named facet that additionally gets pushed every commit. `processor` is a FIELD\n// so it can take what its effects need from this object — reach as a `WithItx` accessor, never a\n// scope: `new Notifier((call) => this.withItx(call))` — and so the same class is constructed bare in\n// a test. A method of the host's own that callers reach by itx expression goes on its list:\n// `static override publicMethods = [...super.publicMethods, \"message\"]`.\n//\n// IDENTITY is `ctx.props` — `{ iterateContextName, name }`, minted by the parent, the only party\n// that knows it (pinned in __workers-tests__/facet-props.test.ts), plus `fedByPushes` when a row\n// pushes it (FacetProps). THE STREAM is the itx scope `this.withItx(fn)` hands `fn` (apps/os\n// iterate-context.ts `ItxEntrypoint`); the engine's `append`/`read` ride it like any other dotted call.\n//\n// NEVER define alarm(): facets have none (workerd#6810 — the runtime answers \"Facets currently\n// cannot set alarms.\"); a timer, when one is needed, is a scheduled append on the context. The\n// engine's own recovery is a CLAIM on the context's alarm (processor.ts, rule 3): while a\n// `runInBackground` attempt is in flight the context owes this facet a `revive()`, so a host that\n// dies mid-attempt is re-materialized and runs its at-head pass again\n// (__workers-tests__/agent-revive.test.ts: an LLM call survives its context's death).\n//\n// THE CLAIM IS ALSO WHAT KEEPS A FACET RUNNING: a loaded facet that holds none when its context\n// starts a new incarnation is reset then (os FacetHost `resetUnclaimedLoadedFacets`). So work that\n// must outlive the call that started it — a model request, a retry's backoff sleep, an open\n// provider socket — runs through `runInBackground` (ProcessEventArgs), never as a bare floating\n// promise, a `ctx.waitUntil` or a timer the facet keeps on its own.\n\n/** What the parent mints a facet's class with — the whole identity, and one fact about its feed. */\nexport type FacetProps = {\n iterateContextName: string;\n name: string;\n /** Set when, as this facet started, a subscription row of its context pushed it every commit it\n * consumes (`processEventBatch`, the delivery loop's push): a processor's engine then trusts the\n * head a catch-up read until the next push (stream/processor.ts, the read verbs). Absent, only a\n * push is proof, so a processor no row pushes reads its log on every read. */\n fedByPushes?: true;\n};\n\n/** THE FACET SHELL: a `DurableObject` a context hosts as a facet — `itx.facets.get(name, { source,\n * className })`, a rule naming it, or a processor's row. A caller reaches a facet by itx expression\n * (`itx.facets.get(name).<method>(…)`) only through what its class lists in `publicMethods`: the\n * context refuses any other first step FORBIDDEN before the call reaches the facet\n * (apps/os context/facet-public-methods.ts). The platform's own calls — the delivery loop's push\n * and catch-up, the alarm's revive — never go through the list. A loaded class that does not\n * extend this shell lists nothing, so no caller reaches it by expression. */\nexport abstract class FacetDurableObject<Env = unknown> extends DurableObject<Env, FacetProps> {\n /** What a caller may reach by itx expression: the FIRST step of `itx.facets.get(name).<step>…`, a\n * method or a property of this class. A subclass lists its own on top of its parent's:\n * `static override publicMethods = [...super.publicMethods, \"send\"]`. */\n static publicMethods: readonly string[] = [\"fetch\"];\n\n /** This class's `publicMethods`, for the context that loaded it — a static does not cross the\n * isolate. On no list: only the context asks it. */\n listPublicMethods(): readonly string[] {\n // `this.constructor` is the concrete facet class, a subclass of this one; TypeScript types it as\n // `Function`, which has no `publicMethods`.\n return (this.constructor as typeof FacetDurableObject).publicMethods;\n }\n}\n\n/** The itx scope `withItx` hands its callback: a context's declared API (api.ts) — a capnweb stub\n * of apps/os's `IterateContextRpcTarget`, which satisfies it. */\nexport type ItxScope = IterateContextApi;\n/** What hands the scope over: the loopback entrypoint a loaded worker has as `env.ITX`, or the one a\n * class of the platform's own worker mints from `ctx.exports`. */\nexport type ItxEntrypointService = { get(): ItxScope };\n/** The least a host needs of its scope: the fixed-point log calls the engine makes. The platform's own\n * facets pass the Workers-RPC STUB of a context (every dotted step pipelined; a property there is a\n * promise), which no plain-promise interface can name — so the constraint is this, not `ItxScope`. */\nexport type ProcessorScope = {\n append(...events: StreamEventInput[]): Promise<unknown>;\n readEvents(afterOffset?: number, limit?: number): Promise<unknown>;\n /** The engine's claim on the context's alarm (processor.ts rule 3): \"come back by `at`\", or null. */\n processors: { claim(name: string, at: number | null): Promise<unknown> };\n /** Another context of the project by its dotted surface (`.append`), which the platform's handle\n * and a loaded worker's alike answer — how an entity's processor cross-posts its certificate to\n * `/` (`withItx((itx) => itx.cd(\"/\").append(certificate))`). Through the table like every other\n * word here: a loaded processor's `cd` goes down only (the app wall), the platform's own go\n * anywhere within the project. */\n cd(path: string): { append(...events: StreamEventInput[]): Promise<unknown> };\n};\n\n/** THE SCOPE ACCESSOR a host hands its processor: one pipelined round trip on the context's itx,\n * released after (`StreamProcessorDurableObject.withItx`). A processor that needs an effect —\n * `itx.cfArtifacts.create(path)`, `itx.ai.run(…)` — takes this and nothing else, so a unit test\n * hands it a fake and the e2e lends one by rule on the context. */\nexport type WithItx<Scope = ItxScope> = <T>(call: (itx: Scope) => T) => Promise<Awaited<T>>;\n\nexport abstract class StreamProcessorDurableObject<\n State = unknown,\n Env extends { ITX?: ItxEntrypointService } = { ITX: ItxEntrypointService },\n Scope extends ProcessorScope = ItxScope,\n> extends FacetDurableObject<Env> {\n /** The reads a caller reaches on every processor: `fetch`, and the state caught up through the log\n * (`snapshot`, `liveSnapshot`) or awaited (`waitUntilProcessed`). What feeds the processor —\n * `processEventBatch`, `catchUpFromLog`, `revive` — is the platform's, never a caller's. */\n static override publicMethods = [\n ...super.publicMethods,\n \"snapshot\",\n \"liveSnapshot\",\n \"waitUntilProcessed\",\n ];\n\n /** The processor this object hosts — `processor = new PresenceProcessor()` at the top of the subclass. */\n abstract readonly processor: StreamProcessor<State>;\n\n // ── what an author reaches (the itx scope: `this.withItx(fn)`; identity: `this.ctx.props`) ──\n\n /** After a runtime field on the processor moved OUTSIDE a batch (an RPC method on this object);\n * inside `processEvent` the engine re-projects on its own. */\n protected publishLiveState(): void {\n this.#engine.publishLiveState();\n }\n\n // ── what the platform calls: the delivery loop's push and catch-up, the alarm's revive ──\n\n /** THE push: the context hands over each committed batch with its scanned-range proof. */\n processEventBatch(events: StreamEvent[], range: ScannedRange): Promise<void> {\n return this.#engine.processEventBatch(events, range);\n }\n /** Catch up from the log (the delivery loop's, when a row is configured or resumed). */\n catchUpFromLog(): Promise<void> {\n return this.#engine.catchUpFromLog();\n }\n /** THE REVIVE: the context's alarm pass calls it for a due claim — catch up, then run the\n * at-head pass, so an attempt the last incarnation was running is started again from state. */\n revive(): Promise<void> {\n return this.#engine.revive();\n }\n\n // ── what a caller reaches by itx expression (`publicMethods`) ──\n\n /** Caught up through the log, then `{ offset, state }`. */\n snapshot(): Promise<{ offset: number; state: State }> {\n return this.#engine.snapshot();\n }\n /** The live-state seed read: `{ rev, state: projectLiveState(reduced) }`. */\n liveSnapshot(): Promise<{ rev: number; state: unknown }> {\n return this.#engine.liveSnapshot();\n }\n /** The barrier: resolves once processed at least through `offset` (default timeout 10s). */\n waitUntilProcessed(input: { offset: number; timeoutMs?: number }): Promise<void> {\n return this.#engine.waitUntilProcessed(input);\n }\n\n /** The loopback to this facet's context: a LOADED class gets it as `env.ITX` (the loader bakes the\n * stub in, worker-loader.ts); a class of THIS worker hosted through `ctx.exports` has the\n * worker's real env and mints the same stub itself from its props — `ctx.exports` is populated\n * inside a facet (__workers-tests__/facet-props.test.ts). */\n #itxEntrypoint(): { get(): Scope } {\n return (this.env.ITX ??\n (\n this.ctx.exports as unknown as {\n ItxEntrypoint: (options: { props: object }) => ItxEntrypointService;\n }\n ).ItxEntrypoint({\n // PLATFORM: this worker's own class, minted from its own exports — the full handle, the fixed\n // point spellable, `cd` free to go up. A LOADED class never reaches this branch (it has\n // `env.ITX`, baked in by the loader, and its own module's exports).\n props: { iterateContextName: this.ctx.props.iterateContextName, platform: true },\n })) as unknown as {\n get(): Scope;\n };\n }\n // ── the engine: one ProcessorEngine over `processor` and this object's storage, built on first use —\n // `processor` is a subclass field, which does not exist yet while this base class constructs. ──\n #engineBuiltOnFirstUse?: ProcessorEngine<State>;\n get #engine(): ProcessorEngine<State> {\n return (this.#engineBuiltOnFirstUse ??= new ProcessorEngine(this.processor, {\n // The engine's own emits, catch-up and gap repair are the CONTEXT ROOTS `append`, `readEvents`,\n // `processors.claim` — implicit in every context (itx-expression-rewriting.ts rule 3), so they\n // resolve to this log with no row and no hop; a row at `itx.append` is the OWNER's deliberate\n // wall (a jailed processor halts visibly), never a loaded worker's — the fixed point is not a\n // loaded worker's word.\n stream: {\n // A stub scope's answers are pipelined shapes by type and plain data on the wire (the\n // engine awaits them): the engine's own types, asserted.\n append: (...events) =>\n this.withItx((itx) => itx.append(...events)) as Promise<StreamEvent[]>,\n read: (after, limit) =>\n this.withItx((itx) => itx.readEvents(after, limit)) as Promise<StreamPage>,\n claim: (at) => this.withItx((itx) => itx.processors.claim(this.ctx.props.name, at)),\n },\n storage: new ReduceCheckpointTable(this.ctx.storage.sql),\n fedByPushes: this.ctx.props.fedByPushes === true,\n }));\n }\n\n /** ONE round trip on the itx scope, then RELEASE EVERYTHING IT REACHED: the get, and every call the\n * callback made through it — not only the last. A Workers-RPC value this facet leaves undisposed —\n * the `itx.cd(path)` of `itx.cd(path).append(…)`, the `cfArtifacts.get(p)` of `.remote()`, an\n * answer awaited inside the callback (`const { state } = await context.invoke(…)`), data included —\n * keeps THIS FACET running after its context is evicted, until V8 collects the value, which an\n * idle isolate may not do for many minutes: each new incarnation of the context reattaches to the\n * facet, and the object stays billed (measured 2026-09-23: a new website project's `/` and\n * `/repos/config` billed 60 s of every minute for 30 min with no request). The context's own\n * `invoke` cannot end this from its side: the facet holds the value (context-residency.e2e.test.ts,\n * \"… does not outlive …\"). Protected: a host with methods of its own (the workspace,\n * src/workspace/durable-object.ts) reaches its context the same way. */\n protected withItx<T>(call: (itx: Scope) => T): Promise<Awaited<T>> {\n return withItx(this.#itxEntrypoint(), call);\n }\n}\n\n// ConfigWorker is a stateless event handler loaded with an explicit workers.get spec.\n// Subscribe its processEventBatch method explicitly; fetch routing is configured separately.\nexport type ConfigEventArgs = { event: StreamEvent; range: ScannedRange; itx: ItxScope };\n\nexport abstract class ConfigWorker<\n Env extends { ITX: ItxEntrypointService } = { ITX: ItxEntrypointService },\n> extends WorkerEntrypoint<Env> {\n /** At fetch entry: `const denied = this.auth.require(request); if (denied) return denied;`\n * `x-itx-principal` is on a request only when a project member (or the operator) sent it, safe\n * to act on. A private route written by hand answers the platform's sign-in challenge, which\n * the edge turns into the sign-in for a page load (`auth.require` does the same):\n *\n * ```js\n * if (!request.headers.get(\"x-itx-principal\"))\n * return new Response(\"Sign in\\n\", { status: 401, headers: { \"WWW-Authenticate\": 'Bearer realm=\"iterate\"' } });\n * ``` */\n protected readonly auth = auth;\n /** Process an explicitly subscribed batch with this worker's context scope. */\n async processEventBatch(events: StreamEvent[], range: ScannedRange): Promise<void> {\n await this.withItx(async (itx) => {\n for (const event of events) {\n await this.processEvent({ event, range, itx });\n }\n });\n }\n\n /** ONE round trip on the itx scope, then release the scope and every call made through it\n * (`StreamProcessorDurableObject.withItx` says why an undisposed step keeps a context billed). */\n protected withItx<T>(call: (itx: ItxScope) => T): Promise<Awaited<T>> {\n return withItx(this.env.ITX, call);\n }\n\n /** THE AUTHOR HOOK — one event at a time, in offset order. Append reactions through the itx scope;\n * make them idempotent (a redelivery must be a no-op). Default: ignore the event. */\n processEvent(_args: ConfigEventArgs): void | Promise<void> {}\n\n /** THE WEB ROOT — every Request on a host of the project (the project's configured ingress\n * target). The host's routing slug is in `x-iterate-routing-slug` (`notes` for\n * `notes--<project>.<hostname>`; absent on the apex), written only by the platform: route on it\n * in plain code, answering here (reaching the context through `this.withItx`) or forwarding the\n * Request. Default: not found. */\n override fetch(_request: Request): Response | Promise<Response> {\n return new Response(\"Not found\\n\", { status: 404 });\n }\n}\n\nexport { RunContract, RunRequested, RunSettled } from \"../stream/run.ts\";\n"],"mappings":"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"}