toiljs 0.0.116 → 0.0.118

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 (337) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +16 -7
  3. package/build/backend/.tsbuildinfo +1 -1
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +2375 -258
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/client/auth.d.ts +3 -0
  8. package/build/client/auth.js +19 -10
  9. package/build/client/dev/devtools.js +1 -1
  10. package/build/client/dev/error-overlay.js +1 -1
  11. package/build/client/index.d.ts +1 -1
  12. package/build/client/index.js +1 -1
  13. package/build/client/navigation/navigation.js +28 -3
  14. package/build/client/routing/Router.js +5 -2
  15. package/build/client/routing/mount.js +1 -1
  16. package/build/compiler/.tsbuildinfo +1 -1
  17. package/build/compiler/auth-emails.js +7 -3
  18. package/build/compiler/emails.js +1 -1
  19. package/build/compiler/index.d.ts +1 -0
  20. package/build/compiler/index.js +26 -5
  21. package/build/compiler/pages.js +2 -3
  22. package/build/compiler/prerender.d.ts +3 -8
  23. package/build/compiler/prerender.js +88 -127
  24. package/build/compiler/ssg.js +3 -4
  25. package/build/compiler/template-build.js +3 -4
  26. package/build/compiler/toil-docs.generated.js +8 -7
  27. package/build/compiler/vite.js +4 -6
  28. package/build/devserver/.tsbuildinfo +1 -1
  29. package/build/devserver/db/database.js +7 -3
  30. package/build/devserver/db/types.js +1 -1
  31. package/build/devserver/production.js +4 -3
  32. package/build/devserver/runtime/crypto.d.ts +1 -1
  33. package/build/io/.tsbuildinfo +1 -1
  34. package/build/logger/.tsbuildinfo +1 -1
  35. package/build/shared/.tsbuildinfo +1 -1
  36. package/examples/basic/client/routes/analytics.tsx +7 -5
  37. package/examples/basic/client/routes/pq.tsx +2 -1
  38. package/examples/basic/server/tsconfig.json +1 -3
  39. package/package.json +45 -44
  40. package/presets/no-uint8array-tostring.js +120 -153
  41. package/presets/oxlint.d.ts +5 -0
  42. package/presets/oxlint.js +224 -0
  43. package/server/auth/AuthController.ts +53 -14
  44. package/server/auth/FactorSettings.ts +41 -0
  45. package/server/auth/SessionEpochs.ts +58 -0
  46. package/server/auth/SessionRevocations.ts +13 -0
  47. package/server/globals/auth.ts +27 -2
  48. package/server/runtime/tsconfig.json +1 -3
  49. package/tsconfig.json +2 -11
  50. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  51. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  52. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  53. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  54. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  55. package/.github/changelog-config.json +0 -45
  56. package/.github/dependabot.yml +0 -27
  57. package/.github/workflows/ci.yml +0 -222
  58. package/.vscode/settings.json +0 -10
  59. package/docs/README.md +0 -107
  60. package/docs/auth/README.md +0 -104
  61. package/docs/auth/configuration.md +0 -105
  62. package/docs/auth/emails.md +0 -92
  63. package/docs/auth/extending.md +0 -202
  64. package/docs/auth/how-it-works.md +0 -138
  65. package/docs/auth/usage.md +0 -188
  66. package/docs/backend/README.md +0 -143
  67. package/docs/backend/data.md +0 -351
  68. package/docs/backend/rest.md +0 -402
  69. package/docs/backend/rpc.md +0 -226
  70. package/docs/background/README.md +0 -114
  71. package/docs/background/daemons.md +0 -276
  72. package/docs/background/derive.md +0 -179
  73. package/docs/cli/README.md +0 -379
  74. package/docs/concepts/ai-guide.md +0 -132
  75. package/docs/concepts/config.md +0 -416
  76. package/docs/concepts/decorators.md +0 -127
  77. package/docs/concepts/security.md +0 -108
  78. package/docs/concepts/tiers.md +0 -166
  79. package/docs/concepts/types.md +0 -216
  80. package/docs/database/README.md +0 -143
  81. package/docs/database/capacity.md +0 -350
  82. package/docs/database/counters.md +0 -174
  83. package/docs/database/documents.md +0 -277
  84. package/docs/database/events.md +0 -307
  85. package/docs/database/membership.md +0 -246
  86. package/docs/database/setup.md +0 -155
  87. package/docs/database/unique.md +0 -216
  88. package/docs/database/views.md +0 -246
  89. package/docs/frontend/README.md +0 -104
  90. package/docs/frontend/components.md +0 -286
  91. package/docs/frontend/data-fetching.md +0 -260
  92. package/docs/frontend/images.md +0 -148
  93. package/docs/frontend/metadata.md +0 -344
  94. package/docs/frontend/navigation.md +0 -296
  95. package/docs/frontend/rendering.md +0 -236
  96. package/docs/frontend/routing.md +0 -346
  97. package/docs/frontend/scripts.md +0 -118
  98. package/docs/frontend/search.md +0 -191
  99. package/docs/frontend/styling.md +0 -147
  100. package/docs/frontend/toil-global.md +0 -216
  101. package/docs/getting-started/README.md +0 -81
  102. package/docs/getting-started/create-project.md +0 -131
  103. package/docs/getting-started/deploy.md +0 -101
  104. package/docs/getting-started/first-app.md +0 -215
  105. package/docs/getting-started/installation.md +0 -124
  106. package/docs/getting-started/migrating.md +0 -125
  107. package/docs/getting-started/project-structure.md +0 -163
  108. package/docs/introduction/README.md +0 -66
  109. package/docs/introduction/design-principles.md +0 -81
  110. package/docs/introduction/distributed.md +0 -91
  111. package/docs/introduction/how-it-works.md +0 -121
  112. package/docs/introduction/hyperscale.md +0 -70
  113. package/docs/introduction/modern-stack.md +0 -85
  114. package/docs/introduction/vs-other-frameworks.md +0 -64
  115. package/docs/introduction/why-toil.md +0 -99
  116. package/docs/llms.txt +0 -95
  117. package/docs/realtime/README.md +0 -102
  118. package/docs/realtime/channels.md +0 -211
  119. package/docs/realtime/streams.md +0 -369
  120. package/docs/services/README.md +0 -60
  121. package/docs/services/analytics.md +0 -279
  122. package/docs/services/caching.md +0 -175
  123. package/docs/services/cookies.md +0 -235
  124. package/docs/services/crypto.md +0 -209
  125. package/docs/services/email.md +0 -289
  126. package/docs/services/environment.md +0 -141
  127. package/docs/services/ratelimit.md +0 -174
  128. package/docs/services/time.md +0 -85
  129. package/eslint.config.js +0 -57
  130. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  131. package/presets/eslint.js +0 -88
  132. package/src/backend/index.ts +0 -204
  133. package/src/cli/configure.ts +0 -363
  134. package/src/cli/create.ts +0 -924
  135. package/src/cli/db.ts +0 -158
  136. package/src/cli/diagnostics.ts +0 -665
  137. package/src/cli/doctor.ts +0 -982
  138. package/src/cli/features.ts +0 -170
  139. package/src/cli/index.ts +0 -326
  140. package/src/cli/notify.ts +0 -120
  141. package/src/cli/proc.ts +0 -56
  142. package/src/cli/ui.ts +0 -169
  143. package/src/cli/update.ts +0 -248
  144. package/src/cli/updates.ts +0 -92
  145. package/src/cli/validate.ts +0 -31
  146. package/src/cli/version-check.ts +0 -125
  147. package/src/client/auth.ts +0 -869
  148. package/src/client/channel/channel.ts +0 -146
  149. package/src/client/components/Form.tsx +0 -65
  150. package/src/client/components/Image.tsx +0 -160
  151. package/src/client/components/Script.tsx +0 -113
  152. package/src/client/components/Slot.tsx +0 -21
  153. package/src/client/dev/devtools.tsx +0 -1089
  154. package/src/client/dev/error-overlay.tsx +0 -225
  155. package/src/client/errors.ts +0 -11
  156. package/src/client/head/head.ts +0 -182
  157. package/src/client/head/metadata.ts +0 -110
  158. package/src/client/index.ts +0 -130
  159. package/src/client/navigation/Link.tsx +0 -108
  160. package/src/client/navigation/NavLink.tsx +0 -86
  161. package/src/client/navigation/navigation.ts +0 -235
  162. package/src/client/navigation/prefetch.ts +0 -169
  163. package/src/client/navigation/scroll.ts +0 -53
  164. package/src/client/routing/Router.tsx +0 -216
  165. package/src/client/routing/action.ts +0 -122
  166. package/src/client/routing/error-boundary.tsx +0 -43
  167. package/src/client/routing/hooks.ts +0 -140
  168. package/src/client/routing/lazy.ts +0 -77
  169. package/src/client/routing/loader.ts +0 -381
  170. package/src/client/routing/match.ts +0 -47
  171. package/src/client/routing/mount.tsx +0 -90
  172. package/src/client/routing/params-context.ts +0 -10
  173. package/src/client/routing/slot-context.ts +0 -7
  174. package/src/client/rpc.ts +0 -97
  175. package/src/client/search/search.ts +0 -189
  176. package/src/client/search/use-page-search.ts +0 -73
  177. package/src/client/ssr/markers.tsx +0 -172
  178. package/src/client/stream/client.ts +0 -237
  179. package/src/client/types.ts +0 -73
  180. package/src/compiler/auth-emails.ts +0 -312
  181. package/src/compiler/config.ts +0 -369
  182. package/src/compiler/docs.ts +0 -127
  183. package/src/compiler/email-preview.ts +0 -314
  184. package/src/compiler/emails.ts +0 -404
  185. package/src/compiler/fonts.ts +0 -89
  186. package/src/compiler/generate.ts +0 -471
  187. package/src/compiler/image-blur.ts +0 -42
  188. package/src/compiler/image-report.ts +0 -88
  189. package/src/compiler/index.ts +0 -1362
  190. package/src/compiler/pages.ts +0 -49
  191. package/src/compiler/plugin.ts +0 -391
  192. package/src/compiler/prerender.ts +0 -263
  193. package/src/compiler/routes.ts +0 -170
  194. package/src/compiler/seo.ts +0 -415
  195. package/src/compiler/sri.ts +0 -144
  196. package/src/compiler/ssg.ts +0 -177
  197. package/src/compiler/ssr-codegen.ts +0 -85
  198. package/src/compiler/template-build.ts +0 -676
  199. package/src/compiler/template.ts +0 -262
  200. package/src/compiler/toil-docs.generated.ts +0 -77
  201. package/src/compiler/vite.ts +0 -245
  202. package/src/devserver/analytics/index.ts +0 -317
  203. package/src/devserver/config/dotenv.ts +0 -102
  204. package/src/devserver/config/env.ts +0 -30
  205. package/src/devserver/config/ratelimit.ts +0 -123
  206. package/src/devserver/daemon/catalog.ts +0 -120
  207. package/src/devserver/daemon/cron.ts +0 -87
  208. package/src/devserver/daemon/host.ts +0 -134
  209. package/src/devserver/daemon/index.ts +0 -369
  210. package/src/devserver/daemon/runtime.ts +0 -48
  211. package/src/devserver/db/catalog.ts +0 -109
  212. package/src/devserver/db/database.ts +0 -1834
  213. package/src/devserver/db/derives.ts +0 -121
  214. package/src/devserver/db/index.ts +0 -20
  215. package/src/devserver/db/routeKinds.ts +0 -191
  216. package/src/devserver/db/types.ts +0 -168
  217. package/src/devserver/email/caps.ts +0 -0
  218. package/src/devserver/email/config.ts +0 -141
  219. package/src/devserver/email/console.ts +0 -348
  220. package/src/devserver/email/index.ts +0 -155
  221. package/src/devserver/email/providers.ts +0 -131
  222. package/src/devserver/email/status.ts +0 -23
  223. package/src/devserver/email/validate.ts +0 -37
  224. package/src/devserver/email/wire.ts +0 -55
  225. package/src/devserver/http/cache.ts +0 -0
  226. package/src/devserver/http/envelope.ts +0 -157
  227. package/src/devserver/http/proxy.ts +0 -172
  228. package/src/devserver/http/runtime.ts +0 -387
  229. package/src/devserver/index.ts +0 -43
  230. package/src/devserver/production-ipc.ts +0 -63
  231. package/src/devserver/production-worker.ts +0 -83
  232. package/src/devserver/production.ts +0 -718
  233. package/src/devserver/runtime/crypto.ts +0 -587
  234. package/src/devserver/runtime/host.ts +0 -427
  235. package/src/devserver/runtime/module.ts +0 -442
  236. package/src/devserver/server.ts +0 -251
  237. package/src/devserver/ssr.ts +0 -208
  238. package/src/devserver/stream/catalog.ts +0 -106
  239. package/src/devserver/stream/host.ts +0 -42
  240. package/src/devserver/stream/index.ts +0 -301
  241. package/src/devserver/stream/manager.ts +0 -167
  242. package/src/devserver/stream/router.ts +0 -101
  243. package/src/devserver/stream/wire.ts +0 -58
  244. package/src/devserver/stream/ws.ts +0 -78
  245. package/src/devserver/wasm/sections.ts +0 -59
  246. package/src/devserver/wasm/surface.ts +0 -115
  247. package/src/io/FastMap.ts +0 -151
  248. package/src/io/FastSet.ts +0 -113
  249. package/src/io/codec.ts +0 -362
  250. package/src/io/index.ts +0 -10
  251. package/src/io/lengths.ts +0 -14
  252. package/src/io/types.ts +0 -19
  253. package/src/logger/index.ts +0 -22
  254. package/src/shared/index.ts +0 -46
  255. package/test/analytics-dev.test.ts +0 -113
  256. package/test/assembly/aspect-shim.ts +0 -33
  257. package/test/assembly/cookie.spec.ts +0 -303
  258. package/test/assembly/example.spec.ts +0 -22
  259. package/test/assembly/ssr.spec.ts +0 -95
  260. package/test/assembly.test.ts +0 -59
  261. package/test/auth-emails.test.ts +0 -118
  262. package/test/built-ssr.test.ts +0 -148
  263. package/test/channel.test.ts +0 -21
  264. package/test/config-host-port.test.ts +0 -59
  265. package/test/configure.test.ts +0 -94
  266. package/test/daemon-build.test.ts +0 -278
  267. package/test/daemon-catalog.test.ts +0 -349
  268. package/test/daemon-emulation.test.ts +0 -256
  269. package/test/db.test.ts +0 -0
  270. package/test/devserver-database.test.ts +0 -1102
  271. package/test/devserver-email.test.ts +0 -306
  272. package/test/devserver-pqauth.test.ts +0 -93
  273. package/test/devserver-secrets.test.ts +0 -63
  274. package/test/devserver.test.ts +0 -259
  275. package/test/doctor.test.ts +0 -298
  276. package/test/dom/Image.test.tsx +0 -144
  277. package/test/dom/Link.test.tsx +0 -67
  278. package/test/dom/NavLink.test.tsx +0 -37
  279. package/test/dom/Script.test.tsx +0 -48
  280. package/test/dom/action.test.tsx +0 -146
  281. package/test/dom/error-overlay.test.tsx +0 -44
  282. package/test/dom/loader.test.tsx +0 -121
  283. package/test/dom/navigation.test.ts +0 -59
  284. package/test/dom/revalidate.test.tsx +0 -38
  285. package/test/dom/route-head.test.tsx +0 -56
  286. package/test/dom/router-loading.test.tsx +0 -44
  287. package/test/dom/scroll.test.ts +0 -56
  288. package/test/dom/slot.test.tsx +0 -131
  289. package/test/dom/use-metadata.test.tsx +0 -58
  290. package/test/dom/view-transitions.test.tsx +0 -53
  291. package/test/dynamic-routing.test.ts +0 -132
  292. package/test/email-console.test.ts +0 -86
  293. package/test/email-preview.test.ts +0 -73
  294. package/test/emails.test.ts +0 -58
  295. package/test/errors.test.ts +0 -21
  296. package/test/example-guestbook.test.ts +0 -123
  297. package/test/features.test.ts +0 -149
  298. package/test/fixtures/bignum-wire/spec.ts +0 -25
  299. package/test/fixtures/daemon-app.ts +0 -65
  300. package/test/fixtures/stream-echo.ts +0 -26
  301. package/test/fixtures/stream-gate.ts +0 -28
  302. package/test/fixtures/stream-trap.ts +0 -18
  303. package/test/fixtures/stream-typed.ts +0 -41
  304. package/test/fonts.test.ts +0 -28
  305. package/test/global-setup.ts +0 -17
  306. package/test/head.test.ts +0 -42
  307. package/test/image-blur.test.ts +0 -50
  308. package/test/io.test.ts +0 -117
  309. package/test/metadata.test.ts +0 -40
  310. package/test/navlink.test.ts +0 -28
  311. package/test/pages.test.ts +0 -105
  312. package/test/placeholder.test.ts +0 -9
  313. package/test/pqauth-e2e.test.ts +0 -216
  314. package/test/pqauth-email.test.ts +0 -801
  315. package/test/prerender.test.ts +0 -116
  316. package/test/prettier-plugin.test.ts +0 -46
  317. package/test/routes.test.ts +0 -76
  318. package/test/rpc-bignum-wire.test.ts +0 -164
  319. package/test/rpc-dispatch.test.ts +0 -132
  320. package/test/rpc-kinds.test.ts +0 -18
  321. package/test/rpc.test.ts +0 -66
  322. package/test/search.test.ts +0 -114
  323. package/test/seo.test.ts +0 -175
  324. package/test/slot-layouts.test.ts +0 -69
  325. package/test/sri.test.ts +0 -129
  326. package/test/ssg.test.ts +0 -36
  327. package/test/ssr-hydration.test.tsx +0 -122
  328. package/test/ssr-render.test.ts +0 -197
  329. package/test/ssr-template.test.tsx +0 -482
  330. package/test/stream-emulation.test.ts +0 -479
  331. package/test/ui.test.ts +0 -40
  332. package/test/update.test.ts +0 -58
  333. package/test/validate.test.ts +0 -42
  334. package/test/version-check.test.ts +0 -130
  335. package/tests/data-parity/generated-parity.ts +0 -99
  336. package/tests/data-parity/parity.ts +0 -80
  337. 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
- }