@nyte-ai/host 0.0.0-stage → 0.0.18

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/THIRD-PARTY-NOTICES.md +800 -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-0nrnbt76.js +497 -0
  7. package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
  8. package/dist/chunks/canvas-preview-1ta3avah.js +56 -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-5hxat7hh.js +239 -0
  15. package/dist/chunks/canvas-preview-5mex725c.js +14 -0
  16. package/dist/chunks/canvas-preview-5z63am63.js +295 -0
  17. package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
  18. package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
  19. package/dist/chunks/canvas-preview-75db60xy.js +457 -0
  20. package/dist/chunks/canvas-preview-76fwvd6e.js +272 -0
  21. package/dist/chunks/canvas-preview-8xgt67ka.js +598 -0
  22. package/dist/chunks/canvas-preview-98aj5p34.js +176 -0
  23. package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
  24. package/dist/chunks/canvas-preview-a3yw0j70.js +72 -0
  25. package/dist/chunks/canvas-preview-azg56359.js +68 -0
  26. package/dist/chunks/canvas-preview-d9wwhbtz.js +240 -0
  27. package/dist/chunks/canvas-preview-ehf3vp31.js +411 -0
  28. package/dist/chunks/canvas-preview-evh92ywd.js +369 -0
  29. package/dist/chunks/canvas-preview-ewgspmsd.js +81 -0
  30. package/dist/chunks/canvas-preview-g3qj3y3k.js +156 -0
  31. package/dist/chunks/canvas-preview-h8hshnff.js +63 -0
  32. package/dist/chunks/canvas-preview-j1tnf51w.js +36 -0
  33. package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
  34. package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
  35. package/dist/chunks/canvas-preview-m02v4yy6.js +386 -0
  36. package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
  37. package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
  38. package/dist/chunks/canvas-preview-rp6fz0w5.js +130 -0
  39. package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
  40. package/dist/chunks/canvas-preview-senpjwep.js +518 -0
  41. package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
  42. package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
  43. package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
  44. package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
  45. package/dist/chunks/canvas-preview-vax9ne4e.js +341 -0
  46. package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
  47. package/dist/chunks/canvas-preview-xn10d4f8.js +608 -0
  48. package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
  49. package/dist/chunks/canvas-preview-ymgz0937.js +587 -0
  50. package/dist/chunks/canvas-preview-yrfz2ak6.js +134 -0
  51. package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
  52. package/dist/chunks/canvas-preview-zn2d0k9d.js +133 -0
  53. package/dist/src/canvas-preview.d.ts +29 -0
  54. package/dist/src/canvas-preview.js +267 -0
  55. package/dist/src/credentials.d.ts +44 -0
  56. package/dist/src/credentials.js +10 -0
  57. package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
  58. package/dist/src/deploy/cloudflare/worker.js +196 -0
  59. package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
  60. package/dist/src/deploy/vercel/nitro.config.js +30 -0
  61. package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
  62. package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
  63. package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
  64. package/dist/src/deploy/vercel/server/utils/host.js +7 -0
  65. package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
  66. package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
  67. package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
  68. package/dist/src/deploy/vercel/server/utils/step.js +6 -0
  69. package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
  70. package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
  71. package/dist/src/durable-object.d.ts +14 -0
  72. package/dist/src/durable-object.js +231 -0
  73. package/dist/src/hash.d.ts +1 -0
  74. package/dist/src/hash.js +6 -0
  75. package/dist/src/identity-key.d.ts +11 -0
  76. package/dist/src/identity-key.js +9 -0
  77. package/dist/src/identity.d.ts +12 -0
  78. package/dist/src/identity.js +6 -0
  79. package/dist/src/index.d.ts +21 -0
  80. package/dist/src/index.js +120 -0
  81. package/dist/src/key-file.d.ts +1 -0
  82. package/dist/src/key-file.js +7 -0
  83. package/dist/src/listen-fetch.d.ts +8 -0
  84. package/dist/src/listen-fetch.js +9 -0
  85. package/dist/src/listen.d.ts +4 -0
  86. package/dist/src/listen.js +10 -0
  87. package/dist/src/local/command.d.ts +15 -0
  88. package/dist/src/local/command.js +8 -0
  89. package/dist/src/local/feed.d.ts +7 -0
  90. package/dist/src/local/feed.js +6 -0
  91. package/dist/src/local/files.d.ts +18 -0
  92. package/dist/src/local/files.js +13 -0
  93. package/dist/src/local/git-watch.d.ts +15 -0
  94. package/dist/src/local/git-watch.js +7 -0
  95. package/dist/src/local/git.d.ts +5 -0
  96. package/dist/src/local/git.js +13 -0
  97. package/dist/src/local/github.d.ts +30 -0
  98. package/dist/src/local/github.js +9 -0
  99. package/dist/src/local/identity.d.ts +3 -0
  100. package/dist/src/local/identity.js +7 -0
  101. package/dist/src/local/jobs.d.ts +7 -0
  102. package/dist/src/local/jobs.js +6 -0
  103. package/dist/src/local/legacy-trust.d.ts +10 -0
  104. package/dist/src/local/legacy-trust.js +6 -0
  105. package/dist/src/local/patch.d.ts +16 -0
  106. package/dist/src/local/patch.js +17 -0
  107. package/dist/src/local/provider.d.ts +11 -0
  108. package/dist/src/local/provider.js +20 -0
  109. package/dist/src/local/snapshots.d.ts +7 -0
  110. package/dist/src/local/snapshots.js +11 -0
  111. package/dist/src/local/text.d.ts +1 -0
  112. package/dist/src/local/text.js +6 -0
  113. package/dist/src/local/tree.d.ts +50 -0
  114. package/dist/src/local/tree.js +16 -0
  115. package/dist/src/local/workspace-migration.d.ts +2 -0
  116. package/dist/src/local/workspace-migration.js +7 -0
  117. package/dist/src/local/worktrees.d.ts +46 -0
  118. package/dist/src/local/worktrees.js +14 -0
  119. package/dist/src/mcp/config.d.ts +75 -0
  120. package/dist/src/mcp/config.js +12 -0
  121. package/dist/src/mcp/connect.d.ts +28 -0
  122. package/dist/src/mcp/connect.js +9 -0
  123. package/dist/src/mcp/hub.d.ts +19 -0
  124. package/dist/src/mcp/hub.js +11 -0
  125. package/dist/src/mcp/oauth.d.ts +46 -0
  126. package/dist/src/mcp/oauth.js +12 -0
  127. package/dist/src/mcp/tools.d.ts +29 -0
  128. package/dist/src/mcp/tools.js +12 -0
  129. package/dist/src/open.d.ts +42 -0
  130. package/dist/src/open.js +44 -0
  131. package/dist/src/plugins/loader.d.ts +21 -0
  132. package/dist/src/plugins/loader.js +11 -0
  133. package/dist/src/postgres.d.ts +19 -0
  134. package/dist/src/postgres.js +8 -0
  135. package/dist/src/providers.d.ts +16 -0
  136. package/dist/src/providers.js +112 -0
  137. package/dist/src/secrets.d.ts +12 -0
  138. package/dist/src/secrets.js +8 -0
  139. package/dist/src/serve/binding.d.ts +39 -0
  140. package/dist/src/serve/binding.js +6 -0
  141. package/dist/src/serve/handler.d.ts +15 -0
  142. package/dist/src/serve/handler.js +8 -0
  143. package/dist/src/serve/stream-server.d.ts +6 -0
  144. package/dist/src/serve/stream-server.js +6 -0
  145. package/dist/src/session-index.d.ts +11 -0
  146. package/dist/src/session-index.js +11 -0
  147. package/dist/src/shell-env.d.ts +14 -0
  148. package/dist/src/shell-env.js +108 -0
  149. package/dist/src/sqlite-workers.d.ts +6 -0
  150. package/dist/src/sqlite-workers.js +6 -0
  151. package/dist/src/sqlite.d.ts +7 -0
  152. package/dist/src/sqlite.js +10 -0
  153. package/dist/src/store-owner.d.ts +15 -0
  154. package/dist/src/store-owner.js +9 -0
  155. package/dist/src/store-rows.d.ts +13 -0
  156. package/dist/src/store-rows.js +26 -0
  157. package/dist/src/store-usage.d.ts +25 -0
  158. package/dist/src/store-usage.js +16 -0
  159. package/dist/src/usage-scan.d.ts +24 -0
  160. package/dist/src/usage-scan.js +65 -0
  161. package/dist/src/usage-service.d.ts +22 -0
  162. package/dist/src/usage-service.js +12 -0
  163. package/dist/src/usage-summary.d.ts +9 -0
  164. package/dist/src/usage-summary.js +16 -0
  165. package/dist/src/usage-types.d.ts +86 -0
  166. package/dist/src/usage-types.js +0 -0
  167. package/dist/src/usage-worker.d.ts +1 -0
  168. package/dist/src/usage-worker.js +25 -0
  169. package/dist/src/usage.d.ts +72 -0
  170. package/dist/src/usage.js +13 -0
  171. package/package.json +132 -3
  172. package/src/canvas-preview.ts +402 -0
  173. package/src/credentials.ts +294 -0
  174. package/src/deploy/cloudflare/README.md +3 -0
  175. package/src/deploy/cloudflare/worker.ts +306 -0
  176. package/src/deploy/cloudflare/wrangler.jsonc +12 -0
  177. package/src/deploy/vercel/README.md +3 -0
  178. package/src/deploy/vercel/nitro.config.ts +29 -0
  179. package/src/deploy/vercel/server/routes/[...].ts +10 -0
  180. package/src/deploy/vercel/server/utils/host.ts +104 -0
  181. package/src/deploy/vercel/server/utils/repair.ts +35 -0
  182. package/src/deploy/vercel/server/utils/step.ts +25 -0
  183. package/src/deploy/vercel/server/workflows/drive.ts +46 -0
  184. package/src/deploy/vercel/tsconfig.json +9 -0
  185. package/src/deploy/vercel/types/bun.d.ts +2 -0
  186. package/src/durable-object.ts +386 -0
  187. package/src/hash.ts +5 -0
  188. package/src/identity-key.ts +20 -0
  189. package/src/identity.ts +66 -0
  190. package/src/index.ts +21 -0
  191. package/src/key-file.ts +53 -0
  192. package/src/listen-fetch.ts +409 -0
  193. package/src/listen.ts +228 -0
  194. package/src/local/command.ts +109 -0
  195. package/src/local/feed.ts +70 -0
  196. package/src/local/files.ts +812 -0
  197. package/src/local/git-watch.ts +189 -0
  198. package/src/local/git.ts +914 -0
  199. package/src/local/github.ts +692 -0
  200. package/src/local/identity.ts +21 -0
  201. package/src/local/jobs.ts +587 -0
  202. package/src/local/legacy-trust.ts +105 -0
  203. package/src/local/patch.ts +334 -0
  204. package/src/local/provider.ts +184 -0
  205. package/src/local/snapshots.ts +776 -0
  206. package/src/local/text.ts +9 -0
  207. package/src/local/tree.ts +398 -0
  208. package/src/local/workspace-migration.ts +95 -0
  209. package/src/local/worktrees.ts +239 -0
  210. package/src/mcp/config.ts +330 -0
  211. package/src/mcp/connect.ts +105 -0
  212. package/src/mcp/hub.ts +836 -0
  213. package/src/mcp/oauth.ts +436 -0
  214. package/src/mcp/tools.ts +333 -0
  215. package/src/open.ts +469 -0
  216. package/src/plugins/loader.ts +186 -0
  217. package/src/postgres.ts +492 -0
  218. package/src/providers.ts +135 -0
  219. package/src/secrets.ts +31 -0
  220. package/src/serve/binding.ts +628 -0
  221. package/src/serve/handler.ts +362 -0
  222. package/src/serve/stream-server.ts +176 -0
  223. package/src/session-index.ts +99 -0
  224. package/src/shell-env.ts +153 -0
  225. package/src/sqlite-workers.ts +283 -0
  226. package/src/sqlite.ts +528 -0
  227. package/src/store-owner.ts +98 -0
  228. package/src/store-rows.ts +110 -0
  229. package/src/store-usage.ts +344 -0
  230. package/src/usage-scan.ts +81 -0
  231. package/src/usage-service.ts +73 -0
  232. package/src/usage-summary.ts +79 -0
  233. package/src/usage-types.ts +87 -0
  234. package/src/usage-worker.ts +24 -0
  235. package/src/usage.ts +701 -0
@@ -0,0 +1,402 @@
1
+ /**
2
+ * The canvas previewer for a host without Electron: a headless Chromium
3
+ * already on this machine, driven over the DevTools protocol on a pipe, so
4
+ * nothing new is installed and no debugging port is opened. Each preview
5
+ * starts its own browser with a throwaway profile and stops it after. The
6
+ * page loads the shell directory the chat serves, from a made-up https
7
+ * origin the previewer answers itself under the chat's shell policy and
8
+ * frame sandbox; every other request is refused, no host name resolves,
9
+ * WebRTC may only go through a proxy that leads nowhere, and permission
10
+ * prompts are denied, so the canvas stays as sealed as in the chat.
11
+ */
12
+ import { spawn } from "node:child_process";
13
+ import type { ChildProcess } from "node:child_process";
14
+ import { existsSync, readdirSync } from "node:fs";
15
+ import { mkdtemp, rm } from "node:fs/promises";
16
+ import { homedir, tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import process from "node:process";
19
+ import { Readable, Writable } from "node:stream";
20
+ import { runCanvasPreview } from "@nyte-ai/plugin/core/canvas-preview";
21
+ import type {
22
+ CanvasPreviewOutcome,
23
+ CanvasPreviewPage,
24
+ CanvasPreviewRequest,
25
+ CanvasPreviewer,
26
+ } from "@nyte-ai/plugin/core/canvas-preview";
27
+ import { Type } from "typebox";
28
+ import type { TSchema } from "typebox";
29
+ import { Value } from "typebox/value";
30
+
31
+ const ORIGIN = "https://canvas.preview/";
32
+
33
+ /**
34
+ * Returns the shell a previewer serves: every file of the canvas package's
35
+ * `dist/shell`, base64 by its path from it (`index.html`,
36
+ * `libraries/three.js`), as `@nyte-ai/canvas/shell-files` exports them.
37
+ *
38
+ * @category host
39
+ * @since 0.1.0
40
+ */
41
+ export type CanvasShellFiles = Readonly<Record<string, string>>;
42
+
43
+ /** The policy the chat's clients serve the shell's page under, plus the sandbox its frame adds there, so the top-level page gets the frame's opaque origin. */
44
+ const SHELL_POLICY =
45
+ "default-src 'none'; script-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src data: blob:; font-src 'self'; connect-src 'none'; base-uri 'none'; form-action 'none'; sandbox allow-scripts";
46
+
47
+ const SHELL_TYPES = new Map([
48
+ ["html", "text/html; charset=utf-8"],
49
+ ["js", "text/javascript; charset=utf-8"],
50
+ ["css", "text/css; charset=utf-8"],
51
+ ["woff2", "font/woff2"],
52
+ ]);
53
+
54
+ /** The newest browser of `prefix` in Playwright's download cache, which the canvas package's own checks install. */
55
+ function playwrightBrowser(prefix: string, executables: readonly string[]): string | undefined {
56
+ const cache =
57
+ process.env["PLAYWRIGHT_BROWSERS_PATH"] ??
58
+ (process.platform === "darwin"
59
+ ? join(homedir(), "Library", "Caches", "ms-playwright")
60
+ : process.platform === "win32"
61
+ ? join(process.env["LOCALAPPDATA"] ?? homedir(), "ms-playwright")
62
+ : join(homedir(), ".cache", "ms-playwright"));
63
+
64
+ if (!existsSync(cache)) return undefined;
65
+ const versions = readdirSync(cache)
66
+ .filter((name) => name.startsWith(`${prefix}-`))
67
+ .toSorted(
68
+ (left, right) =>
69
+ Number(right.slice(prefix.length + 1)) - Number(left.slice(prefix.length + 1)),
70
+ );
71
+
72
+ return versions
73
+ .flatMap((version) => executables.map((executable) => join(cache, version, executable)))
74
+ .find((path) => existsSync(path));
75
+ }
76
+
77
+ /**
78
+ * Finds a Chromium to preview with: `NYTE_CANVAS_CHROMIUM`, then
79
+ * Playwright's headless shell or Chromium, then an installed Chrome or
80
+ * Chromium. Undefined when there is none.
81
+ *
82
+ * @category host
83
+ * @since 0.1.0
84
+ */
85
+ export function findChromium(): string | undefined {
86
+ const named = process.env["NYTE_CANVAS_CHROMIUM"];
87
+
88
+ if (named !== undefined && named !== "") return existsSync(named) ? named : undefined;
89
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
90
+
91
+ return (
92
+ playwrightBrowser("chromium_headless_shell", [
93
+ `chrome-headless-shell-mac-${arch}/chrome-headless-shell`,
94
+ "chrome-headless-shell-linux64/chrome-headless-shell",
95
+ "chrome-headless-shell-win64/chrome-headless-shell.exe",
96
+ ]) ??
97
+ playwrightBrowser("chromium", [
98
+ `chrome-mac-${arch}/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing`,
99
+ "chrome-mac/Chromium.app/Contents/MacOS/Chromium",
100
+ "chrome-linux64/chrome",
101
+ "chrome-linux/chrome",
102
+ "chrome-win64/chrome.exe",
103
+ ]) ??
104
+ [
105
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
106
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
107
+ "/usr/bin/google-chrome",
108
+ "/usr/bin/google-chrome-stable",
109
+ "/usr/bin/chromium",
110
+ "/usr/bin/chromium-browser",
111
+ join(
112
+ process.env["PROGRAMFILES"] ?? "C:\\Program Files",
113
+ "Google/Chrome/Application/chrome.exe",
114
+ ),
115
+ ].find((path) => existsSync(path))
116
+ );
117
+ }
118
+
119
+ const Reply = Type.Object({
120
+ id: Type.Number(),
121
+ result: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
122
+ error: Type.Optional(Type.Object({ message: Type.String() })),
123
+ });
124
+
125
+ const Event = Type.Object({
126
+ method: Type.String(),
127
+ params: Type.Record(Type.String(), Type.Unknown()),
128
+ sessionId: Type.Optional(Type.String()),
129
+ });
130
+
131
+ const Paused = Type.Object({
132
+ requestId: Type.String(),
133
+ request: Type.Object({ url: Type.String() }),
134
+ });
135
+
136
+ function read<S extends TSchema>(schema: S, value: unknown, what: string) {
137
+ if (!Value.Check(schema, value)) throw new Error(`Chromium answered ${what} unreadably.`);
138
+
139
+ return value;
140
+ }
141
+
142
+ /** One DevTools connection over Chromium's pipe: NUL-terminated JSON both ways. */
143
+ class Pipe {
144
+ readonly #write: Writable;
145
+ readonly #waiting = new Map<
146
+ number,
147
+ { resolve(value: unknown): void; reject(error: Error): void }
148
+ >();
149
+ readonly #listeners = new Set<(method: string, params: Record<string, unknown>) => void>();
150
+ #next = 0;
151
+ #buffer = "";
152
+ #closed: Error | undefined;
153
+
154
+ constructor(write: Writable, readable: Readable) {
155
+ this.#write = write;
156
+ readable.setEncoding("utf8");
157
+ readable.on("data", (chunk: string) => this.#receive(chunk));
158
+ readable.on("close", () => this.close(new Error("Chromium closed its DevTools pipe.")));
159
+ write.on("error", (error: Error) => this.close(error));
160
+ }
161
+
162
+ send(method: string, params: Record<string, unknown> = {}, sessionId?: string): Promise<unknown> {
163
+ if (this.#closed !== undefined) return Promise.reject(this.#closed);
164
+ this.#next += 1;
165
+ const id = this.#next;
166
+
167
+ return new Promise((resolve, reject) => {
168
+ this.#waiting.set(id, { resolve, reject });
169
+ this.#write.write(`${JSON.stringify({ id, method, params, sessionId })}\0`);
170
+ });
171
+ }
172
+
173
+ listen(listener: (method: string, params: Record<string, unknown>) => void): void {
174
+ this.#listeners.add(listener);
175
+ }
176
+
177
+ close(error: Error): void {
178
+ this.#closed ??= error;
179
+
180
+ for (const waiting of this.#waiting.values()) waiting.reject(error);
181
+ this.#waiting.clear();
182
+ }
183
+
184
+ #receive(chunk: string): void {
185
+ this.#buffer += chunk;
186
+ const messages = this.#buffer.split("\0");
187
+ this.#buffer = messages.pop() ?? "";
188
+
189
+ for (const text of messages) {
190
+ const message: unknown = JSON.parse(text);
191
+
192
+ if (Value.Check(Reply, message)) {
193
+ const waiting = this.#waiting.get(message.id);
194
+ this.#waiting.delete(message.id);
195
+
196
+ if (message.error === undefined) waiting?.resolve(message.result ?? {});
197
+ else waiting?.reject(new Error(message.error.message));
198
+ } else if (Value.Check(Event, message))
199
+ for (const listener of this.#listeners) listener(message.method, message.params);
200
+ }
201
+ }
202
+ }
203
+
204
+ /** Answers the page's requests: the shell's files from memory, nothing else at all. */
205
+ function serve(pipe: Pipe, shell: CanvasShellFiles, sessionId: string): void {
206
+ pipe.listen((method, params) => {
207
+ if (method !== "Fetch.requestPaused" || !Value.Check(Paused, params)) return;
208
+ const { requestId, request } = params;
209
+ const requested = request.url.startsWith(ORIGIN)
210
+ ? request.url.slice(ORIGIN.length).split(/[?#]/u)[0]
211
+ : undefined;
212
+ const path = requested === "" ? "index.html" : requested;
213
+ const body = path !== undefined && Object.hasOwn(shell, path) ? shell[path] : undefined;
214
+ const type = SHELL_TYPES.get(path?.split(".").at(-1) ?? "");
215
+
216
+ if (path === undefined || body === undefined || type === undefined) {
217
+ void pipe
218
+ .send("Fetch.failRequest", { requestId, errorReason: "BlockedByClient" }, sessionId)
219
+ .catch(() => undefined);
220
+
221
+ return;
222
+ }
223
+
224
+ void pipe
225
+ .send(
226
+ "Fetch.fulfillRequest",
227
+ {
228
+ requestId,
229
+ responseCode: 200,
230
+ responseHeaders: [
231
+ { name: "Content-Type", value: type },
232
+ { name: "Access-Control-Allow-Origin", value: "*" },
233
+ ...(path === "index.html"
234
+ ? [{ name: "Content-Security-Policy", value: SHELL_POLICY }]
235
+ : []),
236
+ ],
237
+ body,
238
+ },
239
+ sessionId,
240
+ )
241
+ .catch(() => undefined);
242
+ });
243
+ }
244
+
245
+ async function previewIn(
246
+ pipe: Pipe,
247
+ shell: CanvasShellFiles,
248
+ request: CanvasPreviewRequest,
249
+ ): Promise<CanvasPreviewOutcome> {
250
+ const target = read(
251
+ Type.Object({ targetId: Type.String() }),
252
+ await pipe.send("Target.createTarget", { url: "about:blank" }),
253
+ "a new page",
254
+ );
255
+ const { sessionId } = read(
256
+ Type.Object({ sessionId: Type.String() }),
257
+ await pipe.send("Target.attachToTarget", { targetId: target.targetId, flatten: true }),
258
+ "an attach",
259
+ );
260
+ const send = (method: string, params: Record<string, unknown> = {}) =>
261
+ pipe.send(method, params, sessionId);
262
+ serve(pipe, shell, sessionId);
263
+ const loaded = new Promise<void>((resolve) =>
264
+ pipe.listen((method) => {
265
+ if (method === "Page.loadEventFired") resolve();
266
+ }),
267
+ );
268
+ await send("Fetch.enable", { patterns: [{ urlPattern: "*" }] });
269
+ await send("Page.enable");
270
+ await send("Page.navigate", { url: ORIGIN });
271
+ await loaded;
272
+
273
+ const page: CanvasPreviewPage = {
274
+ async evaluate(expression) {
275
+ const answer = read(
276
+ Type.Object({
277
+ result: Type.Object({ value: Type.Optional(Type.Unknown()) }),
278
+ exceptionDetails: Type.Optional(Type.Object({ text: Type.String() })),
279
+ }),
280
+ await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true }),
281
+ "an evaluation",
282
+ );
283
+
284
+ if (answer.exceptionDetails !== undefined)
285
+ throw new Error(`The preview page threw: ${answer.exceptionDetails.text}`);
286
+
287
+ return answer.result.value;
288
+ },
289
+ async resize(width, height) {
290
+ await send("Emulation.setDeviceMetricsOverride", {
291
+ width,
292
+ height,
293
+ deviceScaleFactor: 1,
294
+ mobile: false,
295
+ });
296
+ },
297
+ async capture() {
298
+ const { data } = read(
299
+ Type.Object({ data: Type.String() }),
300
+ await send("Page.captureScreenshot", { format: "png" }),
301
+ "a screenshot",
302
+ );
303
+
304
+ return new Uint8Array(Buffer.from(data, "base64"));
305
+ },
306
+ };
307
+
308
+ return { kind: "rendered", report: await runCanvasPreview(page, request) };
309
+ }
310
+
311
+ function stop(child: ChildProcess): Promise<void> {
312
+ if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
313
+
314
+ return new Promise((resolve) => {
315
+ const kill = setTimeout(() => child.kill("SIGKILL"), 2000);
316
+ child.once("exit", () => {
317
+ clearTimeout(kill);
318
+ resolve();
319
+ });
320
+ child.kill();
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Returns a previewer over a headless Chromium on this machine
326
+ * (`findChromium`), serving `shell`; it answers `unavailable` when there is
327
+ * no Chromium or it fails to start, and stops the browser when the
328
+ * preview's signal aborts.
329
+ *
330
+ * @category host
331
+ * @since 0.1.0
332
+ */
333
+ export function chromiumCanvasPreview(
334
+ shell: CanvasShellFiles,
335
+ executable: () => string | undefined = findChromium,
336
+ ): CanvasPreviewer {
337
+ return async (request, signal) => {
338
+ const path = executable();
339
+
340
+ if (path === undefined)
341
+ return {
342
+ kind: "unavailable",
343
+ reason: "No Chrome or Chromium was found on this machine.",
344
+ };
345
+ const profile = await mkdtemp(join(tmpdir(), "nyte-canvas-preview-"));
346
+ const child = spawn(
347
+ path,
348
+ [
349
+ "--headless",
350
+ "--remote-debugging-pipe",
351
+ `--user-data-dir=${profile}`,
352
+ "--no-first-run",
353
+ "--no-default-browser-check",
354
+ "--disable-extensions",
355
+ "--disable-background-networking",
356
+ "--disable-sync",
357
+ "--mute-audio",
358
+ "--deny-permission-prompts",
359
+ "--force-webrtc-ip-handling-policy=disable_non_proxied_udp",
360
+ "--webrtc-ip-handling-policy=disable_non_proxied_udp",
361
+ "--proxy-server=socks5://127.0.0.1:9",
362
+ "--proxy-bypass-list=<-loopback>",
363
+ "--host-resolver-rules=MAP * ~NOTFOUND",
364
+ "--hide-scrollbars",
365
+ "--enable-unsafe-swiftshader",
366
+ "--use-mock-keychain",
367
+ "--password-store=basic",
368
+ "about:blank",
369
+ ],
370
+ { stdio: ["ignore", "ignore", "ignore", "pipe", "pipe"] },
371
+ );
372
+ const [, , , input, output] = child.stdio;
373
+
374
+ try {
375
+ if (!(input instanceof Writable) || !(output instanceof Readable))
376
+ return { kind: "unavailable", reason: "Chromium did not open its DevTools pipe." };
377
+ const pipe = new Pipe(input, output);
378
+ child.once("error", (error) => pipe.close(error));
379
+ const stopped = Promise.withResolvers<never>();
380
+ const stop = (): void => stopped.reject(new Error("The preview was stopped."));
381
+ signal.addEventListener("abort", stop, { once: true });
382
+
383
+ try {
384
+ signal.throwIfAborted();
385
+
386
+ return await Promise.race([previewIn(pipe, shell, request), stopped.promise]);
387
+ } catch (error) {
388
+ return {
389
+ kind: "unavailable",
390
+ reason: `Chromium could not render the preview: ${error instanceof Error ? error.message : String(error)}`,
391
+ };
392
+ } finally {
393
+ signal.removeEventListener("abort", stop);
394
+ void pipe.send("Browser.close").catch(() => undefined);
395
+ pipe.close(new Error("The preview finished."));
396
+ }
397
+ } finally {
398
+ await stop(child);
399
+ await rm(profile, { recursive: true, force: true });
400
+ }
401
+ };
402
+ }
@@ -0,0 +1,294 @@
1
+ import { readCredentialAccounts } from "@nyte-ai/ai/auth/types";
2
+ import type { CredentialAccounts, CredentialStore } from "@nyte-ai/ai/auth/types";
3
+ import { SdkError } from "@nyte-ai/schema";
4
+ import { keyedSerial } from "@nyte-ai/schema/serial";
5
+ import type { Json, Ref } from "@nyte-ai/schema";
6
+ import { decodeKvValue, envelope, kvRef } from "@nyte-ai/core/store";
7
+ import type { Store, StoreConnection } from "@nyte-ai/core";
8
+ import { Type } from "typebox";
9
+ import type { Static } from "typebox";
10
+ import { Value } from "typebox/value";
11
+ import type { SecretsSource } from "./secrets.ts";
12
+
13
+ const SealedCredentialSchema = Type.Union([
14
+ Type.Object({
15
+ type: Type.Union([Type.Literal("api_key"), Type.Literal("oauth")]),
16
+ iv: Type.String(),
17
+ ciphertext: Type.String(),
18
+ }),
19
+ Type.Object({
20
+ active: Type.Union([Type.String(), Type.Null()]),
21
+ accounts: Type.Array(
22
+ Type.Object({
23
+ id: Type.String({ minLength: 1 }),
24
+ label: Type.String({ minLength: 1 }),
25
+ type: Type.Union([Type.Literal("api_key"), Type.Literal("oauth")]),
26
+ }),
27
+ ),
28
+ iv: Type.String(),
29
+ ciphertext: Type.String(),
30
+ }),
31
+ ]);
32
+
33
+ export type SealedCredential = Static<typeof SealedCredentialSchema>;
34
+
35
+ const PREFIX = kvRef("secret", "models", "");
36
+
37
+ const RETRIES = 5;
38
+
39
+ /**
40
+ * Returns `@nyte-ai/ai`'s `CredentialStore` over a host `Store`, so a host's
41
+ * model logins live with its sessions and pass to
42
+ * `createModels({ credentials })` (§42e); a Durable Object and a Vercel
43
+ * instance keep theirs.
44
+ *
45
+ * Each call opens a store connection and closes it when done; the `Store` is
46
+ * the caller's. `read` is `unsealCredential` over the record, `list` answers
47
+ * non-secret account metadata per ref under `kv/secret/models/`, `modify` is
48
+ * `modifyCredential`, and `delete` sets the ref to null, serialised as
49
+ * `modify` is. Building it touches nothing, `list` never calls `key()` and
50
+ * `read` calls it only when that provider's record exists (§50b, §55); the
51
+ * imported key is kept once `key()` resolves, a rejection is not (§43k). While `key()` rejects (a deploy
52
+ * without `NYTE_SECRET_KEY`), `read` answers undefined, `list` and `delete`
53
+ * work (they need no key) and `modify` rejects that error, so provider keys
54
+ * from the environment still resolve and logins answer `unavailable`. Web
55
+ * Crypto only, no `node:*` (the `./credentials` subpath).
56
+ */
57
+ export function credentialStore(store: Store, secrets: SecretsSource): CredentialStore {
58
+ let key: Promise<CryptoKey> | null = null;
59
+ const writes = keyedSerial<string>();
60
+
61
+ const readKey = (): Promise<CryptoKey> => {
62
+ const known = key ?? importedKey(secrets);
63
+
64
+ key = known;
65
+ known.catch(() => {
66
+ if (key === known) key = null;
67
+ });
68
+
69
+ return known;
70
+ };
71
+
72
+ const serialised = <T>(
73
+ provider: string,
74
+ signal: AbortSignal | undefined,
75
+ task: () => Promise<T>,
76
+ ): Promise<T> =>
77
+ writes.run(provider, () => {
78
+ signal?.throwIfAborted();
79
+
80
+ return task();
81
+ });
82
+
83
+ return {
84
+ async read(provider, options) {
85
+ options?.signal?.throwIfAborted();
86
+ const { sealed } = await connected(store, (connection) => recordAt(connection, provider));
87
+
88
+ if (sealed === null) return undefined;
89
+ const unlocked = await readKey().catch(() => null);
90
+
91
+ return unlocked === null ? undefined : unsealCredential(unlocked, sealed);
92
+ },
93
+ async list(options) {
94
+ options?.signal?.throwIfAborted();
95
+
96
+ return connected(store, async (connection) => {
97
+ const refs = await connection.refs.list(PREFIX);
98
+
99
+ const rows = await Promise.all(
100
+ refs.map(async (ref) => {
101
+ const sealed =
102
+ ref.target === null
103
+ ? null
104
+ : readSealedCredential(decodeKvValue(await connection.objects.get(ref.target)));
105
+
106
+ if (sealed === null) return [];
107
+ const providerId = ref.name.slice(PREFIX.length);
108
+
109
+ if ("type" in sealed)
110
+ return [
111
+ { providerId, id: "default", label: "Default", type: sealed.type, active: true },
112
+ ];
113
+
114
+ return sealed.accounts.map((account) => ({
115
+ providerId,
116
+ ...account,
117
+ active: sealed.active === account.id,
118
+ }));
119
+ }),
120
+ );
121
+
122
+ return rows.flat();
123
+ });
124
+ },
125
+ modify(provider, fn, options) {
126
+ return serialised(provider, options?.signal, async () =>
127
+ modifyCredential(store, await readKey(), provider, fn, options?.signal),
128
+ );
129
+ },
130
+ delete(provider, options) {
131
+ return serialised(provider, options?.signal, () =>
132
+ connected(store, async (connection) => {
133
+ await connection.refs.set([{ name: kvRef("secret", "models", provider), target: null }]);
134
+ }),
135
+ );
136
+ },
137
+ };
138
+ }
139
+
140
+ /** Returns `plain` sealed under the secrets key as `<iv>.<ciphertext>`, both base64; a desktop keeps its account token this way (§55). */
141
+ export async function sealSecret(secrets: SecretsSource, plain: string): Promise<string> {
142
+ const iv = crypto.getRandomValues(new Uint8Array(12));
143
+
144
+ const ciphertext = await crypto.subtle.encrypt(
145
+ { name: "AES-GCM", iv },
146
+ await importedKey(secrets),
147
+ new TextEncoder().encode(plain),
148
+ );
149
+
150
+ return `${iv.toBase64()}.${new Uint8Array(ciphertext).toBase64()}`;
151
+ }
152
+
153
+ /** Returns the text `sealSecret` sealed; rejects when it was sealed under another key or is not such a text. */
154
+ export async function openSecret(secrets: SecretsSource, sealed: string): Promise<string> {
155
+ const [iv = "", ciphertext = ""] = sealed.split(".");
156
+
157
+ const plain = await crypto.subtle.decrypt(
158
+ { name: "AES-GCM", iv: Uint8Array.fromBase64(iv) },
159
+ await importedKey(secrets),
160
+ Uint8Array.fromBase64(ciphertext),
161
+ );
162
+
163
+ return new TextDecoder().decode(plain);
164
+ }
165
+
166
+ async function importedKey(secrets: SecretsSource): Promise<CryptoKey> {
167
+ const bytes = await secrets.key();
168
+
169
+ if (bytes.length !== 32)
170
+ throw new SdkError("invalid", `The secrets key is ${bytes.length} bytes; AES-256 needs 32.`);
171
+
172
+ return crypto.subtle.importKey("raw", new Uint8Array(bytes), "AES-GCM", false, [
173
+ "encrypt",
174
+ "decrypt",
175
+ ]);
176
+ }
177
+
178
+ async function unsealCredential(
179
+ key: CryptoKey,
180
+ record: SealedCredential,
181
+ ): Promise<CredentialAccounts | undefined> {
182
+ let parsed: unknown;
183
+
184
+ try {
185
+ const plain = await crypto.subtle.decrypt(
186
+ { name: "AES-GCM", iv: Uint8Array.fromBase64(record.iv) },
187
+ key,
188
+ Uint8Array.fromBase64(record.ciphertext),
189
+ );
190
+
191
+ parsed = JSON.parse(new TextDecoder().decode(plain));
192
+ } catch {
193
+ return undefined;
194
+ }
195
+
196
+ return readCredentialAccounts(parsed);
197
+ }
198
+
199
+ async function modifyCredential(
200
+ store: Store,
201
+ key: CryptoKey,
202
+ provider: string,
203
+ fn: (current: CredentialAccounts | undefined) => Promise<CredentialAccounts | undefined>,
204
+ signal: AbortSignal | undefined,
205
+ ): Promise<CredentialAccounts | undefined> {
206
+ const name = kvRef("secret", "models", provider);
207
+ let started = false;
208
+
209
+ return connected(store, async (connection) => {
210
+ for (let attempt = 0; attempt <= RETRIES; attempt += 1) {
211
+ const { ref, sealed } = await recordAt(connection, provider);
212
+ const current = sealed === null ? undefined : await unsealCredential(key, sealed);
213
+
214
+ if (!started) signal?.throwIfAborted();
215
+ started = true;
216
+ const next = await fn(current);
217
+
218
+ if (next === undefined) return current;
219
+
220
+ const target =
221
+ next.accounts.length === 0
222
+ ? null
223
+ : await connection.objects.put(envelope("kv", await seal(key, next), Date.now()));
224
+
225
+ const outcome = await connection.refs.set([
226
+ { name, target, expect: ref === null ? "absent" : ref.revision },
227
+ ]);
228
+
229
+ if (outcome.kind === "set") return next;
230
+ }
231
+
232
+ throw new SdkError(
233
+ "unavailable",
234
+ `The ${provider} credential kept changing under this write; try again.`,
235
+ );
236
+ });
237
+ }
238
+
239
+ async function seal(key: CryptoKey, credential: CredentialAccounts): Promise<Json> {
240
+ const iv = crypto.getRandomValues(new Uint8Array(12));
241
+
242
+ const ciphertext = new Uint8Array(
243
+ await crypto.subtle.encrypt(
244
+ { name: "AES-GCM", iv },
245
+ key,
246
+ new TextEncoder().encode(JSON.stringify(credential)),
247
+ ),
248
+ );
249
+
250
+ return {
251
+ active: credential.active,
252
+ accounts: credential.accounts.map((account) => ({
253
+ id: account.id,
254
+ label: account.label,
255
+ type: account.credential.type,
256
+ })),
257
+ iv: iv.toBase64(),
258
+ ciphertext: ciphertext.toBase64(),
259
+ };
260
+ }
261
+
262
+ async function recordAt(
263
+ connection: StoreConnection,
264
+ provider: string,
265
+ ): Promise<{ readonly ref: Ref | null; readonly sealed: SealedCredential | null }> {
266
+ const ref = await connection.refs.get(kvRef("secret", "models", provider));
267
+
268
+ return {
269
+ ref,
270
+ sealed:
271
+ ref === null || ref.target === null
272
+ ? null
273
+ : readSealedCredential(decodeKvValue(await connection.objects.get(ref.target))),
274
+ };
275
+ }
276
+
277
+ function readSealedCredential(body: Json | undefined): SealedCredential | null {
278
+ if (!Value.Check(SealedCredentialSchema, body)) return null;
279
+
280
+ if ("type" in body) return body;
281
+
282
+ const ids = new Set(body.accounts.map((account) => account.id));
283
+
284
+ return ids.size === body.accounts.length && (body.active === null || ids.has(body.active))
285
+ ? body
286
+ : null;
287
+ }
288
+
289
+ async function connected<T>(
290
+ store: Store,
291
+ task: (connection: StoreConnection) => Promise<T>,
292
+ ): Promise<T> {
293
+ return task(await store.open());
294
+ }
@@ -0,0 +1,3 @@
1
+ # Cloudflare
2
+
3
+ `worker.ts` runs one Nyte host in one SQLite-backed Durable Object, `NyteHost`, and the Worker in front of it sends every request to that object. The object keeps sessions in its own storage through `durableObjectStore`, serves a chat-only workspace (no files, no processes), answers the protocol through `listenFetch`, and runs sessions from its alarm: each pass advances up to 32 due heads within 10 seconds, re-alarms at once for the rest, and sets the alarm for the next due head from its `nyte_schedule` table. It imports only the node-free subpaths (`@nyte-ai/host/durable-object`, `/listen-fetch`, `/providers`), never the Node root. `wrangler.jsonc` requires the `NYTE_TOKEN` secret (at least 16 characters): run `wrangler secret put NYTE_TOKEN`, to enable `secrets.set` also `wrangler secret put NYTE_SECRET_KEY` with a base64 key, and to answer identity challenges `wrangler secret put NYTE_IDENTITY_KEY` with a base64 32-byte Ed25519 seed (without it `/.nyte/identity` answers `unknown`), then `wrangler deploy` from this directory.