iterate 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/README.md +164 -82
  2. package/dist/{next/api.d.ts → api.d.ts} +197 -33
  3. package/dist/{next/app-server.d.ts → app-server.d.ts} +7 -0
  4. package/dist/{next/app-server.mjs → app-server.mjs} +17 -15
  5. package/dist/app-server.mjs.map +1 -0
  6. package/dist/{next/app-session.mjs → app-session.mjs} +12 -15
  7. package/dist/app-session.mjs.map +1 -0
  8. package/dist/{next/app.mjs → app.mjs} +53 -14
  9. package/dist/app.mjs.map +1 -0
  10. package/dist/{next/client → client}/live-state.d.ts +11 -11
  11. package/dist/client/oauth.d.ts +17 -0
  12. package/dist/{next/client → client}/react.d.ts +10 -42
  13. package/dist/{next/client → client}/socket.d.ts +1 -0
  14. package/dist/client.mjs +156 -4
  15. package/dist/client.mjs.map +1 -0
  16. package/dist/{next/expression.d.ts → expression.d.ts} +11 -69
  17. package/dist/{next/expression.mjs → expression.mjs} +11 -109
  18. package/dist/expression.mjs.map +1 -0
  19. package/dist/lib-BWr-5mFO.mjs +36 -0
  20. package/dist/lib-BWr-5mFO.mjs.map +1 -0
  21. package/dist/{next/lib.d.ts → lib.d.ts} +16 -2
  22. package/dist/{next/lib.mjs → lib.mjs} +32 -3
  23. package/dist/lib.mjs.map +1 -0
  24. package/dist/node.d.ts +15 -3
  25. package/dist/node.mjs +36 -174
  26. package/dist/node.mjs.map +1 -1
  27. package/dist/{next/oauth-scopes.mjs → oauth-scopes.mjs} +1 -1
  28. package/dist/oauth-scopes.mjs.map +1 -0
  29. package/dist/{next/oauth.mjs → oauth.mjs} +14 -2
  30. package/dist/oauth.mjs.map +1 -0
  31. package/dist/principal.d.ts +8 -0
  32. package/dist/principal.mjs +8 -0
  33. package/dist/principal.mjs.map +1 -0
  34. package/dist/project-ingress.d.ts +58 -0
  35. package/dist/project-ingress.mjs +104 -0
  36. package/dist/project-ingress.mjs.map +1 -0
  37. package/dist/{next/react.mjs → react.mjs} +12 -12
  38. package/dist/react.mjs.map +1 -0
  39. package/dist/sdk/auth.d.ts +25 -0
  40. package/dist/sdk/index.d.ts +155 -0
  41. package/dist/sdk/record-pipelined-steps.d.ts +19 -0
  42. package/dist/sdk.mjs +245 -2
  43. package/dist/sdk.mjs.map +1 -0
  44. package/dist/{next/stream → stream}/processor.d.ts +28 -23
  45. package/dist/{next/stream → stream}/processor.mjs +48 -25
  46. package/dist/stream/processor.mjs.map +1 -0
  47. package/dist/{next/stream → stream}/run.d.ts +9 -6
  48. package/dist/{next/stream → stream}/run.mjs +13 -8
  49. package/dist/stream/run.mjs.map +1 -0
  50. package/dist/stream/test-support.d.ts +45 -0
  51. package/dist/stream/test-support.mjs +196 -0
  52. package/dist/stream/test-support.mjs.map +1 -0
  53. package/package.json +65 -219
  54. package/THIRD_PARTY_NOTICES.md +0 -55
  55. package/bin/iterate.js +0 -94
  56. package/dist/api-url-B6404M82.mjs +0 -17
  57. package/dist/api-url-B6404M82.mjs.map +0 -1
  58. package/dist/app-ref-BipL0feU.mjs +0 -35
  59. package/dist/app-ref-BipL0feU.mjs.map +0 -1
  60. package/dist/app-ref-C1CrgXqX.mjs +0 -7
  61. package/dist/app-ref-C1CrgXqX.mjs.map +0 -1
  62. package/dist/app-ref-DYai_om1.mjs +0 -7
  63. package/dist/app-ref-DYai_om1.mjs.map +0 -1
  64. package/dist/cli-D0c-pDL_.mjs +0 -1010
  65. package/dist/cli-D0c-pDL_.mjs.map +0 -1
  66. package/dist/client.d.ts +0 -3
  67. package/dist/cloudflare-BTm90gQ4.mjs +0 -951
  68. package/dist/cloudflare-BTm90gQ4.mjs.map +0 -1
  69. package/dist/contract-s4FW4eES.mjs +0 -309
  70. package/dist/contract-s4FW4eES.mjs.map +0 -1
  71. package/dist/document-review/index.d.ts +0 -5
  72. package/dist/document-review/types.d.ts +0 -107
  73. package/dist/document-review.mjs +0 -7015
  74. package/dist/document-review.mjs.map +0 -1
  75. package/dist/durable-object-processor-durability-CNsTjAJS.mjs +0 -205
  76. package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +0 -1
  77. package/dist/idempotency-DleloJNt.mjs +0 -28
  78. package/dist/idempotency-DleloJNt.mjs.map +0 -1
  79. package/dist/index.d.mts +0 -5
  80. package/dist/index.mjs +0 -8
  81. package/dist/index.mjs.map +0 -1
  82. package/dist/itx/api-url.d.ts +0 -6
  83. package/dist/itx/itx-node-client.d.ts +0 -65
  84. package/dist/itx/itx-session.d.ts +0 -215
  85. package/dist/itx/owned-rpc-session.d.ts +0 -14
  86. package/dist/itx/query-client.d.ts +0 -10
  87. package/dist/itx-api.generated.d.ts +0 -6195
  88. package/dist/itx-session-sjud8GiT.mjs +0 -534
  89. package/dist/itx-session-sjud8GiT.mjs.map +0 -1
  90. package/dist/live-state-BJNqOwFw.mjs +0 -299
  91. package/dist/live-state-BJNqOwFw.mjs.map +0 -1
  92. package/dist/next/app-server.mjs.map +0 -1
  93. package/dist/next/app-session.mjs.map +0 -1
  94. package/dist/next/app.mjs.map +0 -1
  95. package/dist/next/client/oauth.d.ts +0 -12
  96. package/dist/next/client.mjs +0 -156
  97. package/dist/next/client.mjs.map +0 -1
  98. package/dist/next/expression.mjs.map +0 -1
  99. package/dist/next/lib.mjs.map +0 -1
  100. package/dist/next/oauth-scopes.mjs.map +0 -1
  101. package/dist/next/oauth.mjs.map +0 -1
  102. package/dist/next/principal.d.ts +0 -64
  103. package/dist/next/principal.mjs +0 -98
  104. package/dist/next/principal.mjs.map +0 -1
  105. package/dist/next/project-ingress.d.ts +0 -37
  106. package/dist/next/project-ingress.mjs +0 -75
  107. package/dist/next/project-ingress.mjs.map +0 -1
  108. package/dist/next/react.mjs.map +0 -1
  109. package/dist/next/sdk/auth.d.ts +0 -5
  110. package/dist/next/sdk/index.d.ts +0 -112
  111. package/dist/next/sdk.mjs +0 -139
  112. package/dist/next/sdk.mjs.map +0 -1
  113. package/dist/next/stream/processor.mjs.map +0 -1
  114. package/dist/next/stream/run.mjs.map +0 -1
  115. package/dist/next-node.d.ts +0 -15
  116. package/dist/next-node.mjs +0 -51
  117. package/dist/next-node.mjs.map +0 -1
  118. package/dist/processor-host-capabilities-BMFH3KTM.mjs +0 -56
  119. package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +0 -1
  120. package/dist/processors/cloudflare.d.ts +0 -3
  121. package/dist/processors/durable-object-processor-durability.d.ts +0 -79
  122. package/dist/processors/event-consumption-metrics.d.ts +0 -82
  123. package/dist/processors/idempotency.d.ts +0 -13
  124. package/dist/processors/index.d.ts +0 -12
  125. package/dist/processors/processor-contracts.d.ts +0 -342
  126. package/dist/processors/processor-facet.d.ts +0 -186
  127. package/dist/processors/processor-host-capabilities.d.ts +0 -60
  128. package/dist/processors/prompt-sections.d.ts +0 -17
  129. package/dist/processors/rpc-types.d.ts +0 -515
  130. package/dist/processors/schemas.d.ts +0 -102
  131. package/dist/processors/stream-handle.d.ts +0 -45
  132. package/dist/processors/stream-processor-keepalive.d.ts +0 -95
  133. package/dist/processors/stream-processor-registry.d.ts +0 -233
  134. package/dist/processors/stream-processor-runner.d.ts +0 -289
  135. package/dist/processors/stream-processor.d.ts +0 -339
  136. package/dist/processors/stream-runtime-metrics.d.ts +0 -107
  137. package/dist/processors/testing.d.ts +0 -302
  138. package/dist/processors-BoNyeBfQ.mjs +0 -10
  139. package/dist/processors-BoNyeBfQ.mjs.map +0 -1
  140. package/dist/processors-cloudflare.mjs +0 -3
  141. package/dist/processors-testing.mjs +0 -435
  142. package/dist/processors-testing.mjs.map +0 -1
  143. package/dist/processors.mjs +0 -52
  144. package/dist/processors.mjs.map +0 -1
  145. package/dist/protocol-DnK_f2m6.mjs +0 -251
  146. package/dist/protocol-DnK_f2m6.mjs.map +0 -1
  147. package/dist/sdk/capnweb/index.d.ts +0 -2
  148. package/dist/sdk/capnweb/live-state/compact.d.ts +0 -5
  149. package/dist/sdk/capnweb/live-state/diff.d.ts +0 -41
  150. package/dist/sdk/capnweb/live-state/engine.d.ts +0 -44
  151. package/dist/sdk/capnweb/live-state/index.d.ts +0 -41
  152. package/dist/sdk/capnweb/live-state/protocol.d.ts +0 -87
  153. package/dist/sdk/capnweb/live-state/retain.d.ts +0 -23
  154. package/dist/sdk/capnweb/live-state/store.d.ts +0 -20
  155. package/dist/sdk/capnweb/live-state/types.d.ts +0 -11
  156. package/dist/sdk/capnweb/react.d.ts +0 -45
  157. package/dist/sdk/capnweb/react.mjs +0 -316
  158. package/dist/sdk/capnweb/react.mjs.map +0 -1
  159. package/dist/sdk/capnweb.mjs +0 -4
  160. package/dist/sdk/itx/react.d.ts +0 -191
  161. package/dist/sdk/itx/react.mjs +0 -383
  162. package/dist/sdk/itx/react.mjs.map +0 -1
  163. package/dist/sdk-DMB-IM11.mjs +0 -933
  164. package/dist/sdk-DMB-IM11.mjs.map +0 -1
  165. package/dist/sdk.d.ts +0 -339
  166. package/dist/serve-itx.d.ts +0 -46
  167. package/dist/starter-apps/flake-dashboard/app-ref.d.ts +0 -31
  168. package/dist/starter-apps/flake-dashboard/configured-worker.mjs +0 -1055
  169. package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +0 -1
  170. package/dist/starter-apps/flake-dashboard/contract.d.ts +0 -4839
  171. package/dist/starter-apps/flake-dashboard/contract.mjs +0 -2
  172. package/dist/starter-apps/flake-dashboard/index.d.ts +0 -17
  173. package/dist/starter-apps/flake-dashboard/index.mjs +0 -56
  174. package/dist/starter-apps/flake-dashboard/index.mjs.map +0 -1
  175. package/dist/starter-apps/flake-dashboard/worker.d.ts +0 -4607
  176. package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +0 -8914
  177. package/dist/starter-apps/github-ai-linter/configured-worker.mjs +0 -17987
  178. package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +0 -1
  179. package/dist/starter-apps/github-ai-linter/contract.d.ts +0 -9193
  180. package/dist/starter-apps/github-ai-linter/index.d.ts +0 -10
  181. package/dist/starter-apps/github-ai-linter/index.mjs +0 -36
  182. package/dist/starter-apps/github-ai-linter/index.mjs.map +0 -1
  183. package/dist/starter-apps/github-ai-linter/prompt.d.ts +0 -13
  184. package/dist/starter-apps/github-ai-linter/review-bot.d.ts +0 -808
  185. package/dist/starter-apps/github-ai-linter/rules.d.ts +0 -34
  186. package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +0 -19
  187. package/dist/starter-apps/github-ai-linter/worker.d.ts +0 -19
  188. package/dist/starter-apps/github-ai-linter/worker.mjs +0 -947
  189. package/dist/starter-apps/github-ai-linter/worker.mjs.map +0 -1
  190. package/dist/starter-apps/guestbook/app-ref.d.ts +0 -27
  191. package/dist/starter-apps/guestbook/client.d.ts +0 -7
  192. package/dist/starter-apps/guestbook/client.mjs +0 -59
  193. package/dist/starter-apps/guestbook/configured-worker.mjs +0 -205
  194. package/dist/starter-apps/guestbook/configured-worker.mjs.map +0 -1
  195. package/dist/starter-apps/guestbook/index.d.ts +0 -9
  196. package/dist/starter-apps/guestbook/index.mjs +0 -31
  197. package/dist/starter-apps/guestbook/index.mjs.map +0 -1
  198. package/dist/starter-apps/guestbook/processor.d.ts +0 -2267
  199. package/dist/starter-apps/guestbook/worker.d.ts +0 -26
  200. package/dist/starter-apps/guestbook/worker.mjs +0 -191
  201. package/dist/starter-apps/guestbook/worker.mjs.map +0 -1
  202. package/dist/starter-apps/media/configured-worker.mjs +0 -577
  203. package/dist/starter-apps/media/configured-worker.mjs.map +0 -1
  204. package/dist/starter-apps/media/index.mjs +0 -36
  205. package/dist/starter-apps/media/index.mjs.map +0 -1
  206. package/dist/starter-apps/media/ref.mjs +0 -20
  207. package/dist/starter-apps/media/ref.mjs.map +0 -1
  208. package/dist/starter-apps/media/worker.mjs +0 -579
  209. package/dist/starter-apps/media/worker.mjs.map +0 -1
  210. package/dist/starter-apps/notes/configured-worker.mjs +0 -6134
  211. package/dist/starter-apps/notes/configured-worker.mjs.map +0 -1
  212. package/dist/starter-apps/notes/index.mjs +0 -23
  213. package/dist/starter-apps/notes/index.mjs.map +0 -1
  214. package/dist/starter-apps/notes/ref.mjs +0 -21
  215. package/dist/starter-apps/notes/ref.mjs.map +0 -1
  216. package/dist/starter-apps/notes/worker.mjs +0 -427
  217. package/dist/starter-apps/notes/worker.mjs.map +0 -1
  218. package/dist/starter-apps/todo/client.mjs +0 -59
  219. package/dist/starter-apps/todo/configured-worker.mjs +0 -2864
  220. package/dist/starter-apps/todo/configured-worker.mjs.map +0 -1
  221. package/dist/starter-apps/todo/index.d.ts +0 -8
  222. package/dist/starter-apps/todo/index.mjs +0 -29
  223. package/dist/starter-apps/todo/index.mjs.map +0 -1
  224. package/dist/stream-processor-keepalive-DAQTP6m3.mjs +0 -2082
  225. package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +0 -1
  226. package/dist/usingCtx-mZx5nsAW.mjs +0 -11800
  227. package/dist/usingCtx-mZx5nsAW.mjs.map +0 -1
  228. package/dist/worker-ref-DZxPDmb_.mjs +0 -390
  229. package/dist/worker-ref-DZxPDmb_.mjs.map +0 -1
  230. package/dist/worker.d.mts +0 -33
  231. package/dist/worker.mjs +0 -18
  232. package/dist/worker.mjs.map +0 -1
  233. package/menubar/Iterate.entitlements +0 -12
  234. package/menubar/Iterate.swift +0 -914
  235. package/menubar/IterateIcon.swift +0 -145
  236. package/menubar/README.md +0 -28
  237. package/menubar/build-menubar-app.sh +0 -59
  238. /package/dist/{next/api.mjs → api.mjs} +0 -0
  239. /package/dist/{next/app-session.d.ts → app-session.d.ts} +0 -0
  240. /package/dist/{next/app.d.ts → app.d.ts} +0 -0
  241. /package/dist/{next/oauth-scopes.d.ts → oauth-scopes.d.ts} +0 -0
@@ -0,0 +1,58 @@
1
+ /** THE ROUTING SLUG a project host named (`notes` for `notes--<project>.<hostname>`), as the project's
2
+ * config worker `fetch` reads it; absent on the apex. Written only by the platform: the edge sets or
3
+ * deletes it on every project-host request, and the context DO deletes it from every other
4
+ * expression fetch, so neither a visitor nor loaded code can pick a routing slug. */
5
+ export declare const ITERATE_ROUTING_SLUG_HEADER = "x-iterate-routing-slug";
6
+ /** How projects are reached over HTTP; null ⇒ no ingress (`/api` and `/mcp` still answer). */
7
+ export type IngressRouting = {
8
+ type: "subdomains";
9
+ hostname: string;
10
+ } | {
11
+ type: "paths";
12
+ } | null;
13
+ /** What a request names: the project (its slug, as written — whether it EXISTS is the directory's
14
+ * answer), the routing slug (null ⇒ the apex; either way the project's config worker answers), and
15
+ * the path prefix the edge strips before the config worker sees the URL ("" under subdomains;
16
+ * "/projects/<project>" or "/projects/<project>/<routingSlug>" under paths). */
17
+ export type ProjectAddress = {
18
+ project: string;
19
+ routingSlug: string | null;
20
+ basePath: string;
21
+ };
22
+ /** The project + routing slug `url` names under `routing`, or null when it names none. Pure. */
23
+ export declare function projectAddressOf(routing: IngressRouting, url: URL, platformOrigin: string): ProjectAddress | null;
24
+ /** A PROJECT WILDCARD — an owned zone served as one project's apex (apps/os
25
+ * `urls.projectWildcard`, `{ hostname: "iterate.com", project: "iterate" }`): the zone's apex and
26
+ * every first-level name under it but the excluded ones, in the apex shape, `routingSlug: null`, so the
27
+ * project's config worker `fetch` answers exactly as it does on `<project>.<hostname>`. Null for
28
+ * anything else. Case and a trailing dot are forgiven. Pure. */
29
+ export declare function projectWildcardHostOf(hostname: string, wildcard: {
30
+ hostname: string;
31
+ project: string;
32
+ excludedHostnames?: string[];
33
+ } | undefined): {
34
+ routingSlug: null;
35
+ project: string;
36
+ } | null;
37
+ /** A PROJECT'S OWN HOSTNAME — `iterate.example.com`, added by the project (apps/os
38
+ * project/custom-hostnames.ts) — is that project's apex, and one label under it names a routing
39
+ * slug: `notes.iterate.example.com` carries `notes`, as `notes--<project>.<hostname>` does. The
40
+ * hostnames a request's host could be a project's own hostname for, most specific first: the host
41
+ * itself (the apex), then its parent with the first label as the routing slug. The caller
42
+ * looks them up in that order; the first a project holds wins. Case and a trailing dot are
43
+ * forgiven. Pure. */
44
+ export declare function customHostnameCandidatesOf(host: string): {
45
+ hostname: string;
46
+ routingSlug: string | null;
47
+ }[];
48
+ /** The URL of `routingSlug` (null ⇒ the apex) in `project` under `routing`, at `path` (default "/",
49
+ * must start with "/"). Null when there is no ingress, or when the result would not parse back to
50
+ * the same address (a bad slug; a `path` that climbs out of its routing slug). subdomains: the
51
+ * protocol and port are `platformOrigin`'s (local dev is `http://localhost:8788`, so
52
+ * `http://<routingSlug>--<project>.localhost:8788/…`); paths:
53
+ * `<platformOrigin>/projects/<project>[/<routingSlug>]<path>`. Pure. */
54
+ export declare function projectUrlOf(routing: IngressRouting, platformOrigin: string, target: {
55
+ project: string;
56
+ routingSlug?: string | null;
57
+ path?: string;
58
+ }): URL | null;
@@ -0,0 +1,104 @@
1
+ //#region src/project-ingress.ts
2
+ /** THE ROUTING SLUG a project host named (`notes` for `notes--<project>.<hostname>`), as the project's
3
+ * config worker `fetch` reads it; absent on the apex. Written only by the platform: the edge sets or
4
+ * deletes it on every project-host request, and the context DO deletes it from every other
5
+ * expression fetch, so neither a visitor nor loaded code can pick a routing slug. */
6
+ const ITERATE_ROUTING_SLUG_HEADER = "x-iterate-routing-slug";
7
+ /** A DNS label: lowercase letters and digits, single hyphens inside. */
8
+ const DNS_LABEL = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9
+ /** A routing slug: a DNS label starting with a letter. */
10
+ const ROUTING_SLUG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
11
+ /** The labels `host` has under `hostname` — `site--p.iterate.app` ⇒ `["site--p"]` — lowercased, a
12
+ * trailing dot (a fully-qualified Host, `site--p.base.`) dropped; null when `host` is not under
13
+ * `hostname` at all. */
14
+ function labelsUnder(host, hostname) {
15
+ const name = host.toLowerCase().replace(/\.$/, "");
16
+ const suffix = `.${hostname.toLowerCase()}`;
17
+ return name.endsWith(suffix) ? name.slice(0, -suffix.length).split(".") : null;
18
+ }
19
+ /** The project + routing slug `url` names under `routing`, or null when it names none. Pure. */
20
+ function projectAddressOf(routing, url, platformOrigin) {
21
+ if (!routing) return null;
22
+ if (routing.type === "subdomains") {
23
+ const labels = labelsUnder(url.hostname, routing.hostname);
24
+ if (!labels || labels.length > 2) return null;
25
+ const [first, second] = labels;
26
+ const separator = first.startsWith("xn--") ? -1 : first.indexOf("--");
27
+ const [routingSlug, project] = second !== void 0 ? [first, second] : separator === -1 ? [null, first] : [first.slice(0, separator), first.slice(separator + 2)];
28
+ if (!DNS_LABEL.test(project) || routingSlug !== null && !ROUTING_SLUG.test(routingSlug)) return null;
29
+ return {
30
+ routingSlug,
31
+ project,
32
+ basePath: ""
33
+ };
34
+ }
35
+ if (url.origin !== new URL(platformOrigin).origin) return null;
36
+ const [, prefix, project = "", routingSlug] = url.pathname.split("/");
37
+ if (prefix !== "projects" || !DNS_LABEL.test(project)) return null;
38
+ if (routingSlug === void 0 || routingSlug === "") return {
39
+ routingSlug: null,
40
+ project,
41
+ basePath: `/projects/${project}`
42
+ };
43
+ if (!ROUTING_SLUG.test(routingSlug)) return null;
44
+ return {
45
+ routingSlug,
46
+ project,
47
+ basePath: `/projects/${project}/${routingSlug}`
48
+ };
49
+ }
50
+ /** A PROJECT WILDCARD — an owned zone served as one project's apex (apps/os
51
+ * `urls.projectWildcard`, `{ hostname: "iterate.com", project: "iterate" }`): the zone's apex and
52
+ * every first-level name under it but the excluded ones, in the apex shape, `routingSlug: null`, so the
53
+ * project's config worker `fetch` answers exactly as it does on `<project>.<hostname>`. Null for
54
+ * anything else. Case and a trailing dot are forgiven. Pure. */
55
+ function projectWildcardHostOf(hostname, wildcard) {
56
+ if (!wildcard) return null;
57
+ const normalized = hostname.toLowerCase().replace(/\.$/, "");
58
+ if (wildcard.excludedHostnames?.includes(normalized)) return null;
59
+ const suffix = `.${wildcard.hostname}`;
60
+ const label = normalized.endsWith(suffix) ? normalized.slice(0, -suffix.length) : null;
61
+ return normalized === wildcard.hostname || label && !label.includes(".") ? {
62
+ routingSlug: null,
63
+ project: wildcard.project
64
+ } : null;
65
+ }
66
+ /** A PROJECT'S OWN HOSTNAME — `iterate.example.com`, added by the project (apps/os
67
+ * project/custom-hostnames.ts) — is that project's apex, and one label under it names a routing
68
+ * slug: `notes.iterate.example.com` carries `notes`, as `notes--<project>.<hostname>` does. The
69
+ * hostnames a request's host could be a project's own hostname for, most specific first: the host
70
+ * itself (the apex), then its parent with the first label as the routing slug. The caller
71
+ * looks them up in that order; the first a project holds wins. Case and a trailing dot are
72
+ * forgiven. Pure. */
73
+ function customHostnameCandidatesOf(host) {
74
+ const hostname = host.toLowerCase().replace(/\.$/, "");
75
+ const dot = hostname.indexOf(".");
76
+ const [routingSlug, parent] = [hostname.slice(0, dot), hostname.slice(dot + 1)];
77
+ return [{
78
+ hostname,
79
+ routingSlug: null
80
+ }, ...dot > 0 && parent.includes(".") && ROUTING_SLUG.test(routingSlug) ? [{
81
+ hostname: parent,
82
+ routingSlug
83
+ }] : []];
84
+ }
85
+ /** The URL of `routingSlug` (null ⇒ the apex) in `project` under `routing`, at `path` (default "/",
86
+ * must start with "/"). Null when there is no ingress, or when the result would not parse back to
87
+ * the same address (a bad slug; a `path` that climbs out of its routing slug). subdomains: the
88
+ * protocol and port are `platformOrigin`'s (local dev is `http://localhost:8788`, so
89
+ * `http://<routingSlug>--<project>.localhost:8788/…`); paths:
90
+ * `<platformOrigin>/projects/<project>[/<routingSlug>]<path>`. Pure. */
91
+ function projectUrlOf(routing, platformOrigin, target) {
92
+ if (!routing) return null;
93
+ const path = target.path || "/";
94
+ if (!path.startsWith("/")) throw new Error(`projectUrlOf: path must start with "/": ${path}`);
95
+ const routingSlug = target.routingSlug || null;
96
+ const origin = new URL(platformOrigin);
97
+ const url = routing.type === "subdomains" ? new URL(path, `${origin.protocol}//${routingSlug ? `${routingSlug}--` : ""}${target.project}.${routing.hostname}${origin.port ? `:${origin.port}` : ""}`) : new URL(`/projects/${target.project}${routingSlug ? `/${routingSlug}` : ""}${path}`, origin.origin);
98
+ const parsed = projectAddressOf(routing, url, platformOrigin);
99
+ return parsed && parsed.project === target.project && parsed.routingSlug === routingSlug ? url : null;
100
+ }
101
+ //#endregion
102
+ export { ITERATE_ROUTING_SLUG_HEADER, customHostnameCandidatesOf, projectAddressOf, projectUrlOf, projectWildcardHostOf };
103
+
104
+ //# sourceMappingURL=project-ingress.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-ingress.mjs","names":[],"sources":["../src/project-ingress.ts"],"sourcesContent":["// project-ingress.ts — HOW PROJECTS ARE REACHED OVER HTTP, both directions in ONE pure file. A\n// deployment's `urls.ingressRouting` (apps/os app-config.ts) names the mechanism; `projectAddressOf`\n// parses a request's URL into the project and routing slug it names, `projectUrlOf` composes the URL\n// of a routing slug in a project. The platform's edge parses; the platform, the dash and an app\n// compose — one implementation, table-tested to round-trip (project-ingress.test.ts). No imports.\n//\n// The edge picks the PROJECT only: every host of a project reaches the project's config worker\n// `fetch`, with the routing slug in `x-iterate-routing-slug` (absent on the apex), and the config\n// worker routes on it in plain code.\n//\n// subdomains `<routingSlug>--<project>.<hostname>`, `<routingSlug>.<project>.<hostname>`, the apex\n// `<project>.<hostname>` — every routing slug its own origin, under one wildcard on `hostname`.\n// paths `<platformOrigin>/projects/<project>/<routingSlug>/…`, the apex `<platformOrigin>/projects/<project>/`\n// — one origin (workers.dev has no wildcard), every project under `/projects/` so the\n// platform's own paths (`/api`, `/mcp`, `/login`, …) need no reserved list; the edge\n// sandboxes what a project serves.\n\n/** THE ROUTING SLUG a project host named (`notes` for `notes--<project>.<hostname>`), as the project's\n * config worker `fetch` reads it; absent on the apex. Written only by the platform: the edge sets or\n * deletes it on every project-host request, and the context DO deletes it from every other\n * expression fetch, so neither a visitor nor loaded code can pick a routing slug. */\nexport const ITERATE_ROUTING_SLUG_HEADER = \"x-iterate-routing-slug\";\n\n/** How projects are reached over HTTP; null ⇒ no ingress (`/api` and `/mcp` still answer). */\nexport type IngressRouting = { type: \"subdomains\"; hostname: string } | { type: \"paths\" } | null;\n\n/** What a request names: the project (its slug, as written — whether it EXISTS is the directory's\n * answer), the routing slug (null ⇒ the apex; either way the project's config worker answers), and\n * the path prefix the edge strips before the config worker sees the URL (\"\" under subdomains;\n * \"/projects/<project>\" or \"/projects/<project>/<routingSlug>\" under paths). */\nexport type ProjectAddress = { project: string; routingSlug: string | null; basePath: string };\n\n/** A DNS label: lowercase letters and digits, single hyphens inside. */\nconst DNS_LABEL = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n/** A routing slug: a DNS label starting with a letter. */\nconst ROUTING_SLUG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;\n\n/** The labels `host` has under `hostname` — `site--p.iterate.app` ⇒ `[\"site--p\"]` — lowercased, a\n * trailing dot (a fully-qualified Host, `site--p.base.`) dropped; null when `host` is not under\n * `hostname` at all. */\nfunction labelsUnder(host: string, hostname: string): string[] | null {\n const name = host.toLowerCase().replace(/\\.$/, \"\");\n const suffix = `.${hostname.toLowerCase()}`;\n return name.endsWith(suffix) ? name.slice(0, -suffix.length).split(\".\") : null;\n}\n\n/** The project + routing slug `url` names under `routing`, or null when it names none. Pure. */\nexport function projectAddressOf(\n routing: IngressRouting,\n url: URL,\n platformOrigin: string,\n): ProjectAddress | null {\n if (!routing) return null;\n if (routing.type === \"subdomains\") {\n const labels = labelsUnder(url.hostname, routing.hostname);\n if (!labels || labels.length > 2) return null; // deeper than `<routingSlug>.<project>` is not a project host\n const [first, second] = labels as [string, string?];\n const separator = first.startsWith(\"xn--\") ? -1 : first.indexOf(\"--\"); // `xn--…` is an IDN label (punycode), never `<routingSlug>--<project>`\n const [routingSlug, project] =\n // oxlint-disable-next-line iterate/simple-truthiness-check -- a PRESENT-but-empty second label (`<routingSlug>..<base>`) is the `<routingSlug>.<project>` shape (rejected below by DNS_LABEL), not the single-label `<project>` shape a truthiness check would route it to\n second !== undefined\n ? [first, second] // `<routingSlug>.<project>`\n : separator === -1\n ? [null, first] // the apex, `<project>`\n : [first.slice(0, separator), first.slice(separator + 2)]; // `<routingSlug>--<project>`\n // oxlint-disable-next-line iterate/simple-truthiness-check -- an empty routing slug (`--<project>.<base>`) must still be rejected by ROUTING_SLUG; truthiness would skip the check and admit it\n if (!DNS_LABEL.test(project) || (routingSlug !== null && !ROUTING_SLUG.test(routingSlug)))\n return null;\n return { routingSlug, project, basePath: \"\" };\n }\n if (url.origin !== new URL(platformOrigin).origin) return null;\n const [, prefix, project = \"\", routingSlug] = url.pathname.split(\"/\");\n if (prefix !== \"projects\" || !DNS_LABEL.test(project)) return null;\n // oxlint-disable-next-line iterate/simple-truthiness-check -- `/projects/<project>` and `/projects/<project>/` are both the apex; a present-but-empty next segment is not a routing slug\n if (routingSlug === undefined || routingSlug === \"\")\n return { routingSlug: null, project, basePath: `/projects/${project}` };\n if (!ROUTING_SLUG.test(routingSlug)) return null;\n return { routingSlug, project, basePath: `/projects/${project}/${routingSlug}` };\n}\n\n/** A PROJECT WILDCARD — an owned zone served as one project's apex (apps/os\n * `urls.projectWildcard`, `{ hostname: \"iterate.com\", project: \"iterate\" }`): the zone's apex and\n * every first-level name under it but the excluded ones, in the apex shape, `routingSlug: null`, so the\n * project's config worker `fetch` answers exactly as it does on `<project>.<hostname>`. Null for\n * anything else. Case and a trailing dot are forgiven. Pure. */\nexport function projectWildcardHostOf(\n hostname: string,\n wildcard: { hostname: string; project: string; excludedHostnames?: string[] } | undefined,\n): { routingSlug: null; project: string } | null {\n if (!wildcard) return null;\n const normalized = hostname.toLowerCase().replace(/\\.$/, \"\");\n if (wildcard.excludedHostnames?.includes(normalized)) return null;\n const suffix = `.${wildcard.hostname}`;\n const label = normalized.endsWith(suffix) ? normalized.slice(0, -suffix.length) : null;\n return normalized === wildcard.hostname || (label && !label.includes(\".\"))\n ? { routingSlug: null, project: wildcard.project }\n : null;\n}\n\n/** A PROJECT'S OWN HOSTNAME — `iterate.example.com`, added by the project (apps/os\n * project/custom-hostnames.ts) — is that project's apex, and one label under it names a routing\n * slug: `notes.iterate.example.com` carries `notes`, as `notes--<project>.<hostname>` does. The\n * hostnames a request's host could be a project's own hostname for, most specific first: the host\n * itself (the apex), then its parent with the first label as the routing slug. The caller\n * looks them up in that order; the first a project holds wins. Case and a trailing dot are\n * forgiven. Pure. */\nexport function customHostnameCandidatesOf(\n host: string,\n): { hostname: string; routingSlug: string | null }[] {\n const hostname = host.toLowerCase().replace(/\\.$/, \"\");\n const dot = hostname.indexOf(\".\");\n const [routingSlug, parent] = [hostname.slice(0, dot), hostname.slice(dot + 1)];\n return [\n { hostname, routingSlug: null },\n ...(dot > 0 && parent.includes(\".\") && ROUTING_SLUG.test(routingSlug)\n ? [{ hostname: parent, routingSlug }]\n : []),\n ];\n}\n\n/** The URL of `routingSlug` (null ⇒ the apex) in `project` under `routing`, at `path` (default \"/\",\n * must start with \"/\"). Null when there is no ingress, or when the result would not parse back to\n * the same address (a bad slug; a `path` that climbs out of its routing slug). subdomains: the\n * protocol and port are `platformOrigin`'s (local dev is `http://localhost:8788`, so\n * `http://<routingSlug>--<project>.localhost:8788/…`); paths:\n * `<platformOrigin>/projects/<project>[/<routingSlug>]<path>`. Pure. */\nexport function projectUrlOf(\n routing: IngressRouting,\n platformOrigin: string,\n target: { project: string; routingSlug?: string | null; path?: string },\n): URL | null {\n if (!routing) return null;\n const path = target.path || \"/\";\n if (!path.startsWith(\"/\")) throw new Error(`projectUrlOf: path must start with \"/\": ${path}`);\n const routingSlug = target.routingSlug || null;\n const origin = new URL(platformOrigin);\n const url =\n routing.type === \"subdomains\"\n ? new URL(\n path,\n `${origin.protocol}//${routingSlug ? `${routingSlug}--` : \"\"}${target.project}.${routing.hostname}${origin.port ? `:${origin.port}` : \"\"}`,\n )\n : new URL(\n `/projects/${target.project}${routingSlug ? `/${routingSlug}` : \"\"}${path}`,\n origin.origin,\n );\n const parsed = projectAddressOf(routing, url, platformOrigin);\n return parsed && parsed.project === target.project && parsed.routingSlug === routingSlug\n ? url\n : null;\n}\n"],"mappings":";;;;;AAqBA,MAAa,8BAA8B;;AAY3C,MAAM,YAAY;;AAElB,MAAM,eAAe;;;;AAKrB,SAAS,YAAY,MAAc,UAAmC;CACpE,MAAM,OAAO,KAAK,YAAY,CAAC,CAAC,QAAQ,OAAO,EAAE;CACjD,MAAM,SAAS,IAAI,SAAS,YAAY;CACxC,OAAO,KAAK,SAAS,MAAM,IAAI,KAAK,MAAM,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,GAAG,IAAI;AAC5E;;AAGA,SAAgB,iBACd,SACA,KACA,gBACuB;CACvB,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,QAAQ,SAAS,cAAc;EACjC,MAAM,SAAS,YAAY,IAAI,UAAU,QAAQ,QAAQ;EACzD,IAAI,CAAC,UAAU,OAAO,SAAS,GAAG,OAAO;EACzC,MAAM,CAAC,OAAO,UAAU;EACxB,MAAM,YAAY,MAAM,WAAW,MAAM,IAAI,KAAK,MAAM,QAAQ,IAAI;EACpE,MAAM,CAAC,aAAa,WAElB,WAAW,KAAA,IACP,CAAC,OAAO,MAAM,IACd,cAAc,KACZ,CAAC,MAAM,KAAK,IACZ,CAAC,MAAM,MAAM,GAAG,SAAS,GAAG,MAAM,MAAM,YAAY,CAAC,CAAC;EAE9D,IAAI,CAAC,UAAU,KAAK,OAAO,KAAM,gBAAgB,QAAQ,CAAC,aAAa,KAAK,WAAW,GACrF,OAAO;EACT,OAAO;GAAE;GAAa;GAAS,UAAU;EAAG;CAC9C;CACA,IAAI,IAAI,WAAW,IAAI,IAAI,cAAc,CAAC,CAAC,QAAQ,OAAO;CAC1D,MAAM,GAAG,QAAQ,UAAU,IAAI,eAAe,IAAI,SAAS,MAAM,GAAG;CACpE,IAAI,WAAW,cAAc,CAAC,UAAU,KAAK,OAAO,GAAG,OAAO;CAE9D,IAAI,gBAAgB,KAAA,KAAa,gBAAgB,IAC/C,OAAO;EAAE,aAAa;EAAM;EAAS,UAAU,aAAa;CAAU;CACxE,IAAI,CAAC,aAAa,KAAK,WAAW,GAAG,OAAO;CAC5C,OAAO;EAAE;EAAa;EAAS,UAAU,aAAa,QAAQ,GAAG;CAAc;AACjF;;;;;;AAOA,SAAgB,sBACd,UACA,UAC+C;CAC/C,IAAI,CAAC,UAAU,OAAO;CACtB,MAAM,aAAa,SAAS,YAAY,CAAC,CAAC,QAAQ,OAAO,EAAE;CAC3D,IAAI,SAAS,mBAAmB,SAAS,UAAU,GAAG,OAAO;CAC7D,MAAM,SAAS,IAAI,SAAS;CAC5B,MAAM,QAAQ,WAAW,SAAS,MAAM,IAAI,WAAW,MAAM,GAAG,CAAC,OAAO,MAAM,IAAI;CAClF,OAAO,eAAe,SAAS,YAAa,SAAS,CAAC,MAAM,SAAS,GAAG,IACpE;EAAE,aAAa;EAAM,SAAS,SAAS;CAAQ,IAC/C;AACN;;;;;;;;AASA,SAAgB,2BACd,MACoD;CACpD,MAAM,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ,OAAO,EAAE;CACrD,MAAM,MAAM,SAAS,QAAQ,GAAG;CAChC,MAAM,CAAC,aAAa,UAAU,CAAC,SAAS,MAAM,GAAG,GAAG,GAAG,SAAS,MAAM,MAAM,CAAC,CAAC;CAC9E,OAAO,CACL;EAAE;EAAU,aAAa;CAAK,GAC9B,GAAI,MAAM,KAAK,OAAO,SAAS,GAAG,KAAK,aAAa,KAAK,WAAW,IAChE,CAAC;EAAE,UAAU;EAAQ;CAAY,CAAC,IAClC,CAAC,CACP;AACF;;;;;;;AAQA,SAAgB,aACd,SACA,gBACA,QACY;CACZ,IAAI,CAAC,SAAS,OAAO;CACrB,MAAM,OAAO,OAAO,QAAQ;CAC5B,IAAI,CAAC,KAAK,WAAW,GAAG,GAAG,MAAM,IAAI,MAAM,2CAA2C,MAAM;CAC5F,MAAM,cAAc,OAAO,eAAe;CAC1C,MAAM,SAAS,IAAI,IAAI,cAAc;CACrC,MAAM,MACJ,QAAQ,SAAS,eACb,IAAI,IACF,MACA,GAAG,OAAO,SAAS,IAAI,cAAc,GAAG,YAAY,MAAM,KAAK,OAAO,QAAQ,GAAG,QAAQ,WAAW,OAAO,OAAO,IAAI,OAAO,SAAS,IACxI,IACA,IAAI,IACF,aAAa,OAAO,UAAU,cAAc,IAAI,gBAAgB,KAAK,QACrE,OAAO,MACT;CACN,MAAM,SAAS,iBAAiB,SAAS,KAAK,cAAc;CAC5D,OAAO,UAAU,OAAO,YAAY,OAAO,WAAW,OAAO,gBAAgB,cACzE,MACA;AACN"}
@@ -1,9 +1,9 @@
1
1
  import { connectLiveState } from "./client.mjs";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
- //#region src/next/client/react.tsx
3
+ //#region src/client/react.tsx
4
4
  /** @jsxImportSource react */
5
5
  /** Subscribe to a producer's live state and render its latest value. Pass a ready `itx` (a capnweb
6
- * `api.authenticate(credentials).user` or `.projects.get(id)`), the producer's `key`, and a `door`
6
+ * `api.authenticate(credentials).user` or `.projects.get(id)`), the producer's `key`, and a `readSeed`
7
7
  * thunk that reads `{rev, state}` (`() => itx.invoke("itx.facets.get('slug').liveSnapshot()")`).
8
8
  * Re-subscribes when the session, `key`, or `name` changes; unmount (and every re-subscribe)
9
9
  * disposes the previous server-side subscription. */
@@ -11,21 +11,21 @@ function useLiveState(itx, opts) {
11
11
  const [store, setStore] = useState();
12
12
  const [status, setStatus] = useState("connecting");
13
13
  const [error, setError] = useState();
14
- const doorRef = useRef(opts.door);
15
- doorRef.current = opts.door;
14
+ const readSeedRef = useRef(opts.readSeed);
15
+ readSeedRef.current = opts.readSeed;
16
16
  useEffect(() => {
17
17
  setStore(void 0);
18
18
  setStatus("connecting");
19
19
  setError(void 0);
20
20
  if (!itx) return;
21
- const door = doorRef.current;
21
+ const readSeed = readSeedRef.current;
22
22
  let disposed = false;
23
23
  let dispose;
24
24
  const unmounted = new AbortController();
25
25
  connectLiveState(itx, {
26
26
  key: opts.key,
27
27
  name: opts.name,
28
- door,
28
+ readSeed,
29
29
  signal: unmounted.signal,
30
30
  onResync: (r) => {
31
31
  if (disposed) return;
@@ -67,10 +67,10 @@ function useLiveState(itx, opts) {
67
67
  error
68
68
  };
69
69
  }
70
- /** A wire event (a capnweb proxy value or a plain object) as an `IterateContextEvent`, or null when
70
+ /** A wire event (a capnweb proxy value or a plain object) as a `StreamEvent`, or null when
71
71
  * it is not a committed row. Structural, not a schema: the transport validated it; this only refuses
72
72
  * a shape the view cannot place (no offset, type or time). */
73
- function toIterateContextEvent(raw) {
73
+ function toStreamEvent(raw) {
74
74
  const value = JSON.parse(JSON.stringify(raw));
75
75
  if (!value || typeof value.offset !== "number" || typeof value.type !== "string" || typeof value.createdAt !== "string") return null;
76
76
  return value;
@@ -108,7 +108,7 @@ function useIterateContext(itx, opts = {}) {
108
108
  const merge = (batch) => setEvents((held) => {
109
109
  const next = new Map(held);
110
110
  for (const raw of batch) {
111
- const event = toIterateContextEvent(raw);
111
+ const event = toStreamEvent(raw);
112
112
  if (event) next.set(event.offset, event);
113
113
  }
114
114
  return next;
@@ -158,11 +158,11 @@ function useIterateContext(itx, opts = {}) {
158
158
  return () => {
159
159
  disposed = true;
160
160
  };
161
- }, [itx, sorted.reduce((last, event) => event.type.startsWith("events.iterate.com/stream/subscription-") ? event.offset : last, 0)]);
161
+ }, [itx, sorted.reduce((last, event) => event.type.startsWith("events.iterate.com/itx/subscription-") ? event.offset : last, 0)]);
162
162
  const currentTable = itx && table?.itx === itx ? table : void 0;
163
163
  const [census, setCensus] = useState();
164
164
  useEffect(() => {
165
- if (!itx?.rpcStubs) return;
165
+ if (!itx) return;
166
166
  let disposed = false;
167
167
  Promise.resolve(itx.rpcStubs.list()).then((list) => !disposed && setCensus({
168
168
  itx,
@@ -213,7 +213,7 @@ function useIterateContext(itx, opts = {}) {
213
213
  });
214
214
  for (const name of names) connectLiveState(itx, {
215
215
  key: name,
216
- door: async () => await itx.invoke(`itx.facets.get('${name}').liveSnapshot()`),
216
+ readSeed: async () => await itx.invoke(`itx.facets.get('${name}').liveSnapshot()`),
217
217
  signal: unmounted.signal,
218
218
  onResync: (result) => {
219
219
  if (disposed) return;
@@ -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;