toiljs 0.0.115 → 0.0.117

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 (326) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +16 -7
  3. package/TYPESCRIPT_LAW.md +1946 -2073
  4. package/build/backend/.tsbuildinfo +1 -1
  5. package/build/cli/.tsbuildinfo +1 -1
  6. package/build/cli/index.js +2375 -258
  7. package/build/client/.tsbuildinfo +1 -1
  8. package/build/client/dev/devtools.js +1 -1
  9. package/build/client/dev/error-overlay.js +1 -1
  10. package/build/client/routing/mount.js +1 -1
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/auth-emails.js +7 -3
  13. package/build/compiler/emails.js +1 -1
  14. package/build/compiler/index.d.ts +1 -0
  15. package/build/compiler/index.js +25 -4
  16. package/build/compiler/pages.js +2 -3
  17. package/build/compiler/prerender.d.ts +3 -8
  18. package/build/compiler/prerender.js +88 -127
  19. package/build/compiler/ssg.js +3 -4
  20. package/build/compiler/template-build.js +3 -4
  21. package/build/compiler/toil-docs.generated.js +6 -5
  22. package/build/compiler/vite.js +3 -6
  23. package/build/devserver/.tsbuildinfo +1 -1
  24. package/build/devserver/db/database.js +7 -3
  25. package/build/devserver/db/types.js +1 -1
  26. package/build/devserver/production.js +4 -3
  27. package/build/devserver/runtime/crypto.d.ts +1 -1
  28. package/build/io/.tsbuildinfo +1 -1
  29. package/build/logger/.tsbuildinfo +1 -1
  30. package/build/shared/.tsbuildinfo +1 -1
  31. package/examples/basic/client/routes/analytics.tsx +7 -5
  32. package/examples/basic/server/tsconfig.json +1 -3
  33. package/package.json +45 -44
  34. package/presets/no-uint8array-tostring.js +120 -153
  35. package/presets/oxlint.d.ts +5 -0
  36. package/presets/oxlint.js +224 -0
  37. package/server/runtime/tsconfig.json +1 -3
  38. package/tsconfig.json +2 -11
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  40. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  41. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  42. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  43. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  44. package/.github/changelog-config.json +0 -45
  45. package/.github/dependabot.yml +0 -27
  46. package/.github/workflows/ci.yml +0 -222
  47. package/.vscode/settings.json +0 -10
  48. package/docs/README.md +0 -107
  49. package/docs/auth/README.md +0 -104
  50. package/docs/auth/configuration.md +0 -105
  51. package/docs/auth/emails.md +0 -92
  52. package/docs/auth/extending.md +0 -202
  53. package/docs/auth/how-it-works.md +0 -138
  54. package/docs/auth/usage.md +0 -188
  55. package/docs/backend/README.md +0 -143
  56. package/docs/backend/data.md +0 -351
  57. package/docs/backend/rest.md +0 -402
  58. package/docs/backend/rpc.md +0 -226
  59. package/docs/background/README.md +0 -114
  60. package/docs/background/daemons.md +0 -276
  61. package/docs/background/derive.md +0 -179
  62. package/docs/cli/README.md +0 -379
  63. package/docs/concepts/ai-guide.md +0 -132
  64. package/docs/concepts/config.md +0 -416
  65. package/docs/concepts/decorators.md +0 -127
  66. package/docs/concepts/security.md +0 -108
  67. package/docs/concepts/tiers.md +0 -166
  68. package/docs/concepts/types.md +0 -216
  69. package/docs/database/README.md +0 -143
  70. package/docs/database/capacity.md +0 -350
  71. package/docs/database/counters.md +0 -174
  72. package/docs/database/documents.md +0 -277
  73. package/docs/database/events.md +0 -307
  74. package/docs/database/membership.md +0 -246
  75. package/docs/database/setup.md +0 -155
  76. package/docs/database/unique.md +0 -216
  77. package/docs/database/views.md +0 -246
  78. package/docs/frontend/README.md +0 -104
  79. package/docs/frontend/components.md +0 -286
  80. package/docs/frontend/data-fetching.md +0 -260
  81. package/docs/frontend/images.md +0 -148
  82. package/docs/frontend/metadata.md +0 -344
  83. package/docs/frontend/navigation.md +0 -296
  84. package/docs/frontend/rendering.md +0 -236
  85. package/docs/frontend/routing.md +0 -346
  86. package/docs/frontend/scripts.md +0 -118
  87. package/docs/frontend/search.md +0 -191
  88. package/docs/frontend/styling.md +0 -147
  89. package/docs/frontend/toil-global.md +0 -216
  90. package/docs/getting-started/README.md +0 -81
  91. package/docs/getting-started/create-project.md +0 -131
  92. package/docs/getting-started/deploy.md +0 -101
  93. package/docs/getting-started/first-app.md +0 -215
  94. package/docs/getting-started/installation.md +0 -124
  95. package/docs/getting-started/migrating.md +0 -125
  96. package/docs/getting-started/project-structure.md +0 -163
  97. package/docs/introduction/README.md +0 -66
  98. package/docs/introduction/design-principles.md +0 -81
  99. package/docs/introduction/distributed.md +0 -91
  100. package/docs/introduction/how-it-works.md +0 -121
  101. package/docs/introduction/hyperscale.md +0 -70
  102. package/docs/introduction/modern-stack.md +0 -85
  103. package/docs/introduction/vs-other-frameworks.md +0 -64
  104. package/docs/introduction/why-toil.md +0 -99
  105. package/docs/llms.txt +0 -95
  106. package/docs/realtime/README.md +0 -102
  107. package/docs/realtime/channels.md +0 -211
  108. package/docs/realtime/streams.md +0 -369
  109. package/docs/services/README.md +0 -60
  110. package/docs/services/analytics.md +0 -279
  111. package/docs/services/caching.md +0 -175
  112. package/docs/services/cookies.md +0 -235
  113. package/docs/services/crypto.md +0 -209
  114. package/docs/services/email.md +0 -289
  115. package/docs/services/environment.md +0 -141
  116. package/docs/services/ratelimit.md +0 -174
  117. package/docs/services/time.md +0 -85
  118. package/eslint.config.js +0 -57
  119. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  120. package/presets/eslint.js +0 -88
  121. package/src/backend/index.ts +0 -204
  122. package/src/cli/configure.ts +0 -363
  123. package/src/cli/create.ts +0 -924
  124. package/src/cli/db.ts +0 -158
  125. package/src/cli/diagnostics.ts +0 -665
  126. package/src/cli/doctor.ts +0 -982
  127. package/src/cli/features.ts +0 -170
  128. package/src/cli/index.ts +0 -326
  129. package/src/cli/notify.ts +0 -120
  130. package/src/cli/proc.ts +0 -56
  131. package/src/cli/ui.ts +0 -169
  132. package/src/cli/update.ts +0 -248
  133. package/src/cli/updates.ts +0 -92
  134. package/src/cli/validate.ts +0 -31
  135. package/src/cli/version-check.ts +0 -125
  136. package/src/client/auth.ts +0 -869
  137. package/src/client/channel/channel.ts +0 -146
  138. package/src/client/components/Form.tsx +0 -65
  139. package/src/client/components/Image.tsx +0 -160
  140. package/src/client/components/Script.tsx +0 -113
  141. package/src/client/components/Slot.tsx +0 -21
  142. package/src/client/dev/devtools.tsx +0 -1089
  143. package/src/client/dev/error-overlay.tsx +0 -225
  144. package/src/client/errors.ts +0 -11
  145. package/src/client/head/head.ts +0 -182
  146. package/src/client/head/metadata.ts +0 -110
  147. package/src/client/index.ts +0 -130
  148. package/src/client/navigation/Link.tsx +0 -108
  149. package/src/client/navigation/NavLink.tsx +0 -86
  150. package/src/client/navigation/navigation.ts +0 -235
  151. package/src/client/navigation/prefetch.ts +0 -169
  152. package/src/client/navigation/scroll.ts +0 -53
  153. package/src/client/routing/Router.tsx +0 -216
  154. package/src/client/routing/action.ts +0 -122
  155. package/src/client/routing/error-boundary.tsx +0 -43
  156. package/src/client/routing/hooks.ts +0 -140
  157. package/src/client/routing/lazy.ts +0 -77
  158. package/src/client/routing/loader.ts +0 -381
  159. package/src/client/routing/match.ts +0 -47
  160. package/src/client/routing/mount.tsx +0 -90
  161. package/src/client/routing/params-context.ts +0 -10
  162. package/src/client/routing/slot-context.ts +0 -7
  163. package/src/client/rpc.ts +0 -97
  164. package/src/client/search/search.ts +0 -189
  165. package/src/client/search/use-page-search.ts +0 -73
  166. package/src/client/ssr/markers.tsx +0 -172
  167. package/src/client/stream/client.ts +0 -237
  168. package/src/client/types.ts +0 -73
  169. package/src/compiler/auth-emails.ts +0 -312
  170. package/src/compiler/config.ts +0 -369
  171. package/src/compiler/docs.ts +0 -127
  172. package/src/compiler/email-preview.ts +0 -314
  173. package/src/compiler/emails.ts +0 -404
  174. package/src/compiler/fonts.ts +0 -89
  175. package/src/compiler/generate.ts +0 -471
  176. package/src/compiler/image-blur.ts +0 -42
  177. package/src/compiler/image-report.ts +0 -88
  178. package/src/compiler/index.ts +0 -1362
  179. package/src/compiler/pages.ts +0 -49
  180. package/src/compiler/plugin.ts +0 -391
  181. package/src/compiler/prerender.ts +0 -263
  182. package/src/compiler/routes.ts +0 -170
  183. package/src/compiler/seo.ts +0 -415
  184. package/src/compiler/sri.ts +0 -144
  185. package/src/compiler/ssg.ts +0 -177
  186. package/src/compiler/ssr-codegen.ts +0 -85
  187. package/src/compiler/template-build.ts +0 -676
  188. package/src/compiler/template.ts +0 -262
  189. package/src/compiler/toil-docs.generated.ts +0 -77
  190. package/src/compiler/vite.ts +0 -245
  191. package/src/devserver/analytics/index.ts +0 -317
  192. package/src/devserver/config/dotenv.ts +0 -102
  193. package/src/devserver/config/env.ts +0 -30
  194. package/src/devserver/config/ratelimit.ts +0 -123
  195. package/src/devserver/daemon/catalog.ts +0 -120
  196. package/src/devserver/daemon/cron.ts +0 -87
  197. package/src/devserver/daemon/host.ts +0 -134
  198. package/src/devserver/daemon/index.ts +0 -369
  199. package/src/devserver/daemon/runtime.ts +0 -48
  200. package/src/devserver/db/catalog.ts +0 -109
  201. package/src/devserver/db/database.ts +0 -1834
  202. package/src/devserver/db/derives.ts +0 -121
  203. package/src/devserver/db/index.ts +0 -20
  204. package/src/devserver/db/routeKinds.ts +0 -191
  205. package/src/devserver/db/types.ts +0 -168
  206. package/src/devserver/email/caps.ts +0 -0
  207. package/src/devserver/email/config.ts +0 -141
  208. package/src/devserver/email/console.ts +0 -348
  209. package/src/devserver/email/index.ts +0 -155
  210. package/src/devserver/email/providers.ts +0 -131
  211. package/src/devserver/email/status.ts +0 -23
  212. package/src/devserver/email/validate.ts +0 -37
  213. package/src/devserver/email/wire.ts +0 -55
  214. package/src/devserver/http/cache.ts +0 -0
  215. package/src/devserver/http/envelope.ts +0 -157
  216. package/src/devserver/http/proxy.ts +0 -172
  217. package/src/devserver/http/runtime.ts +0 -387
  218. package/src/devserver/index.ts +0 -43
  219. package/src/devserver/production-ipc.ts +0 -63
  220. package/src/devserver/production-worker.ts +0 -83
  221. package/src/devserver/production.ts +0 -718
  222. package/src/devserver/runtime/crypto.ts +0 -587
  223. package/src/devserver/runtime/host.ts +0 -427
  224. package/src/devserver/runtime/module.ts +0 -442
  225. package/src/devserver/server.ts +0 -251
  226. package/src/devserver/ssr.ts +0 -208
  227. package/src/devserver/stream/catalog.ts +0 -106
  228. package/src/devserver/stream/host.ts +0 -42
  229. package/src/devserver/stream/index.ts +0 -301
  230. package/src/devserver/stream/manager.ts +0 -167
  231. package/src/devserver/stream/router.ts +0 -101
  232. package/src/devserver/stream/wire.ts +0 -58
  233. package/src/devserver/stream/ws.ts +0 -78
  234. package/src/devserver/wasm/sections.ts +0 -59
  235. package/src/devserver/wasm/surface.ts +0 -115
  236. package/src/io/FastMap.ts +0 -151
  237. package/src/io/FastSet.ts +0 -113
  238. package/src/io/codec.ts +0 -362
  239. package/src/io/index.ts +0 -10
  240. package/src/io/lengths.ts +0 -14
  241. package/src/io/types.ts +0 -19
  242. package/src/logger/index.ts +0 -22
  243. package/src/shared/index.ts +0 -46
  244. package/test/analytics-dev.test.ts +0 -113
  245. package/test/assembly/aspect-shim.ts +0 -33
  246. package/test/assembly/cookie.spec.ts +0 -303
  247. package/test/assembly/example.spec.ts +0 -22
  248. package/test/assembly/ssr.spec.ts +0 -95
  249. package/test/assembly.test.ts +0 -59
  250. package/test/auth-emails.test.ts +0 -118
  251. package/test/built-ssr.test.ts +0 -148
  252. package/test/channel.test.ts +0 -21
  253. package/test/config-host-port.test.ts +0 -59
  254. package/test/configure.test.ts +0 -94
  255. package/test/daemon-build.test.ts +0 -278
  256. package/test/daemon-catalog.test.ts +0 -349
  257. package/test/daemon-emulation.test.ts +0 -256
  258. package/test/db.test.ts +0 -0
  259. package/test/devserver-database.test.ts +0 -1102
  260. package/test/devserver-email.test.ts +0 -306
  261. package/test/devserver-pqauth.test.ts +0 -93
  262. package/test/devserver-secrets.test.ts +0 -63
  263. package/test/devserver.test.ts +0 -259
  264. package/test/doctor.test.ts +0 -298
  265. package/test/dom/Image.test.tsx +0 -144
  266. package/test/dom/Link.test.tsx +0 -67
  267. package/test/dom/NavLink.test.tsx +0 -37
  268. package/test/dom/Script.test.tsx +0 -48
  269. package/test/dom/action.test.tsx +0 -146
  270. package/test/dom/error-overlay.test.tsx +0 -44
  271. package/test/dom/loader.test.tsx +0 -121
  272. package/test/dom/navigation.test.ts +0 -59
  273. package/test/dom/revalidate.test.tsx +0 -38
  274. package/test/dom/route-head.test.tsx +0 -56
  275. package/test/dom/router-loading.test.tsx +0 -44
  276. package/test/dom/scroll.test.ts +0 -56
  277. package/test/dom/slot.test.tsx +0 -131
  278. package/test/dom/use-metadata.test.tsx +0 -58
  279. package/test/dom/view-transitions.test.tsx +0 -53
  280. package/test/dynamic-routing.test.ts +0 -132
  281. package/test/email-console.test.ts +0 -86
  282. package/test/email-preview.test.ts +0 -73
  283. package/test/emails.test.ts +0 -58
  284. package/test/errors.test.ts +0 -21
  285. package/test/example-guestbook.test.ts +0 -123
  286. package/test/features.test.ts +0 -149
  287. package/test/fixtures/bignum-wire/spec.ts +0 -25
  288. package/test/fixtures/daemon-app.ts +0 -65
  289. package/test/fixtures/stream-echo.ts +0 -26
  290. package/test/fixtures/stream-gate.ts +0 -28
  291. package/test/fixtures/stream-trap.ts +0 -18
  292. package/test/fixtures/stream-typed.ts +0 -41
  293. package/test/fonts.test.ts +0 -28
  294. package/test/global-setup.ts +0 -17
  295. package/test/head.test.ts +0 -42
  296. package/test/image-blur.test.ts +0 -50
  297. package/test/io.test.ts +0 -117
  298. package/test/metadata.test.ts +0 -40
  299. package/test/navlink.test.ts +0 -28
  300. package/test/pages.test.ts +0 -105
  301. package/test/placeholder.test.ts +0 -9
  302. package/test/pqauth-e2e.test.ts +0 -216
  303. package/test/pqauth-email.test.ts +0 -801
  304. package/test/prerender.test.ts +0 -116
  305. package/test/prettier-plugin.test.ts +0 -46
  306. package/test/routes.test.ts +0 -76
  307. package/test/rpc-bignum-wire.test.ts +0 -164
  308. package/test/rpc-dispatch.test.ts +0 -132
  309. package/test/rpc-kinds.test.ts +0 -18
  310. package/test/rpc.test.ts +0 -66
  311. package/test/search.test.ts +0 -114
  312. package/test/seo.test.ts +0 -175
  313. package/test/slot-layouts.test.ts +0 -69
  314. package/test/sri.test.ts +0 -129
  315. package/test/ssg.test.ts +0 -36
  316. package/test/ssr-hydration.test.tsx +0 -122
  317. package/test/ssr-render.test.ts +0 -197
  318. package/test/ssr-template.test.tsx +0 -482
  319. package/test/stream-emulation.test.ts +0 -479
  320. package/test/ui.test.ts +0 -40
  321. package/test/update.test.ts +0 -58
  322. package/test/validate.test.ts +0 -42
  323. package/test/version-check.test.ts +0 -130
  324. package/tests/data-parity/generated-parity.ts +0 -99
  325. package/tests/data-parity/parity.ts +0 -80
  326. package/tests/data-parity/spec.ts +0 -46
@@ -1,427 +0,0 @@
1
- /**
2
- * The host import surface the dev server exposes to the guest, mirroring the
3
- * functions the production edge (`toil-backend/src/wasm/host/imports.rs`)
4
- * registers under the `env` namespace:
5
- *
6
- * - `abort(msg, file, line, col)` ToilScript panic hook; raises a trap
7
- * - `set_status(code)` imperative status (clamped to [100, 599])
8
- * - `set_header(nPtr, nLen, vPtr, vLen)` imperative response header
9
- * - `respond_file(pathPtr, pathLen)` stream a file as the response body
10
- * - `thread_spawn(startArg)` fail-closed stub, always -1 (no threading in dev)
11
- *
12
- * The ToilScript runtime returns status + headers in-band via the response
13
- * envelope, so a toiljs guest today only imports `abort`; the imperative
14
- * functions are provided for parity with the edge and apply on top of the
15
- * envelope (a `set_status` wins over the envelope status, `set_header` values
16
- * are appended). Extra keys in the import object are ignored by
17
- * `WebAssembly.Instance`, so offering the full surface costs nothing.
18
- */
19
-
20
- import { devEnvGet, devEnvGetSecure } from '../config/env.js';
21
- import { ratelimitCheck } from '../config/ratelimit.js';
22
- import { buildAnalyticsImports } from '../analytics/index.js';
23
- import { buildDatabaseImports, type DbDevState, freshDbState } from '../db/index.js';
24
- import { EmailStatus, getEmailService, recordSentEmail } from '../email/index.js';
25
- import { renderEmailConsole } from '../email/console.js';
26
- import { type ParsedEmail, parseEmailBlob } from '../email/wire.js';
27
- import { buildCryptoImports, type CryptoState, freshCryptoState } from './crypto.js';
28
-
29
- /** Limits identical to the edge's `set_header` / `respond_file` bounds. */
30
- const MAX_TOTAL_HEADERS_BYTES = 64 * 1024;
31
- const MAX_HEADER_NAME_LEN = 256;
32
- const MAX_HEADER_VALUE_LEN = 8192;
33
- const MAX_PATH_LEN = 4096;
34
-
35
- /** RFC 9110 tchar token, the only bytes allowed in a header name. */
36
- const TCHAR = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
37
-
38
- /** A guest `abort()` (ToilScript assert/bounds-check failure), surfaced as a trap. */
39
- export class WasmAbortError extends Error {
40
- constructor(message: string, fileName: string, line: number, column: number) {
41
- super(
42
- `wasm aborted: ${message || '<no message>'}` +
43
- (fileName ? ` at ${fileName}:${String(line)}:${String(column)}` : ''),
44
- );
45
- this.name = 'WasmAbortError';
46
- }
47
- }
48
-
49
- /** Per-dispatch state the imperative host imports write into. */
50
- export interface DispatchState {
51
- /** Status from `set_status`, or `null` when the guest never called it. */
52
- status: number | null;
53
- /** Headers accumulated by `set_header`, in call order. */
54
- headers: [string, string][];
55
- /** Total header bytes so far (cap: {@link MAX_TOTAL_HEADERS_BYTES}). */
56
- headerBytes: number;
57
- /** File path from `respond_file`, or `null`; when set, the envelope body is ignored. */
58
- sendfile: string | null;
59
- /** The connecting client's IP for `client_ip` (the edge uses the socket peer);
60
- * set per dispatch from the Node request's `socket.remoteAddress`, '' if unknown. */
61
- clientIp: string;
62
- /** Per-dispatch Web Crypto keystore + result scratch (mirrors the edge). */
63
- crypto: CryptoState;
64
- /** Per-dispatch ToilDB state: resolved collection handles + result stash. */
65
- db: DbDevState;
66
- }
67
-
68
- /** A fresh, zeroed per-dispatch state (the edge resets the same way before each request). */
69
- export function freshDispatchState(): DispatchState {
70
- return {
71
- status: null,
72
- headers: [],
73
- headerBytes: 0,
74
- sendfile: null,
75
- clientIp: '',
76
- crypto: freshCryptoState(),
77
- db: freshDbState(),
78
- };
79
- }
80
-
81
- /**
82
- * Late-bound memory holder: the import object must exist before the instance
83
- * (and therefore its exported memory) does, so the host functions read through
84
- * this indirection. The module loader fills it in right after instantiation.
85
- */
86
- export interface MemoryRef {
87
- memory: WebAssembly.Memory | null;
88
- }
89
-
90
- function mem(ref: MemoryRef): Buffer {
91
- if (!ref.memory) throw new Error('host import called before memory was bound');
92
- return Buffer.from(ref.memory.buffer);
93
- }
94
-
95
- /** Bounds-checked byte read out of guest linear memory. */
96
- export function readBytes(ref: MemoryRef, ptr: number, len: number): Buffer {
97
- const m = mem(ref);
98
- if (ptr < 0 || len < 0 || ptr + len > m.length)
99
- throw new Error(`host import read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
100
- return m.subarray(ptr, ptr + len);
101
- }
102
-
103
- /**
104
- * Bounds-checked write of a variable-length result into a guest out-buffer, with
105
- * the edge's inline-drain return protocol: the byte length on success, or `-1`
106
- * (STATUS_TOO_SMALL) when `outCap` is too small (the guest retries with a bigger
107
- * buffer).
108
- */
109
- export function writeBytesOut(
110
- ref: MemoryRef,
111
- bytes: Buffer,
112
- outPtr: number,
113
- outCap: number,
114
- ): number {
115
- if (bytes.length > outCap) return -1; // TOO_SMALL
116
- const m = mem(ref);
117
- if (outPtr < 0 || outPtr + bytes.length > m.length)
118
- throw new Error('host import write out of bounds');
119
- bytes.copy(m, outPtr);
120
- return bytes.length;
121
- }
122
-
123
- /**
124
- * Read a ToilScript string (UTF-16LE payload, byte length in the u32 at
125
- * `ptr - 4`). Used by `abort`, whose pointers reference string objects rather
126
- * than raw byte ranges. A null pointer yields ''.
127
- */
128
- function readGuestString(ref: MemoryRef, ptr: number): string {
129
- if (ptr === 0) return '';
130
- const m = mem(ref);
131
- if (ptr < 4 || ptr > m.length) return '';
132
- const byteLen = m.readUInt32LE(ptr - 4);
133
- if (ptr + byteLen > m.length) return '';
134
- return m.toString('utf16le', ptr, ptr + byteLen);
135
- }
136
-
137
- /**
138
- * Framework auth secrets that, when unset, SILENTLY fall back to a published,
139
- * well-known dev default inside the guest (see `server/globals/auth.ts`). Reading
140
- * one that is absent means the wasm is about to sign sessions / derive keys under
141
- * a value anyone can read off npm, so we surface it. Harmless for local dev; a
142
- * deployed node MUST set these out of band (`.env.secrets` / the dashboard).
143
- */
144
- const INSECURE_DEFAULT_SECRETS: Record<string, string> = {
145
- AUTH_SESSION_SECRET:
146
- 'session cookies will be signed with a PUBLISHED key, so anyone can forge one and skip login',
147
- AUTH_OPRF_SEED: 'the password OPRF seed will be the published dev value',
148
- AUTH_KEM_SK: 'the server ML-KEM secret key will be the published dev value',
149
- };
150
-
151
- /** Warned-once set, keyed by secret name, so a hot path cannot spam the log. */
152
- const warnedInsecureSecrets = new Set<string>();
153
-
154
- /** Warn (once per process) that an absent framework secret falls back to a public default. */
155
- function warnInsecureSecretFallback(key: string): void {
156
- if (warnedInsecureSecrets.has(key)) return;
157
- warnedInsecureSecrets.add(key);
158
- process.stdout.write(
159
- ` ⚠ ${key} is not set: ${INSECURE_DEFAULT_SECRETS[key]}. ` +
160
- `Fine for local dev, but a deployed node MUST set it in .env.secrets (or on your deploy target). ` +
161
- `Generate one: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"\n`,
162
- );
163
- }
164
-
165
- /**
166
- * Resolve one `Environment.get`/`getSecure` lookup against the dev env source
167
- * and write it into the guest buffer, with the edge's return protocol: the value
168
- * byte length (`0` = present-but-empty), `-1` if `outCap` is too small (the guest
169
- * retries with a bigger buffer), `-2` if the key is absent.
170
- */
171
- function envLookup(
172
- ref: MemoryRef,
173
- keyPtr: number,
174
- keyLen: number,
175
- outPtr: number,
176
- outCap: number,
177
- secure: boolean,
178
- ): number {
179
- const key = readBytes(ref, keyPtr, keyLen).toString('utf8');
180
- const val = secure ? devEnvGetSecure(key) : devEnvGet(key);
181
- if (val === null) {
182
- if (secure && key in INSECURE_DEFAULT_SECRETS) warnInsecureSecretFallback(key);
183
- return -2; // ABSENT
184
- }
185
- const bytes = Buffer.from(val, 'utf8');
186
- if (bytes.length > outCap) return -1; // TOO_SMALL
187
- const m = mem(ref);
188
- if (outPtr < 0 || outPtr + bytes.length > m.length)
189
- throw new Error('env_get write out of bounds');
190
- bytes.copy(m, outPtr);
191
- return bytes.length;
192
- }
193
-
194
- /**
195
- * `env::email_send` / `env::email_send_detached` in dev: the FULL email pipeline
196
- * (./email) — parse + recipient validation + dedup + per-min/day budget +
197
- * per-recipient cap run SYNCHRONOUSLY (exact status: BadRecipient/Deduped/Budget/
198
- * RecipientCapped), then the real provider send is FIRE-AND-FORGET (a sync wasm
199
- * import can't await it), so the guest gets `Sent` optimistically and the true
200
- * outcome is logged. Unconfigured email stays a log-only mock returning `Sent`.
201
- *
202
- * The dev path already fires-and-forgets, so the SUSPENDING `email_send` and the
203
- * NON-SUSPENDING `email_send_detached` map to the exact same behaviour here (both
204
- * return immediately). BOTH feed the `__sentEmails` capture seam, so the email
205
- * confirm/reset tests find their tokens whichever import the guest used. Mirrors
206
- * the edge's `email_send_import.rs`.
207
- */
208
- /** Render the dev EMAIL EMULATOR box (from the HTML) so confirm/reset/2FA flows are
209
- * testable without a real inbox. Skipped under vitest (tests read the
210
- * `__sentEmails` seam), where a box per email would just be noise. */
211
- function previewDevEmail(parsed: ParsedEmail, status: string): void {
212
- if (process.env.VITEST) return;
213
- process.stdout.write(renderEmailConsole(parsed, status) + '\n');
214
- }
215
-
216
- function devEmailSend(ref: MemoryRef, reqPtr: number, reqLen: number): number {
217
- const raw = readBytes(ref, reqPtr, reqLen);
218
- const svc = getEmailService();
219
- if (svc === null) {
220
- const parsed = parseEmailBlob(raw);
221
- if (parsed !== null) recordSentEmail(parsed); // dev test seam
222
- const to = parsed?.to ?? '<unparsed>';
223
- process.stdout.write(` ✉ dev email_send -> ${to} (no email config; not sent)\n`);
224
- // Draw the email so the flow is testable with no mailer configured.
225
- if (parsed !== null) previewDevEmail(parsed, 'not sent · no provider');
226
- return EmailStatus.Sent;
227
- }
228
- const { status, parsed } = svc.prepare(raw);
229
- if (parsed === null) {
230
- process.stdout.write(` ✉ dev email_send -> ${EmailStatus[status]}\n`);
231
- // The real send was skipped (bad recipient / deduped / budget / cap), but in
232
- // DEV still draw the email so its link/code stays testable. Not recorded to
233
- // the test seam (that tracks accepted sends only).
234
- const skipped = parseEmailBlob(raw);
235
- if (skipped !== null) previewDevEmail(skipped, EmailStatus[status]);
236
- return status;
237
- }
238
- recordSentEmail(parsed); // dev test seam
239
- previewDevEmail(parsed, 'delivering via provider'); // shown even with a real provider
240
- void svc
241
- .deliver(parsed)
242
- .then((s) => {
243
- const label = s === EmailStatus.Sent ? 'sent' : EmailStatus[s];
244
- process.stdout.write(` ✉ dev email_send -> ${parsed.to} (${label})\n`);
245
- })
246
- .catch((e: unknown) => {
247
- process.stdout.write(` ✉ dev email_send -> ${parsed.to} (error: ${String(e)})\n`);
248
- });
249
- return EmailStatus.Sent; // optimistic; sync wasm can't await the send
250
- }
251
-
252
- /**
253
- * The portion of the `env.*` request surface that is SHARED by the daemon (cold)
254
- * box: panic hook, `Environment.get`/`getSecure`, `email_send`, `thread_spawn`,
255
- * and `Date.now`. It deliberately EXCLUDES the response/stream functions a cold
256
- * box must not have (`set_status`/`set_header`/`respond_file`/`client_ip`/
257
- * `ratelimit_check`), which stay in {@link buildHostImports}. None of these read
258
- * the per-dispatch response state, so they need only `ref`. The crypto and DB
259
- * namespaces are spread on top by each box's loader (they carry their own state).
260
- */
261
- export function buildEnvImports(
262
- ref: MemoryRef,
263
- _state: { crypto: CryptoState; db: DbDevState },
264
- ): Record<string, (...a: never[]) => unknown> {
265
- return {
266
- abort: (msgPtr: number, filePtr: number, line: number, col: number): void => {
267
- throw new WasmAbortError(
268
- readGuestString(ref, msgPtr),
269
- readGuestString(ref, filePtr),
270
- line,
271
- col,
272
- );
273
- },
274
-
275
- // `Environment.get` / `getSecure`: copy one tenant env value into the
276
- // guest buffer. Returns the byte length (0 = present-but-empty), -1 if
277
- // the buffer is too small (the guest retries bigger), -2 if absent.
278
- env_get: (keyPtr: number, keyLen: number, outPtr: number, outCap: number): number =>
279
- envLookup(ref, keyPtr, keyLen, outPtr, outCap, false),
280
- env_get_secure: (keyPtr: number, keyLen: number, outPtr: number, outCap: number): number =>
281
- envLookup(ref, keyPtr, keyLen, outPtr, outCap, true),
282
-
283
- thread_spawn: (_startArg: number): number => -1,
284
-
285
- // `Date.now()` -> wall-clock milliseconds, matching the edge host.
286
- 'Date.now': (): bigint => BigInt(Date.now()),
287
-
288
- // `env::email_send` / `email_send_detached`: the FULL email pipeline in
289
- // dev. A daemon may send mail, so this stays in the shared subset (00 B2 /
290
- // doc 08 AN-8). Both map to the same fire-and-forget dev path.
291
- email_send: (reqPtr: number, reqLen: number): number =>
292
- devEmailSend(ref, reqPtr, reqLen),
293
- email_send_detached: (reqPtr: number, reqLen: number): number =>
294
- devEmailSend(ref, reqPtr, reqLen),
295
- };
296
- }
297
-
298
- /**
299
- * Build the `env` import object for one instance. `state` collects what the
300
- * imperative imports produce during a dispatch; bind a fresh state per request.
301
- */
302
- export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssembly.Imports {
303
- return {
304
- env: {
305
- abort: (msgPtr: number, filePtr: number, line: number, col: number): void => {
306
- throw new WasmAbortError(
307
- readGuestString(ref, msgPtr),
308
- readGuestString(ref, filePtr),
309
- line,
310
- col,
311
- );
312
- },
313
-
314
- set_status: (code: number): void => {
315
- state.status = code >= 100 && code <= 599 ? code : 500;
316
- },
317
-
318
- set_header: (
319
- namePtr: number,
320
- nameLen: number,
321
- valPtr: number,
322
- valLen: number,
323
- ): void => {
324
- if (nameLen > MAX_HEADER_NAME_LEN)
325
- throw new Error(`header name too long: ${String(nameLen)} bytes`);
326
- if (valLen > MAX_HEADER_VALUE_LEN)
327
- throw new Error(`header value too long: ${String(valLen)} bytes`);
328
- if (state.headerBytes + nameLen + valLen > MAX_TOTAL_HEADERS_BYTES)
329
- throw new Error('total response headers exceed 64 KiB');
330
- const name = readBytes(ref, namePtr, nameLen).toString('utf8');
331
- const value = readBytes(ref, valPtr, valLen).toString('utf8');
332
- if (!TCHAR.test(name)) throw new Error(`invalid header name: ${name}`);
333
- if (/[\r\n]/.test(value)) throw new Error('header value contains CR/LF');
334
- state.headers.push([name, value]);
335
- state.headerBytes += nameLen + valLen;
336
- },
337
-
338
- respond_file: (pathPtr: number, pathLen: number): void => {
339
- if (pathLen > MAX_PATH_LEN)
340
- throw new Error(`respond_file path too long: ${String(pathLen)} bytes`);
341
- state.sendfile = readBytes(ref, pathPtr, pathLen).toString('utf8');
342
- },
343
-
344
- // Write the client's IP (set per dispatch from the connection's
345
- // remote address) into the guest buffer. Returns the byte length,
346
- // 0 if unknown, -1 if the buffer is too small. Mirrors the edge's
347
- // `client_ip_import.rs`.
348
- client_ip: (outPtr: number, cap: number): number => {
349
- const ip = state.clientIp;
350
- if (ip.length === 0) return 0;
351
- const bytes = Buffer.from(ip, 'utf8');
352
- if (bytes.length > cap) return -1;
353
- const m = mem(ref);
354
- if (outPtr < 0 || outPtr + bytes.length > m.length)
355
- throw new Error('client_ip write out of bounds');
356
- bytes.copy(m, outPtr);
357
- return bytes.length;
358
- },
359
-
360
- // `@ratelimit` decorator hook. Accounts one event for this request
361
- // against the dev limiter, keyed on the explicit guest key when
362
- // given (`keyLen > 0`), else the client IP. Returns the remaining
363
- // budget (>= 0, allowed) or a negative `Retry-After` in seconds
364
- // (denied). Mirrors the edge's `ratelimit_check_import.rs`.
365
- ratelimit_check: (
366
- routeId: number,
367
- strategy: number,
368
- limit: number,
369
- window: number,
370
- keyPtr: number,
371
- keyLen: number,
372
- ): number => {
373
- const identity =
374
- keyLen > 0
375
- ? readBytes(ref, keyPtr, keyLen).toString('utf8')
376
- : state.clientIp || '0';
377
- const d = ratelimitCheck(routeId, strategy, limit, window, identity, Date.now());
378
- return d.allowed ? 1 : -Math.max(1, d.retryAfterSecs);
379
- },
380
-
381
- // `env::email_send` (suspending on the edge) / `email_send_detached`
382
- // (non-suspending on the edge): the FULL dev email pipeline. Both map
383
- // to the same fire-and-forget dev path (a sync wasm import can't await
384
- // the provider send), and BOTH feed `__sentEmails`. See devEmailSend.
385
- email_send: (reqPtr: number, reqLen: number): number =>
386
- devEmailSend(ref, reqPtr, reqLen),
387
- email_send_detached: (reqPtr: number, reqLen: number): number =>
388
- devEmailSend(ref, reqPtr, reqLen),
389
-
390
- // `Environment.get` / `getSecure`: copy one tenant env value into the
391
- // guest buffer. Returns the byte length (0 = present-but-empty), -1 if
392
- // the buffer is too small (the guest retries bigger), -2 if absent.
393
- // Disjoint buckets: `env_get` reads vars, `env_get_secure` reads
394
- // secrets. Mirrors the edge's `env_get_import.rs`; the dev source is
395
- // `.env` (+ process.env vars) and `.env.secrets` (see ./env.ts).
396
- env_get: (keyPtr: number, keyLen: number, outPtr: number, outCap: number): number =>
397
- envLookup(ref, keyPtr, keyLen, outPtr, outCap, false),
398
- env_get_secure: (
399
- keyPtr: number,
400
- keyLen: number,
401
- outPtr: number,
402
- outCap: number,
403
- ): number => envLookup(ref, keyPtr, keyLen, outPtr, outCap, true),
404
-
405
- thread_spawn: (_startArg: number): number => -1,
406
-
407
- // `Date.now()` -> wall-clock milliseconds, matching the edge host.
408
- // The guest divides by 1000 for Unix seconds (sessions, challenges).
409
- 'Date.now': (): bigint => BigInt(Date.now()),
410
-
411
- // Web Crypto host functions (`env.crypto.*`), backed by Node's
412
- // `crypto`. The dev server skips metering, so these charge nothing.
413
- ...buildCryptoImports(ref, state.crypto),
414
-
415
- // `env::data.*`: the ToilDB data API, emulated in process (see
416
- // ./database.ts). Backs the auth example's accounts + login
417
- // challenges so register/login spans requests under `toiljs dev`;
418
- // the production edge backs the SAME imports with ScyllaDB.
419
- ...buildDatabaseImports(ref, state.db),
420
-
421
- // `env.analytics_read`: the per-domain analytics import, emulated in process (see
422
- // ../analytics/index.js). Stashes a sample TenantStats frame into the same `db.lastResult`
423
- // the data.* ops use; the production edge backs it with the real metering counters.
424
- ...buildAnalyticsImports(ref, state.db),
425
- },
426
- };
427
- }