@nyte-ai/host 0.0.0-stage → 0.0.19

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 (250) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/THIRD-PARTY-NOTICES.md +866 -0
  4. package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
  5. package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
  6. package/dist/chunks/canvas-preview-11nadadc.js +241 -0
  7. package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
  8. package/dist/chunks/canvas-preview-2jwbe14k.js +130 -0
  9. package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
  10. package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
  11. package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
  12. package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
  13. package/dist/chunks/canvas-preview-518dh8by.js +169 -0
  14. package/dist/chunks/canvas-preview-55s0y68g.js +387 -0
  15. package/dist/chunks/canvas-preview-5ekjx6sw.js +613 -0
  16. package/dist/chunks/canvas-preview-5mex725c.js +14 -0
  17. package/dist/chunks/canvas-preview-5z63am63.js +295 -0
  18. package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
  19. package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
  20. package/dist/chunks/canvas-preview-8xc0vn7v.js +458 -0
  21. package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
  22. package/dist/chunks/canvas-preview-af86jz8t.js +141 -0
  23. package/dist/chunks/canvas-preview-aq5j4p25.js +240 -0
  24. package/dist/chunks/canvas-preview-b2v8wfnz.js +36 -0
  25. package/dist/chunks/canvas-preview-befkjea2.js +60 -0
  26. package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
  27. package/dist/chunks/canvas-preview-cawy6vbz.js +352 -0
  28. package/dist/chunks/canvas-preview-crmzxxt8.js +518 -0
  29. package/dist/chunks/canvas-preview-drctegz4.js +591 -0
  30. package/dist/chunks/canvas-preview-e3vnkkc5.js +134 -0
  31. package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
  32. package/dist/chunks/canvas-preview-fxa0rfb0.js +61 -0
  33. package/dist/chunks/canvas-preview-g5fkjs34.js +598 -0
  34. package/dist/chunks/canvas-preview-jeddjex8.js +166 -0
  35. package/dist/chunks/canvas-preview-k1yg4fgp.js +571 -0
  36. package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
  37. package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
  38. package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
  39. package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
  40. package/dist/chunks/canvas-preview-n2a35zv5.js +81 -0
  41. package/dist/chunks/canvas-preview-pkzqg96v.js +63 -0
  42. package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
  43. package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
  44. package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
  45. package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
  46. package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
  47. package/dist/chunks/canvas-preview-tnqe7k0b.js +374 -0
  48. package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
  49. package/dist/chunks/canvas-preview-vm9zdp2t.js +398 -0
  50. package/dist/chunks/canvas-preview-w54ymtb3.js +272 -0
  51. package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
  52. package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
  53. package/dist/chunks/canvas-preview-ycepb05s.js +176 -0
  54. package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
  55. package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
  56. package/dist/src/canvas-preview.d.ts +29 -0
  57. package/dist/src/canvas-preview.js +271 -0
  58. package/dist/src/credentials.d.ts +44 -0
  59. package/dist/src/credentials.js +10 -0
  60. package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
  61. package/dist/src/deploy/cloudflare/worker.js +196 -0
  62. package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
  63. package/dist/src/deploy/vercel/nitro.config.js +30 -0
  64. package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
  65. package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
  66. package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
  67. package/dist/src/deploy/vercel/server/utils/host.js +7 -0
  68. package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
  69. package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
  70. package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
  71. package/dist/src/deploy/vercel/server/utils/step.js +6 -0
  72. package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
  73. package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
  74. package/dist/src/durable-object.d.ts +14 -0
  75. package/dist/src/durable-object.js +231 -0
  76. package/dist/src/hash.d.ts +1 -0
  77. package/dist/src/hash.js +6 -0
  78. package/dist/src/identity-key.d.ts +11 -0
  79. package/dist/src/identity-key.js +9 -0
  80. package/dist/src/identity.d.ts +12 -0
  81. package/dist/src/identity.js +6 -0
  82. package/dist/src/images/exif.d.ts +1 -0
  83. package/dist/src/images/exif.js +6 -0
  84. package/dist/src/images/node.d.ts +37 -0
  85. package/dist/src/images/node.js +12 -0
  86. package/dist/src/images/photon.d.ts +3 -0
  87. package/dist/src/images/photon.js +7 -0
  88. package/dist/src/images/worker.d.ts +1 -0
  89. package/dist/src/images/worker.js +23 -0
  90. package/dist/src/index.d.ts +20 -0
  91. package/dist/src/index.js +121 -0
  92. package/dist/src/key-file.d.ts +1 -0
  93. package/dist/src/key-file.js +7 -0
  94. package/dist/src/listen-fetch.d.ts +8 -0
  95. package/dist/src/listen-fetch.js +9 -0
  96. package/dist/src/listen.d.ts +4 -0
  97. package/dist/src/listen.js +10 -0
  98. package/dist/src/local/command.d.ts +20 -0
  99. package/dist/src/local/command.js +12 -0
  100. package/dist/src/local/feed.d.ts +7 -0
  101. package/dist/src/local/feed.js +6 -0
  102. package/dist/src/local/files.d.ts +18 -0
  103. package/dist/src/local/files.js +13 -0
  104. package/dist/src/local/git-watch.d.ts +15 -0
  105. package/dist/src/local/git-watch.js +7 -0
  106. package/dist/src/local/git.d.ts +5 -0
  107. package/dist/src/local/git.js +13 -0
  108. package/dist/src/local/github.d.ts +30 -0
  109. package/dist/src/local/github.js +9 -0
  110. package/dist/src/local/identity.d.ts +3 -0
  111. package/dist/src/local/identity.js +7 -0
  112. package/dist/src/local/jobs.d.ts +7 -0
  113. package/dist/src/local/jobs.js +7 -0
  114. package/dist/src/local/legacy-trust.d.ts +10 -0
  115. package/dist/src/local/legacy-trust.js +6 -0
  116. package/dist/src/local/patch.d.ts +16 -0
  117. package/dist/src/local/patch.js +17 -0
  118. package/dist/src/local/provider.d.ts +11 -0
  119. package/dist/src/local/provider.js +20 -0
  120. package/dist/src/local/snapshots.d.ts +7 -0
  121. package/dist/src/local/snapshots.js +11 -0
  122. package/dist/src/local/text.d.ts +1 -0
  123. package/dist/src/local/text.js +6 -0
  124. package/dist/src/local/tree.d.ts +50 -0
  125. package/dist/src/local/tree.js +16 -0
  126. package/dist/src/local/workspace-migration.d.ts +2 -0
  127. package/dist/src/local/workspace-migration.js +7 -0
  128. package/dist/src/local/worktrees.d.ts +46 -0
  129. package/dist/src/local/worktrees.js +14 -0
  130. package/dist/src/mcp/config.d.ts +75 -0
  131. package/dist/src/mcp/config.js +12 -0
  132. package/dist/src/mcp/connect.d.ts +15 -0
  133. package/dist/src/mcp/connect.js +9 -0
  134. package/dist/src/mcp/hub.d.ts +19 -0
  135. package/dist/src/mcp/hub.js +11 -0
  136. package/dist/src/mcp/oauth.d.ts +46 -0
  137. package/dist/src/mcp/oauth.js +12 -0
  138. package/dist/src/mcp/tools.d.ts +29 -0
  139. package/dist/src/mcp/tools.js +12 -0
  140. package/dist/src/open.d.ts +14 -0
  141. package/dist/src/open.js +46 -0
  142. package/dist/src/plugins/loader.d.ts +21 -0
  143. package/dist/src/plugins/loader.js +11 -0
  144. package/dist/src/postgres.d.ts +19 -0
  145. package/dist/src/postgres.js +8 -0
  146. package/dist/src/providers.d.ts +16 -0
  147. package/dist/src/providers.js +112 -0
  148. package/dist/src/secrets.d.ts +12 -0
  149. package/dist/src/secrets.js +8 -0
  150. package/dist/src/serve/binding.d.ts +39 -0
  151. package/dist/src/serve/binding.js +6 -0
  152. package/dist/src/serve/handler.d.ts +15 -0
  153. package/dist/src/serve/handler.js +8 -0
  154. package/dist/src/serve/stream-server.d.ts +6 -0
  155. package/dist/src/serve/stream-server.js +6 -0
  156. package/dist/src/session-index.d.ts +11 -0
  157. package/dist/src/session-index.js +11 -0
  158. package/dist/src/shell-env.d.ts +14 -0
  159. package/dist/src/shell-env.js +108 -0
  160. package/dist/src/sqlite-workers.d.ts +6 -0
  161. package/dist/src/sqlite-workers.js +6 -0
  162. package/dist/src/sqlite.d.ts +8 -0
  163. package/dist/src/sqlite.js +11 -0
  164. package/dist/src/store-rows.d.ts +13 -0
  165. package/dist/src/store-rows.js +26 -0
  166. package/dist/src/store-usage.d.ts +25 -0
  167. package/dist/src/store-usage.js +19 -0
  168. package/dist/src/usage-history.d.ts +54 -0
  169. package/dist/src/usage-history.js +14 -0
  170. package/dist/src/usage-scan.d.ts +24 -0
  171. package/dist/src/usage-scan.js +65 -0
  172. package/dist/src/usage-service.d.ts +23 -0
  173. package/dist/src/usage-service.js +16 -0
  174. package/dist/src/usage-summary.d.ts +9 -0
  175. package/dist/src/usage-summary.js +16 -0
  176. package/dist/src/usage-types.d.ts +86 -0
  177. package/dist/src/usage-types.js +0 -0
  178. package/dist/src/usage-worker.d.ts +1 -0
  179. package/dist/src/usage-worker.js +29 -0
  180. package/dist/src/usage.d.ts +195 -0
  181. package/dist/src/usage.js +23 -0
  182. package/package.json +140 -3
  183. package/src/canvas-preview.ts +403 -0
  184. package/src/credentials.ts +294 -0
  185. package/src/deploy/cloudflare/README.md +3 -0
  186. package/src/deploy/cloudflare/worker.ts +306 -0
  187. package/src/deploy/cloudflare/wrangler.jsonc +12 -0
  188. package/src/deploy/vercel/README.md +3 -0
  189. package/src/deploy/vercel/nitro.config.ts +29 -0
  190. package/src/deploy/vercel/server/routes/[...].ts +10 -0
  191. package/src/deploy/vercel/server/utils/host.ts +104 -0
  192. package/src/deploy/vercel/server/utils/repair.ts +35 -0
  193. package/src/deploy/vercel/server/utils/step.ts +25 -0
  194. package/src/deploy/vercel/server/workflows/drive.ts +46 -0
  195. package/src/deploy/vercel/tsconfig.json +9 -0
  196. package/src/deploy/vercel/types/bun.d.ts +2 -0
  197. package/src/durable-object.ts +386 -0
  198. package/src/hash.ts +5 -0
  199. package/src/identity-key.ts +20 -0
  200. package/src/identity.ts +66 -0
  201. package/src/images/exif.ts +69 -0
  202. package/src/images/node.ts +163 -0
  203. package/src/images/photon.ts +141 -0
  204. package/src/images/worker.ts +29 -0
  205. package/src/index.ts +39 -0
  206. package/src/key-file.ts +53 -0
  207. package/src/listen-fetch.ts +409 -0
  208. package/src/listen.ts +228 -0
  209. package/src/local/command.ts +161 -0
  210. package/src/local/feed.ts +70 -0
  211. package/src/local/files.ts +817 -0
  212. package/src/local/git-watch.ts +199 -0
  213. package/src/local/git.ts +919 -0
  214. package/src/local/github.ts +691 -0
  215. package/src/local/identity.ts +21 -0
  216. package/src/local/jobs.ts +573 -0
  217. package/src/local/legacy-trust.ts +105 -0
  218. package/src/local/patch.ts +334 -0
  219. package/src/local/provider.ts +184 -0
  220. package/src/local/snapshots.ts +778 -0
  221. package/src/local/text.ts +9 -0
  222. package/src/local/tree.ts +398 -0
  223. package/src/local/workspace-migration.ts +95 -0
  224. package/src/local/worktrees.ts +239 -0
  225. package/src/mcp/config.ts +330 -0
  226. package/src/mcp/connect.ts +92 -0
  227. package/src/mcp/hub.ts +836 -0
  228. package/src/mcp/oauth.ts +436 -0
  229. package/src/mcp/tools.ts +333 -0
  230. package/src/open.ts +449 -0
  231. package/src/plugins/loader.ts +186 -0
  232. package/src/postgres.ts +492 -0
  233. package/src/providers.ts +135 -0
  234. package/src/secrets.ts +31 -0
  235. package/src/serve/binding.ts +629 -0
  236. package/src/serve/handler.ts +362 -0
  237. package/src/serve/stream-server.ts +176 -0
  238. package/src/session-index.ts +99 -0
  239. package/src/shell-env.ts +153 -0
  240. package/src/sqlite-workers.ts +283 -0
  241. package/src/sqlite.ts +553 -0
  242. package/src/store-rows.ts +110 -0
  243. package/src/store-usage.ts +347 -0
  244. package/src/usage-history.ts +219 -0
  245. package/src/usage-scan.ts +81 -0
  246. package/src/usage-service.ts +82 -0
  247. package/src/usage-summary.ts +79 -0
  248. package/src/usage-types.ts +87 -0
  249. package/src/usage-worker.ts +24 -0
  250. package/src/usage.ts +776 -0
@@ -0,0 +1,409 @@
1
+ import { SdkError } from "@nyte-ai/schema";
2
+ import { serial } from "@nyte-ai/schema/serial";
3
+ import type { Authority, Disposer, Owned, PrincipalId } from "@nyte-ai/schema";
4
+ import { dev } from "@nyte-ai/core";
5
+ import { TOKEN, TOKEN_AUTHORITY } from "@nyte-ai/core/server";
6
+ import type { Connection, ListenOptions, Listener } from "@nyte-ai/core";
7
+ import type { ServedHost, Verdict } from "@nyte-ai/core/server";
8
+ import {
9
+ HOST_HEADER,
10
+ OPERATIONS_HEADER,
11
+ asSdkError,
12
+ encodeError,
13
+ errorStatus,
14
+ operationsDigest,
15
+ } from "@nyte-ai/protocol";
16
+ import { createHandler } from "./serve/handler.ts";
17
+ import type { HandlerOptions } from "./serve/handler.ts";
18
+
19
+ type Cached = {
20
+ readonly connection: Connection;
21
+ readonly principal: PrincipalId;
22
+ dispose: Disposer;
23
+ };
24
+
25
+ type Flight = {
26
+ readonly request: Request;
27
+ readonly controller: AbortController;
28
+ principal: PrincipalId | null;
29
+ };
30
+
31
+ type Device = {
32
+ readonly principal: PrincipalId;
33
+ readonly authority: ReadonlySet<Authority>;
34
+ readonly token: string;
35
+ };
36
+
37
+ const encoder = new TextEncoder();
38
+
39
+ /** Serves a composed host as a bare fetch handler with no socket and no `node:*` import, so a Worker can serve it (§41j): `before`, the unauthenticated identity and describe routes, then `createHandler` over one cached connection per principal and authority, every response carrying `X-Nyte-Host` (§43a, §43j). A request authenticates by a pairing code, then the listener token (compared in constant time over SHA-256 digests) or a device token remembered until a commit from another process moves the host's principals, then `authorize` under `custom`; a revoke aborts that principal's open requests and streams. A `token` shorter than 16 characters throws `SdkError("invalid")`; `execution` (default: run covering all) drives covered sessions in process on an `admit-only` host, covers their leases otherwise and takes nothing on a `dispatch` host (§42v); `close()` disconnects the clients and releases the execution once, after which every request answers `unavailable`. */
40
+ export async function listenFetch(
41
+ host: ServedHost,
42
+ options: ListenOptions & { readonly transport: { readonly kind: "fetch" } },
43
+ ): Promise<Listener> {
44
+ const { auth } = options;
45
+
46
+ if (auth.kind === "token" && auth.token.length < 16)
47
+ throw new SdkError("invalid", "A listener token needs at least 16 characters.");
48
+ const { server } = host;
49
+ const operations = await operationsDigest();
50
+ const cache = new Map<string, Cached>();
51
+ const devices = new Map<string, Device>();
52
+ const streams = new Map<PrincipalId, Set<Flight>>();
53
+ const flights = new WeakMap<Request, Flight>();
54
+ let generation = new AbortController();
55
+ let closing: Promise<void> | null = null;
56
+ let principalsRevision: string | null = null;
57
+ const rechecks = serial();
58
+
59
+ const drop = (key: string, entry: Cached, reason: SdkError): void => {
60
+ if (cache.get(key) === entry) cache.delete(key);
61
+ entry.dispose();
62
+
63
+ for (const [digest, device] of devices)
64
+ if (device.principal === entry.principal) devices.delete(digest);
65
+ const open = streams.get(entry.principal);
66
+ streams.delete(entry.principal);
67
+
68
+ for (const flight of open ?? []) flight.controller.abort(reason);
69
+ void entry.connection.close();
70
+ };
71
+
72
+ const land = (flight: Flight): void => {
73
+ for (const [principal, open] of streams)
74
+ if (open.delete(flight) && open.size === 0) streams.delete(principal);
75
+ };
76
+
77
+ const connectionFor = (
78
+ flight: Flight,
79
+ principal: PrincipalId,
80
+ authority: ReadonlySet<Authority>,
81
+ token: string,
82
+ ): Connection => {
83
+ const key = JSON.stringify([principal, [...authority].sort()]);
84
+ const entry = cache.get(key) ?? admit(key, principal, authority, token);
85
+ flight.principal = principal;
86
+ const open = streams.get(principal) ?? new Set<Flight>();
87
+ open.add(flight);
88
+ streams.set(principal, open);
89
+
90
+ return entry.connection;
91
+ };
92
+
93
+ const admit = (
94
+ key: string,
95
+ principal: PrincipalId,
96
+ authority: ReadonlySet<Authority>,
97
+ token: string,
98
+ ): Cached => {
99
+ const connection = server.connect({
100
+ principal,
101
+ authority,
102
+ credential: { kind: "token", token },
103
+ origin: null,
104
+ });
105
+
106
+ const entry: Cached = { connection, principal, dispose: () => undefined };
107
+ const admitted = server.connections.register(principal, () =>
108
+ drop(key, entry, new SdkError("unauthenticated", "The principal was revoked.")),
109
+ );
110
+
111
+ if (admitted.kind === "revoked")
112
+ throw new SdkError("unauthenticated", "The principal was revoked.");
113
+ entry.dispose = admitted.dispose;
114
+ cache.set(key, entry);
115
+
116
+ return entry;
117
+ };
118
+
119
+ const reread = async (token: string, digest: string): Promise<Device | null> => {
120
+ const found = await server.authenticate(token);
121
+
122
+ if (found === null) {
123
+ const known = devices.get(digest);
124
+ devices.delete(digest);
125
+
126
+ if (known !== undefined) server.connections.revoke(known.principal);
127
+
128
+ return null;
129
+ }
130
+
131
+ return { ...found, token };
132
+ };
133
+
134
+ const findDevice = async (
135
+ token: string,
136
+ ): Promise<{ readonly digest: string; readonly device: Device } | null> => {
137
+ const digest = hex(await sha256(token));
138
+ const device = devices.get(digest) ?? (await reread(token, digest));
139
+
140
+ return device === null ? null : { digest, device };
141
+ };
142
+
143
+ const recheck = async (): Promise<void> => {
144
+ if (devices.size === 0) return;
145
+ const revision = await server.principalsRevision();
146
+
147
+ if (revision === principalsRevision) return;
148
+ principalsRevision = revision;
149
+
150
+ await Promise.all([...devices].map(([digest, known]) => reread(known.token, digest)));
151
+ };
152
+
153
+ const externalCommit = (): void => {
154
+ void rechecks
155
+ .run(recheck)
156
+ .catch((error) => dev("listen", "device re-check failed", { error: String(error) }));
157
+ };
158
+
159
+ const unwatchCommits = server.onExternalCommit(externalCommit);
160
+
161
+ const isListenerToken: (token: string) => Promise<boolean> =
162
+ auth.kind === "token" ? (token) => sameSecret(token, auth.token) : async () => false;
163
+
164
+ const fallback: (request: Request, token: string | undefined) => Promise<Verdict> =
165
+ auth.kind === "custom"
166
+ ? async (request) => {
167
+ const verdict = await auth.authorize(request);
168
+
169
+ if ("deny" in verdict)
170
+ throw verdict.deny === "forbidden"
171
+ ? new SdkError("denied", "This principal may not call the host.")
172
+ : new SdkError("unauthenticated", "The request is not authorized.");
173
+
174
+ return verdict;
175
+ }
176
+ : async (_request, token) => {
177
+ throw new SdkError(
178
+ "unauthenticated",
179
+ token === undefined
180
+ ? "Send Authorization: Bearer <token>."
181
+ : "The token is not accepted.",
182
+ );
183
+ };
184
+
185
+ const authenticate = async (
186
+ request: Request,
187
+ flight: Flight,
188
+ ): ReturnType<HandlerOptions["connection"]> => {
189
+ const header = request.headers.get("Authorization") ?? "";
190
+ const pairing = /^Pairing\s+(\S+)\s*$/i.exec(header)?.[1];
191
+
192
+ if (pairing !== undefined) {
193
+ const redeemed = await server.redeem(pairing);
194
+
195
+ if (redeemed === null)
196
+ throw new SdkError("unauthenticated", "The pairing code is unknown or has expired.");
197
+ const digest = hex(await sha256(redeemed.token));
198
+ const { principal, authority, token } = redeemed;
199
+ const connection = connectionFor(flight, principal, authority, token);
200
+ devices.set(digest, { principal, authority, token });
201
+
202
+ return { connection, authority, headers: { "X-Nyte-Credential": token } };
203
+ }
204
+
205
+ const token = /^Bearer\s+(\S+)\s*$/i.exec(header)?.[1];
206
+
207
+ if (token !== undefined) {
208
+ if (await isListenerToken(token))
209
+ return {
210
+ connection: connectionFor(flight, TOKEN, TOKEN_AUTHORITY, token),
211
+ authority: TOKEN_AUTHORITY,
212
+ };
213
+ const remembered = await findDevice(token);
214
+
215
+ if (remembered !== null) {
216
+ const { digest, device } = remembered;
217
+ const connection = connectionFor(flight, device.principal, device.authority, token);
218
+ devices.set(digest, device);
219
+
220
+ return { connection, authority: device.authority };
221
+ }
222
+ }
223
+
224
+ const verdict = await fallback(request, token);
225
+
226
+ return {
227
+ connection: connectionFor(flight, verdict.principal, verdict.authority, ""),
228
+ authority: verdict.authority,
229
+ };
230
+ };
231
+
232
+ const handler = createHandler({
233
+ server,
234
+ origins: options.origins,
235
+ maxBodyBytes: options.maxBodyBytes,
236
+ connection: (request) => {
237
+ const flight = flights.get(request);
238
+ if (flight === undefined) throw new Error("The listener did not register this request.");
239
+ return authenticate(request, flight);
240
+ },
241
+ });
242
+
243
+ const cors = (request: Request): Readonly<Record<string, string>> => {
244
+ const origin = request.headers.get("Origin");
245
+
246
+ return origin !== null && (options.origins ?? []).includes(origin)
247
+ ? {
248
+ "Access-Control-Allow-Origin": origin,
249
+ "Access-Control-Expose-Headers": `${HOST_HEADER}, ${OPERATIONS_HEADER}`,
250
+ Vary: "Origin",
251
+ }
252
+ : {};
253
+ };
254
+
255
+ const serve = async (
256
+ request: Request,
257
+ trace: { principal: PrincipalId | null },
258
+ ): Promise<Response> => {
259
+ if (closing !== null)
260
+ return failure(new SdkError("unavailable", "This listener is closed."), {});
261
+ const early = await options.before?.(request);
262
+
263
+ if (early !== undefined) return early;
264
+ const url = new URL(request.url);
265
+
266
+ if (request.method === "GET" && url.pathname === "/.nyte/identity") {
267
+ const nonce = url.searchParams.get("nonce");
268
+
269
+ if (options.identity === undefined || nonce === null || nonce === "")
270
+ return failure(
271
+ new SdkError("unknown", "This host answers no identity challenge."),
272
+ cors(request),
273
+ );
274
+
275
+ return json(
276
+ { ...(await options.identity.sign(nonce)), hostId: server.hostId },
277
+ cors(request),
278
+ );
279
+ }
280
+
281
+ if (request.method === "GET" && url.pathname === "/.nyte/describe") {
282
+ if (options.describe === undefined)
283
+ return failure(new SdkError("unknown", "This host describes nothing."), cors(request));
284
+
285
+ return json(await options.describe(), cors(request));
286
+ }
287
+
288
+ const controller = new AbortController();
289
+
290
+ const forwarded = new Request(request, {
291
+ signal: AbortSignal.any([request.signal, generation.signal, controller.signal]),
292
+ });
293
+
294
+ const flight: Flight = { request: forwarded, controller, principal: null };
295
+ flights.set(forwarded, flight);
296
+ const response = await handler(forwarded);
297
+ trace.principal = flight.principal;
298
+
299
+ return landing(response, () => land(flight));
300
+ };
301
+
302
+ const fetch = async (request: Request): Promise<Response> => {
303
+ const started = Date.now();
304
+ const trace: { principal: PrincipalId | null } = { principal: null };
305
+
306
+ const response = await serve(request, trace).catch((error: unknown) =>
307
+ failure(asSdkError(error), cors(request)),
308
+ );
309
+
310
+ dev("listen", "request", {
311
+ method: request.method,
312
+ path: new URL(request.url).pathname,
313
+ status: response.status,
314
+ principal:
315
+ trace.principal === null ? "none" : trace.principal === TOKEN ? "token" : "principal",
316
+ ms: Date.now() - started,
317
+ });
318
+
319
+ return withHost(response, server.hostId, operations);
320
+ };
321
+
322
+ const execution = options.execution ?? { kind: "run", coverage: { kind: "all" } };
323
+
324
+ const coverage: Owned | null =
325
+ execution.kind === "admit-only" || server.execution.kind === "dispatch"
326
+ ? null
327
+ : server.execution.kind === "admit-only"
328
+ ? server.drive(execution.coverage)
329
+ : await host.execution.cover(execution.coverage);
330
+
331
+ const disconnectClients = (): void => {
332
+ for (const [key, entry] of cache)
333
+ drop(key, entry, new SdkError("unavailable", "The host disconnected its clients."));
334
+ devices.clear();
335
+ generation.abort(new SdkError("unavailable", "The host disconnected its clients."));
336
+ generation = new AbortController();
337
+ };
338
+
339
+ const close = (): Promise<void> => {
340
+ closing ??= (async () => {
341
+ unwatchCommits();
342
+ disconnectClients();
343
+ await coverage?.close();
344
+ })();
345
+
346
+ return closing;
347
+ };
348
+
349
+ return { address: null, fetch, disconnectClients, close, [Symbol.asyncDispose]: close };
350
+ }
351
+
352
+ function landing(response: Response, done: () => void): Response {
353
+ if (response.body === null) {
354
+ done();
355
+
356
+ return response;
357
+ }
358
+
359
+ const settles = { flush: done, cancel: done };
360
+
361
+ return new Response(
362
+ response.body.pipeThrough(new TransformStream<Uint8Array, Uint8Array>(settles)),
363
+ { status: response.status, statusText: response.statusText, headers: response.headers },
364
+ );
365
+ }
366
+
367
+ function withHost(response: Response, hostId: string, operations: string): Response {
368
+ const headers = new Headers(response.headers);
369
+ headers.set(HOST_HEADER, hostId);
370
+ headers.set(OPERATIONS_HEADER, operations);
371
+
372
+ return new Response(response.body, {
373
+ status: response.status,
374
+ statusText: response.statusText,
375
+ headers,
376
+ });
377
+ }
378
+
379
+ function json(value: unknown, headers: Readonly<Record<string, string>>): Response {
380
+ return new Response(JSON.stringify(value), {
381
+ status: 200,
382
+ headers: { ...headers, "Content-Type": "application/json" },
383
+ });
384
+ }
385
+
386
+ function failure(error: SdkError, headers: Readonly<Record<string, string>>): Response {
387
+ return new Response(JSON.stringify(encodeError(error)), {
388
+ status: errorStatus[error.code],
389
+ headers: { ...headers, "Content-Type": "application/json" },
390
+ });
391
+ }
392
+
393
+ async function sha256(text: string): Promise<Uint8Array> {
394
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(text)));
395
+ }
396
+
397
+ function hex(bytes: Uint8Array): string {
398
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
399
+ }
400
+
401
+ async function sameSecret(given: string, expected: string): Promise<boolean> {
402
+ const [left, right] = await Promise.all([sha256(given), sha256(expected)]);
403
+ let difference = left.length ^ right.length;
404
+
405
+ for (let index = 0; index < left.length; index += 1)
406
+ difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
407
+
408
+ return difference === 0;
409
+ }
package/src/listen.ts ADDED
@@ -0,0 +1,228 @@
1
+ import { createServer } from "node:http";
2
+ import type { IncomingMessage, Server, ServerResponse } from "node:http";
3
+ import { SdkError } from "@nyte-ai/schema";
4
+ import type { ListenOptions, Listener } from "@nyte-ai/core";
5
+ import type { ServedHost } from "@nyte-ai/core/server";
6
+ import { encodeError, errorStatus } from "@nyte-ai/protocol";
7
+ import { listenFetch } from "./listen-fetch.ts";
8
+
9
+ /** Serves a composed host, the `Host.listen` `openHost` attaches: everything but the socket is `listenFetch`'s, and the `fetch` transport answers with it directly. The `http` transport binds `node:http` on `hostname` (default 127.0.0.1) and `port` (default 0), streaming each request body in and each response body out chunk by chunk, with a client gone aborting its request; a failed bind throws `SdkError("unavailable")` after releasing the execution. `disconnectClients()` also destroys every open Node response, and `close()` closes the socket once. */
10
+ export async function listen(host: ServedHost, options: ListenOptions): Promise<Listener> {
11
+ const inner = await listenFetch(host, { ...options, transport: { kind: "fetch" } });
12
+
13
+ if (options.transport.kind === "fetch") return inner;
14
+ const responses = new Set<ServerResponse>();
15
+
16
+ const bound = await bind(options.transport, (request) => inner.fetch(request), responses).catch(
17
+ async (error: unknown) => {
18
+ await inner.close();
19
+
20
+ throw new SdkError(
21
+ "unavailable",
22
+ `Cannot listen: ${error instanceof Error ? error.message : String(error)}`,
23
+ );
24
+ },
25
+ );
26
+
27
+ const disconnectClients = (): void => {
28
+ inner.disconnectClients();
29
+
30
+ for (const response of responses) response.destroy();
31
+ };
32
+
33
+ let closing: Promise<void> | null = null;
34
+
35
+ const close = (): Promise<void> => {
36
+ closing ??= (async () => {
37
+ disconnectClients();
38
+ await inner.close();
39
+ await new Promise<void>((done) => {
40
+ bound.server.close(() => done());
41
+ bound.server.closeAllConnections();
42
+ });
43
+ })();
44
+
45
+ return closing;
46
+ };
47
+
48
+ return {
49
+ address: bound.address,
50
+ fetch: (request) => inner.fetch(request),
51
+ disconnectClients,
52
+ close,
53
+ [Symbol.asyncDispose]: close,
54
+ };
55
+ }
56
+
57
+ async function bind(
58
+ transport: { readonly hostname?: string; readonly port?: number },
59
+ fetch: (request: Request) => Promise<Response>,
60
+ responses: Set<ServerResponse>,
61
+ ): Promise<{ readonly server: Server; readonly address: string }> {
62
+ let origin = "http://127.0.0.1";
63
+
64
+ const server = createServer({ noDelay: true }, (incoming, outgoing) => {
65
+ void respond(incoming, outgoing, origin, fetch, responses);
66
+ });
67
+
68
+ await new Promise<void>((listening, failed) => {
69
+ server.once("error", failed);
70
+ server.listen(transport.port ?? 0, transport.hostname ?? "127.0.0.1", () => {
71
+ server.off("error", failed);
72
+ listening();
73
+ });
74
+ });
75
+
76
+ const info = server.address();
77
+
78
+ if (info === null || typeof info === "string") {
79
+ server.close();
80
+
81
+ throw new Error("the socket has no address");
82
+ }
83
+
84
+ const hostname = info.address.includes(":") ? `[${info.address}]` : info.address;
85
+ origin = `http://${hostname}:${info.port}`;
86
+
87
+ return { server, address: origin };
88
+ }
89
+
90
+ async function respond(
91
+ incoming: IncomingMessage,
92
+ outgoing: ServerResponse,
93
+ origin: string,
94
+ fetch: (request: Request) => Promise<Response>,
95
+ responses: Set<ServerResponse>,
96
+ ): Promise<void> {
97
+ const controller = new AbortController();
98
+ responses.add(outgoing);
99
+ outgoing.on("close", () => {
100
+ responses.delete(outgoing);
101
+
102
+ if (!outgoing.writableFinished) controller.abort();
103
+ });
104
+
105
+ let request: Request;
106
+
107
+ try {
108
+ request = readRequest(incoming, origin, controller.signal);
109
+ } catch (error) {
110
+ await write(
111
+ failure(new SdkError("invalid", error instanceof Error ? error.message : String(error))),
112
+ outgoing,
113
+ );
114
+
115
+ return;
116
+ }
117
+
118
+ await write(await fetch(request), outgoing);
119
+ }
120
+
121
+ function readRequest(incoming: IncomingMessage, origin: string, signal: AbortSignal): Request {
122
+ const headers = new Headers();
123
+
124
+ for (const [name, value] of Object.entries(incoming.headers)) {
125
+ if (name.startsWith(":") || value === undefined) continue;
126
+
127
+ if (Array.isArray(value)) for (const each of value) headers.append(name, each);
128
+ else headers.set(name, value);
129
+ }
130
+
131
+ const host = incoming.headers.host;
132
+ const url = new URL(incoming.url ?? "/", host === undefined ? origin : `http://${host}`);
133
+ const method = incoming.method ?? "GET";
134
+ const body =
135
+ method === "GET" || method === "HEAD"
136
+ ? {}
137
+ : { body: streamBody(incoming), duplex: "half" as const };
138
+
139
+ return new Request(url.href, { method, headers, signal, ...body });
140
+ }
141
+
142
+ function streamBody(incoming: IncomingMessage): ReadableStream<Uint8Array> {
143
+ return new ReadableStream<Uint8Array>({
144
+ start(controller) {
145
+ incoming.on("data", (chunk: Buffer) => {
146
+ controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
147
+
148
+ if ((controller.desiredSize ?? 0) <= 0) incoming.pause();
149
+ });
150
+ incoming.on("end", () => controller.close());
151
+ incoming.on("error", (error) => controller.error(error));
152
+ },
153
+ pull() {
154
+ incoming.resume();
155
+ },
156
+ cancel() {
157
+ incoming.destroy();
158
+ },
159
+ });
160
+ }
161
+
162
+ async function write(response: Response, outgoing: ServerResponse): Promise<void> {
163
+ if (outgoing.destroyed) {
164
+ await response.body?.cancel().catch(() => undefined);
165
+
166
+ return;
167
+ }
168
+
169
+ const headers: Record<string, string | string[]> = {};
170
+
171
+ response.headers.forEach((value, name) => {
172
+ headers[name] = value;
173
+ });
174
+ const cookies = response.headers.getSetCookie();
175
+
176
+ if (cookies.length > 0) headers["set-cookie"] = cookies;
177
+ outgoing.writeHead(response.status, headers);
178
+ outgoing.flushHeaders();
179
+
180
+ if (response.body === null) {
181
+ outgoing.end();
182
+
183
+ return;
184
+ }
185
+
186
+ const reader = response.body.getReader();
187
+
188
+ const cancel = (): void => {
189
+ reader.cancel().catch(() => undefined);
190
+ };
191
+
192
+ outgoing.on("close", cancel);
193
+
194
+ try {
195
+ for (;;) {
196
+ const read = await reader.read();
197
+
198
+ if (read.done) break;
199
+
200
+ if (!outgoing.write(read.value))
201
+ await new Promise<void>((drained) => {
202
+ const done = (): void => {
203
+ outgoing.off("drain", done);
204
+ outgoing.off("close", done);
205
+ drained();
206
+ };
207
+
208
+ outgoing.on("drain", done);
209
+ outgoing.on("close", done);
210
+ });
211
+
212
+ if (outgoing.destroyed) return;
213
+ }
214
+
215
+ outgoing.end();
216
+ } catch {
217
+ outgoing.destroy();
218
+ } finally {
219
+ outgoing.off("close", cancel);
220
+ }
221
+ }
222
+
223
+ function failure(error: SdkError): Response {
224
+ return new Response(JSON.stringify(encodeError(error)), {
225
+ status: errorStatus[error.code],
226
+ headers: { "Content-Type": "application/json" },
227
+ });
228
+ }