@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,362 @@
1
+ import { Type } from "typebox";
2
+ import { compile, isJsonObject, OPERATIONS, SdkError } from "@nyte-ai/schema";
3
+ import type { Authority, Json } from "@nyte-ai/schema";
4
+ import type { Connection } from "@nyte-ai/core";
5
+ import { bindConnection } from "./binding.ts";
6
+ import type {
7
+ Binding,
8
+ BindingOptions,
9
+ BindingRequest,
10
+ ByteBinding,
11
+ CallBinding,
12
+ FrameBinding,
13
+ } from "./binding.ts";
14
+ import {
15
+ CANCEL_PATH,
16
+ CHANNEL_HEADER,
17
+ CHANNEL_PATH,
18
+ HOST_HEADER,
19
+ INPUT_HEADER,
20
+ STREAM_HEADER,
21
+ abortable,
22
+ answersBytes,
23
+ answersOnce,
24
+ asSdkError,
25
+ decodeInput,
26
+ encodeError,
27
+ encodeOutput,
28
+ errorStatus,
29
+ mapped,
30
+ namesOperation,
31
+ octetStream,
32
+ outputFrames,
33
+ parseJson,
34
+ readInputHeader,
35
+ sseFrames,
36
+ } from "@nyte-ai/protocol";
37
+ import type { ByteName, CallName, FrameName, OperationName } from "@nyte-ai/protocol";
38
+ import { createStreamChannels } from "./stream-server.ts";
39
+
40
+ /** Describes the handler: how a request authenticates to a connection (with the headers its answer carries, such as a pairing's `X-Nyte-Credential`, §40j), what the binding needs, the body limit and the allowed origins. */
41
+ export type HandlerOptions = BindingOptions & {
42
+ connection(request: Request): Promise<{
43
+ readonly connection: Connection;
44
+ readonly authority: ReadonlySet<Authority>;
45
+ readonly headers?: Readonly<Record<string, string>>;
46
+ }>;
47
+ readonly maxBodyBytes?: number;
48
+ readonly origins?: readonly string[];
49
+ };
50
+
51
+ const MAX_BODY_BYTES = 8 * 1024 * 1024;
52
+
53
+ const UPLOADS: ReadonlySet<string> = new Set([
54
+ "files.write",
55
+ "job.write",
56
+ "files.upload",
57
+ ] satisfies readonly CallName[]);
58
+
59
+ const ALLOWED_HEADERS = `Authorization, Content-Type, ${INPUT_HEADER}, ${CHANNEL_HEADER}, ${STREAM_HEADER}`;
60
+
61
+ const EXPOSED_HEADERS = `X-Nyte-Version, X-Nyte-Mime, X-Nyte-Credential, ${HOST_HEADER}, ${CHANNEL_HEADER}`;
62
+
63
+ const tooLarge = new WeakSet<SdkError>();
64
+
65
+ const cancellations = compile(Type.Object({ channel: Type.String(), stream: Type.String() }));
66
+
67
+ /** Returns the whole protocol as one fetch handler: `POST /<name>` per row (anything else `unknown`), CORS for `origins`, the row's authority over the connection the request authenticates to, and the body limit (413). Calls answer JSON, watches and JSON streams server-sent events, byte rows an octet stream and `job.output` framed octets (§40h, §40w); the request's abort ends every stream. An error answers its encoded `SdkError` at the status `errorStatus` gives its code. */
68
+ export function createHandler(options: HandlerOptions): (request: Request) => Promise<Response> {
69
+ const limit = options.maxBodyBytes ?? MAX_BODY_BYTES;
70
+ const bindings = new WeakMap<Connection, Binding>();
71
+ const channels = createStreamChannels();
72
+
73
+ const bind = (connection: Connection): Binding => {
74
+ const known = bindings.get(connection);
75
+
76
+ if (known !== undefined) return known;
77
+ const created = bindConnection(connection, options);
78
+ bindings.set(connection, created);
79
+
80
+ return created;
81
+ };
82
+
83
+ return async (request) => {
84
+ const cors = corsHeaders(request, options.origins);
85
+
86
+ if (cors === null)
87
+ return errorResponse(new SdkError("denied", "This origin may not call the host."), {});
88
+
89
+ if (request.method === "OPTIONS")
90
+ return new Response(null, {
91
+ status: 204,
92
+ headers: {
93
+ ...cors,
94
+ "Access-Control-Allow-Methods": "POST",
95
+ "Access-Control-Allow-Headers": ALLOWED_HEADERS,
96
+ "Access-Control-Max-Age": "600",
97
+ },
98
+ });
99
+
100
+ try {
101
+ const path = new URL(request.url).pathname.slice(1);
102
+
103
+ const name =
104
+ request.method === "POST" && (path === CHANNEL_PATH || path === CANCEL_PATH)
105
+ ? path
106
+ : readOperation(request);
107
+
108
+ const authenticated = await options.connection(request);
109
+
110
+ if (name === CHANNEL_PATH) {
111
+ const value = await readBody(request, limit);
112
+ const deflate = isJsonObject(value) && value.deflate === true;
113
+
114
+ return withHeaders(channels.open(authenticated.connection, request.signal, deflate), {
115
+ ...cors,
116
+ ...authenticated.headers,
117
+ });
118
+ }
119
+
120
+ if (name === CANCEL_PATH) {
121
+ const value = await readBody(request, limit);
122
+
123
+ if (!cancellations.Check(value))
124
+ throw new SdkError("invalid", "A stream cancellation needs its channel and stream.");
125
+ channels.cancel(authenticated.connection, value.channel, value.stream);
126
+
127
+ return withHeaders(new Response(null, { status: 204 }), {
128
+ ...cors,
129
+ ...authenticated.headers,
130
+ });
131
+ }
132
+
133
+ const needed = OPERATIONS[name].authority;
134
+
135
+ if (!authenticated.authority.has(needed))
136
+ throw new SdkError("denied", `${name} needs ${needed} authority.`);
137
+ const binding = bind(authenticated.connection);
138
+ let response: Response;
139
+ const channel = request.headers.get(CHANNEL_HEADER);
140
+ const stream = request.headers.get(STREAM_HEADER);
141
+
142
+ if (channel !== null || stream !== null) {
143
+ if (channel === null || stream === null)
144
+ throw new SdkError("invalid", "A shared stream needs its channel and identifier.");
145
+ const value = await readBody(request, limit);
146
+
147
+ response = await channels.start(
148
+ authenticated.connection,
149
+ channel,
150
+ stream,
151
+ request.signal,
152
+ (signal) => answer(binding, name, value, request, signal),
153
+ );
154
+ } else response = await serve(binding, name, request, limit);
155
+
156
+ return withHeaders(response, { ...cors, ...authenticated.headers });
157
+ } catch (error) {
158
+ return errorResponse(asSdkError(error), cors);
159
+ }
160
+ };
161
+ }
162
+
163
+ async function serve(
164
+ binding: Binding,
165
+ name: OperationName,
166
+ request: Request,
167
+ limit: number,
168
+ ): Promise<Response> {
169
+ if (UPLOADS.has(name) && answersOnce(name)) {
170
+ const body = limited(request, limit);
171
+
172
+ return json(
173
+ await call(binding, name, readInputHeader(request) ?? {}, { body, signal: request.signal }),
174
+ );
175
+ }
176
+
177
+ return answer(binding, name, await readBody(request, limit), request, request.signal);
178
+ }
179
+
180
+ /** Answers `name` with its input `value`, already read from `request`: a shared stream's exchange ends with its 202, after which the request's body can no longer be read. */
181
+ async function answer(
182
+ binding: Binding,
183
+ name: OperationName,
184
+ value: Json,
185
+ request: Request,
186
+ signal: AbortSignal,
187
+ ): Promise<Response> {
188
+ const context: BindingRequest = { body: null, signal };
189
+
190
+ if (answersOnce(name)) return json(await call(binding, name, value, context));
191
+
192
+ if (name === "job.output")
193
+ return new Response(
194
+ outputFrames(
195
+ abortable(await binding["job.output"](decodeInput(name, value), context), signal),
196
+ ),
197
+ { status: 200, headers: { "Content-Type": "application/octet-stream" } },
198
+ );
199
+
200
+ if (answersBytes(name))
201
+ return octetStream(await bytes(binding, name, value, context), request.signal);
202
+
203
+ return new Response(sseFrames(ending(await frames(binding, name, value, context), signal)), {
204
+ status: 200,
205
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
206
+ });
207
+ }
208
+
209
+ async function call<N extends CallName>(
210
+ binding: CallBinding,
211
+ name: N,
212
+ value: Json,
213
+ request: BindingRequest,
214
+ ): Promise<Json> {
215
+ return encodeOutput(name, await binding[name](decodeInput(name, value), request));
216
+ }
217
+
218
+ async function bytes<N extends ByteName>(
219
+ binding: ByteBinding,
220
+ name: N,
221
+ value: Json,
222
+ request: BindingRequest,
223
+ ) {
224
+ return binding[name](decodeInput(name, value), request);
225
+ }
226
+
227
+ async function frames<N extends FrameName>(
228
+ binding: FrameBinding,
229
+ name: N,
230
+ value: Json,
231
+ request: BindingRequest,
232
+ ): Promise<AsyncIterable<Json>> {
233
+ return mapped(await binding[name](decodeInput(name, value), request), (frame) =>
234
+ encodeOutput(name, frame),
235
+ );
236
+ }
237
+
238
+ function ending<T>(source: AsyncIterable<T>, signal: AbortSignal): AsyncIterable<T> {
239
+ return {
240
+ [Symbol.asyncIterator]() {
241
+ const iterator = abortable(source, signal)[Symbol.asyncIterator]();
242
+
243
+ return {
244
+ async next() {
245
+ const next = await iterator.next();
246
+
247
+ if (next.done === true && signal.reason instanceof SdkError) throw signal.reason;
248
+
249
+ return next;
250
+ },
251
+ async return() {
252
+ await iterator.return?.();
253
+
254
+ return { done: true, value: undefined };
255
+ },
256
+ };
257
+ },
258
+ };
259
+ }
260
+
261
+ async function readBody(request: Request, limit: number): Promise<Json> {
262
+ const stream = limited(request, limit);
263
+
264
+ if (stream === null) return {};
265
+ const chunks: Uint8Array[] = [];
266
+ const reader = stream.getReader();
267
+
268
+ for (;;) {
269
+ const read = await reader.read();
270
+
271
+ if (read.done) break;
272
+ chunks.push(read.value);
273
+ }
274
+
275
+ const decoder = new TextDecoder();
276
+
277
+ const text =
278
+ chunks.map((chunk) => decoder.decode(chunk, { stream: true })).join("") + decoder.decode();
279
+
280
+ return text.trim() === "" ? {} : parseJson(text);
281
+ }
282
+
283
+ function limited(request: Request, limit: number): ReadableStream<Uint8Array> | null {
284
+ const declared = Number(request.headers.get("Content-Length") ?? "0");
285
+
286
+ if (declared > limit) throw largerThan(limit);
287
+
288
+ if (request.body === null) return null;
289
+ let total = 0;
290
+
291
+ return request.body.pipeThrough(
292
+ new TransformStream<Uint8Array, Uint8Array>({
293
+ transform(chunk, controller) {
294
+ total += chunk.byteLength;
295
+
296
+ if (total > limit) controller.error(largerThan(limit));
297
+ else controller.enqueue(chunk);
298
+ },
299
+ }),
300
+ { signal: request.signal },
301
+ );
302
+ }
303
+
304
+ function largerThan(limit: number): SdkError {
305
+ const error = new SdkError("invalid", `The body is larger than ${limit} bytes.`);
306
+ tooLarge.add(error);
307
+
308
+ return error;
309
+ }
310
+
311
+ function corsHeaders(
312
+ request: Request,
313
+ origins: readonly string[] | undefined,
314
+ ): Readonly<Record<string, string>> | null {
315
+ const origin = request.headers.get("Origin");
316
+
317
+ if (origin === null || origin === new URL(request.url).origin) return {};
318
+
319
+ if (!(origins ?? []).includes(origin)) return null;
320
+
321
+ return {
322
+ "Access-Control-Allow-Origin": origin,
323
+ "Access-Control-Expose-Headers": EXPOSED_HEADERS,
324
+ Vary: "Origin",
325
+ };
326
+ }
327
+
328
+ function readOperation(request: Request): OperationName {
329
+ const path = new URL(request.url).pathname;
330
+ const name = path.slice(1);
331
+
332
+ if (request.method !== "POST" || !namesOperation(name))
333
+ throw new SdkError("unknown", `No operation answers ${request.method} ${path}.`);
334
+
335
+ return name;
336
+ }
337
+
338
+ function json(value: Json): Response {
339
+ return new Response(JSON.stringify(value), {
340
+ status: 200,
341
+ headers: { "Content-Type": "application/json" },
342
+ });
343
+ }
344
+
345
+ function errorResponse(failure: SdkError, headers: Readonly<Record<string, string>>): Response {
346
+ return new Response(JSON.stringify(encodeError(failure)), {
347
+ status: tooLarge.has(failure) ? 413 : errorStatus[failure.code],
348
+ headers: { ...headers, "Content-Type": "application/json" },
349
+ });
350
+ }
351
+
352
+ function withHeaders(response: Response, headers: Readonly<Record<string, string>>): Response {
353
+ const merged = new Headers(response.headers);
354
+
355
+ for (const [key, value] of Object.entries(headers)) merged.set(key, value);
356
+
357
+ return new Response(response.body, {
358
+ status: response.status,
359
+ statusText: response.statusText,
360
+ headers: merged,
361
+ });
362
+ }
@@ -0,0 +1,176 @@
1
+ import { deflateSync } from "fflate";
2
+ import { SdkError } from "@nyte-ai/schema";
3
+ import type { Connection } from "@nyte-ai/core";
4
+ import { CHANNEL_HEADER, asSdkError, encodeError, streamBuffer } from "@nyte-ai/protocol";
5
+ import type { StreamFrame } from "@nyte-ai/protocol";
6
+
7
+ const encoder = new TextEncoder();
8
+
9
+ const CHUNK_BYTES = 48 * 1024;
10
+
11
+ /** Smaller chunks compress under ~4x and save too few bytes to pay for deflating and inflating them. */
12
+ const DEFLATE_MIN_BYTES = 16 * 1024;
13
+
14
+ export function createStreamChannels() {
15
+ type Channel = {
16
+ readonly connection: Connection;
17
+ readonly signal: AbortSignal;
18
+ readonly deflate: boolean;
19
+ readonly streams: Map<string, AbortController>;
20
+ readonly push: (frame: StreamFrame, signal: AbortSignal) => Promise<void>;
21
+ };
22
+
23
+ const channels = new Map<string, Channel>();
24
+
25
+ const owned = (id: string, connection: Connection): Channel => {
26
+ const channel = channels.get(id);
27
+
28
+ if (channel === undefined) throw new SdkError("unavailable", "The stream channel is closed.");
29
+
30
+ if (channel.connection !== connection)
31
+ throw new SdkError("denied", "The stream channel belongs to another connection.");
32
+
33
+ return channel;
34
+ };
35
+
36
+ return {
37
+ open(connection: Connection, signal: AbortSignal, deflate: boolean): Response {
38
+ const id = crypto.randomUUID();
39
+ const controller = new AbortController();
40
+ const streams = new Map<string, AbortController>();
41
+ let timer: ReturnType<typeof setInterval> | undefined;
42
+
43
+ const finish = (): void => {
44
+ if (controller.signal.aborted) return;
45
+ controller.abort();
46
+ channels.delete(id);
47
+ clearInterval(timer);
48
+ signal.removeEventListener("abort", aborted);
49
+
50
+ for (const stream of streams.values()) stream.abort();
51
+ streams.clear();
52
+ };
53
+
54
+ const buffer = streamBuffer(finish);
55
+
56
+ const fail = (error: SdkError): void => {
57
+ if (controller.signal.aborted) return;
58
+ buffer.end(encoder.encode(`event: error\ndata: ${JSON.stringify(encodeError(error))}\n\n`));
59
+ finish();
60
+ };
61
+
62
+ const aborted = (): void => {
63
+ fail(
64
+ signal.reason instanceof SdkError
65
+ ? signal.reason
66
+ : new SdkError("unavailable", "The stream channel disconnected."),
67
+ );
68
+ };
69
+
70
+ channels.set(id, {
71
+ connection,
72
+ signal: controller.signal,
73
+ deflate,
74
+ streams,
75
+ push: (frame, signal) =>
76
+ buffer.write(encoder.encode(`data: ${JSON.stringify(frame)}\n\n`), signal),
77
+ });
78
+ buffer.push(encoder.encode(": connected\n\n"));
79
+ timer = setInterval(() => {
80
+ buffer.push(encoder.encode(": ping\n\n"));
81
+ }, 15_000);
82
+
83
+ if (signal.aborted) aborted();
84
+ else signal.addEventListener("abort", aborted, { once: true });
85
+
86
+ return new Response(buffer.body, {
87
+ headers: {
88
+ "Content-Type": "text/event-stream",
89
+ "Cache-Control": "no-cache",
90
+ [CHANNEL_HEADER]: id,
91
+ },
92
+ });
93
+ },
94
+ async start(
95
+ connection: Connection,
96
+ channelId: string,
97
+ streamId: string,
98
+ request: AbortSignal,
99
+ serve: (signal: AbortSignal) => Promise<Response>,
100
+ ): Promise<Response> {
101
+ const channel = owned(channelId, connection);
102
+
103
+ if (channel.streams.has(streamId))
104
+ throw new SdkError("invalid", "The stream identifier is already in use.");
105
+ const controller = new AbortController();
106
+ channel.streams.set(streamId, controller);
107
+ const abortSetup = (): void => controller.abort(request.reason);
108
+ request.addEventListener("abort", abortSetup, { once: true });
109
+ const signal = AbortSignal.any([controller.signal, channel.signal]);
110
+ void (async () => {
111
+ let reader: Pick<ReadableStreamDefaultReader<Uint8Array>, "read" | "cancel"> | undefined;
112
+
113
+ const cancel = (): void => {
114
+ void reader?.cancel().catch(() => undefined);
115
+ };
116
+
117
+ signal.addEventListener("abort", cancel, { once: true });
118
+
119
+ try {
120
+ if (request.aborted) abortSetup();
121
+ signal.throwIfAborted();
122
+ const response = await serve(signal);
123
+ reader = response.body?.getReader();
124
+ request.removeEventListener("abort", abortSetup);
125
+
126
+ if (signal.aborted) return;
127
+
128
+ if (reader !== undefined)
129
+ for (;;) {
130
+ const next = await reader.read();
131
+
132
+ if (signal.aborted || next.done) break;
133
+
134
+ for (let start = 0; start < next.value.byteLength; start += CHUNK_BYTES) {
135
+ if (signal.aborted) break;
136
+ const chunk = next.value.subarray(start, start + CHUNK_BYTES);
137
+ await channel.push(
138
+ channel.deflate && chunk.byteLength >= DEFLATE_MIN_BYTES
139
+ ? {
140
+ kind: "deflated",
141
+ stream: streamId,
142
+ data: deflateSync(chunk, { level: 1 }).toBase64(),
143
+ }
144
+ : { kind: "chunk", stream: streamId, data: chunk.toBase64() },
145
+ signal,
146
+ );
147
+ }
148
+ }
149
+
150
+ if (!signal.aborted) await channel.push({ kind: "end", stream: streamId }, signal);
151
+ } catch (error) {
152
+ if (!signal.aborted)
153
+ await channel.push(
154
+ {
155
+ kind: "error",
156
+ stream: streamId,
157
+ error: JSON.stringify(encodeError(asSdkError(error))),
158
+ },
159
+ signal,
160
+ );
161
+ } finally {
162
+ request.removeEventListener("abort", abortSetup);
163
+ signal.removeEventListener("abort", cancel);
164
+ channel.streams.delete(streamId);
165
+ controller.abort();
166
+ cancel();
167
+ }
168
+ })();
169
+
170
+ return new Response(null, { status: 202 });
171
+ },
172
+ cancel(connection: Connection, channelId: string, streamId: string): void {
173
+ owned(channelId, connection).streams.get(streamId)?.abort();
174
+ },
175
+ };
176
+ }
@@ -0,0 +1,99 @@
1
+ import { projectSessionIndex, sessionIndexRef } from "@nyte-ai/core/store";
2
+ import type { SessionIndex } from "@nyte-ai/core";
3
+ import { asId } from "@nyte-ai/schema";
4
+ import type { Json, Revision } from "@nyte-ai/schema";
5
+ import { parseRef, parseText } from "./store-rows.ts";
6
+
7
+ type Binding = string | number | null;
8
+
9
+ interface Statement {
10
+ readonly text: string;
11
+ readonly values: readonly Binding[];
12
+ }
13
+
14
+ export function sessionIndexChanges(
15
+ name: string,
16
+ object: Json | null,
17
+ revision: Revision,
18
+ ): readonly Statement[] {
19
+ const ref = sessionIndexRef(name);
20
+
21
+ if (ref === null) return [];
22
+ const entry = projectSessionIndex(name, object);
23
+
24
+ if (ref.kind === "session") {
25
+ return entry?.kind === "session"
26
+ ? [
27
+ {
28
+ text: "INSERT INTO session_index (name, session, parent, sort_at, version) VALUES (?, ?, ?, ?, ?) ON CONFLICT (name) DO UPDATE SET session = excluded.session, parent = excluded.parent, sort_at = excluded.sort_at, version = excluded.version",
29
+ values: [name, entry.session, entry.parent, -entry.at, revision],
30
+ },
31
+ ]
32
+ : [{ text: "DELETE FROM session_index WHERE name = ?", values: [name] }];
33
+ }
34
+
35
+ return entry !== null && entry.kind !== "session"
36
+ ? [
37
+ {
38
+ text: "INSERT INTO pending_work (name, session, kind) VALUES (?, ?, ?) ON CONFLICT (name) DO UPDATE SET session = excluded.session, kind = excluded.kind",
39
+ values: [name, entry.session, entry.kind],
40
+ },
41
+ ]
42
+ : [{ text: "DELETE FROM pending_work WHERE name = ?", values: [name] }];
43
+ }
44
+
45
+ export function sqlSessionIndex(
46
+ read: (text: string, values: readonly Binding[]) => Promise<readonly Record<string, unknown>[]>,
47
+ ): SessionIndex {
48
+ return {
49
+ async list({ parents, after, limit }) {
50
+ const conditions: string[] = [];
51
+ const values: Binding[] = [];
52
+
53
+ if (parents.kind === "roots") conditions.push("s.parent IS NULL");
54
+ if (parents.kind === "children") {
55
+ conditions.push("s.parent = ?");
56
+ values.push(parents.session);
57
+ }
58
+ if (after !== undefined) {
59
+ conditions.push("(s.sort_at, s.session) > (?, ?)");
60
+ values.push(-after.at, after.id);
61
+ }
62
+ values.push(limit);
63
+
64
+ return (
65
+ await read(
66
+ `SELECT r.name, r.target, r.revision, s.version FROM session_index s JOIN refs r ON r.name = s.name${conditions.length === 0 ? "" : ` WHERE ${conditions.join(" AND ")}`} ORDER BY s.sort_at, s.session LIMIT ?`,
67
+ values,
68
+ )
69
+ ).map((row) => ({
70
+ ref: parseRef(row),
71
+ version: asId<"revision">(parseText(row.version) ?? ""),
72
+ }));
73
+ },
74
+ async work({ after, limit }) {
75
+ return (
76
+ await read(
77
+ `SELECT r.name, r.target, r.revision FROM (SELECT DISTINCT session FROM pending_work${after === undefined ? "" : " WHERE session > ?"}) p JOIN session_index s ON s.session = p.session JOIN refs r ON r.name = s.name ORDER BY p.session LIMIT ?`,
78
+ after === undefined ? [limit] : [after, limit],
79
+ )
80
+ ).map(parseRef);
81
+ },
82
+ async pending({ kind, sessions }) {
83
+ if (sessions?.length === 0) return [];
84
+
85
+ return (
86
+ await read(
87
+ `SELECT r.name, r.target, r.revision FROM pending_work p JOIN refs r ON r.name = p.name WHERE p.kind = ?${sessions === undefined ? "" : ` AND p.session IN (${sessions.map(() => "?").join(", ")})`} ORDER BY p.name`,
88
+ [kind, ...(sessions ?? [])],
89
+ )
90
+ ).map(parseRef);
91
+ },
92
+ };
93
+ }
94
+
95
+ export function postgresSessionIndexQuery(text: string): string {
96
+ let parameter = 0;
97
+
98
+ return text.replaceAll("?", () => `$${++parameter}`);
99
+ }