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.
- package/CHANGELOG.md +10 -0
- package/README.md +16 -7
- package/TYPESCRIPT_LAW.md +1946 -2073
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2375 -258
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.js +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/auth-emails.js +7 -3
- package/build/compiler/emails.js +1 -1
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +25 -4
- package/build/compiler/pages.js +2 -3
- package/build/compiler/prerender.d.ts +3 -8
- package/build/compiler/prerender.js +88 -127
- package/build/compiler/ssg.js +3 -4
- package/build/compiler/template-build.js +3 -4
- package/build/compiler/toil-docs.generated.js +6 -5
- package/build/compiler/vite.js +3 -6
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/database.js +7 -3
- package/build/devserver/db/types.js +1 -1
- package/build/devserver/production.js +4 -3
- package/build/devserver/runtime/crypto.d.ts +1 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/examples/basic/client/routes/analytics.tsx +7 -5
- package/examples/basic/server/tsconfig.json +1 -3
- package/package.json +45 -44
- package/presets/no-uint8array-tostring.js +120 -153
- package/presets/oxlint.d.ts +5 -0
- package/presets/oxlint.js +224 -0
- package/server/runtime/tsconfig.json +1 -3
- package/tsconfig.json +2 -11
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
- package/.github/changelog-config.json +0 -45
- package/.github/dependabot.yml +0 -27
- package/.github/workflows/ci.yml +0 -222
- package/.vscode/settings.json +0 -10
- package/docs/README.md +0 -107
- package/docs/auth/README.md +0 -104
- package/docs/auth/configuration.md +0 -105
- package/docs/auth/emails.md +0 -92
- package/docs/auth/extending.md +0 -202
- package/docs/auth/how-it-works.md +0 -138
- package/docs/auth/usage.md +0 -188
- package/docs/backend/README.md +0 -143
- package/docs/backend/data.md +0 -351
- package/docs/backend/rest.md +0 -402
- package/docs/backend/rpc.md +0 -226
- package/docs/background/README.md +0 -114
- package/docs/background/daemons.md +0 -276
- package/docs/background/derive.md +0 -179
- package/docs/cli/README.md +0 -379
- package/docs/concepts/ai-guide.md +0 -132
- package/docs/concepts/config.md +0 -416
- package/docs/concepts/decorators.md +0 -127
- package/docs/concepts/security.md +0 -108
- package/docs/concepts/tiers.md +0 -166
- package/docs/concepts/types.md +0 -216
- package/docs/database/README.md +0 -143
- package/docs/database/capacity.md +0 -350
- package/docs/database/counters.md +0 -174
- package/docs/database/documents.md +0 -277
- package/docs/database/events.md +0 -307
- package/docs/database/membership.md +0 -246
- package/docs/database/setup.md +0 -155
- package/docs/database/unique.md +0 -216
- package/docs/database/views.md +0 -246
- package/docs/frontend/README.md +0 -104
- package/docs/frontend/components.md +0 -286
- package/docs/frontend/data-fetching.md +0 -260
- package/docs/frontend/images.md +0 -148
- package/docs/frontend/metadata.md +0 -344
- package/docs/frontend/navigation.md +0 -296
- package/docs/frontend/rendering.md +0 -236
- package/docs/frontend/routing.md +0 -346
- package/docs/frontend/scripts.md +0 -118
- package/docs/frontend/search.md +0 -191
- package/docs/frontend/styling.md +0 -147
- package/docs/frontend/toil-global.md +0 -216
- package/docs/getting-started/README.md +0 -81
- package/docs/getting-started/create-project.md +0 -131
- package/docs/getting-started/deploy.md +0 -101
- package/docs/getting-started/first-app.md +0 -215
- package/docs/getting-started/installation.md +0 -124
- package/docs/getting-started/migrating.md +0 -125
- package/docs/getting-started/project-structure.md +0 -163
- package/docs/introduction/README.md +0 -66
- package/docs/introduction/design-principles.md +0 -81
- package/docs/introduction/distributed.md +0 -91
- package/docs/introduction/how-it-works.md +0 -121
- package/docs/introduction/hyperscale.md +0 -70
- package/docs/introduction/modern-stack.md +0 -85
- package/docs/introduction/vs-other-frameworks.md +0 -64
- package/docs/introduction/why-toil.md +0 -99
- package/docs/llms.txt +0 -95
- package/docs/realtime/README.md +0 -102
- package/docs/realtime/channels.md +0 -211
- package/docs/realtime/streams.md +0 -369
- package/docs/services/README.md +0 -60
- package/docs/services/analytics.md +0 -279
- package/docs/services/caching.md +0 -175
- package/docs/services/cookies.md +0 -235
- package/docs/services/crypto.md +0 -209
- package/docs/services/email.md +0 -289
- package/docs/services/environment.md +0 -141
- package/docs/services/ratelimit.md +0 -174
- package/docs/services/time.md +0 -85
- package/eslint.config.js +0 -57
- package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
- package/presets/eslint.js +0 -88
- package/src/backend/index.ts +0 -204
- package/src/cli/configure.ts +0 -363
- package/src/cli/create.ts +0 -924
- package/src/cli/db.ts +0 -158
- package/src/cli/diagnostics.ts +0 -665
- package/src/cli/doctor.ts +0 -982
- package/src/cli/features.ts +0 -170
- package/src/cli/index.ts +0 -326
- package/src/cli/notify.ts +0 -120
- package/src/cli/proc.ts +0 -56
- package/src/cli/ui.ts +0 -169
- package/src/cli/update.ts +0 -248
- package/src/cli/updates.ts +0 -92
- package/src/cli/validate.ts +0 -31
- package/src/cli/version-check.ts +0 -125
- package/src/client/auth.ts +0 -869
- package/src/client/channel/channel.ts +0 -146
- package/src/client/components/Form.tsx +0 -65
- package/src/client/components/Image.tsx +0 -160
- package/src/client/components/Script.tsx +0 -113
- package/src/client/components/Slot.tsx +0 -21
- package/src/client/dev/devtools.tsx +0 -1089
- package/src/client/dev/error-overlay.tsx +0 -225
- package/src/client/errors.ts +0 -11
- package/src/client/head/head.ts +0 -182
- package/src/client/head/metadata.ts +0 -110
- package/src/client/index.ts +0 -130
- package/src/client/navigation/Link.tsx +0 -108
- package/src/client/navigation/NavLink.tsx +0 -86
- package/src/client/navigation/navigation.ts +0 -235
- package/src/client/navigation/prefetch.ts +0 -169
- package/src/client/navigation/scroll.ts +0 -53
- package/src/client/routing/Router.tsx +0 -216
- package/src/client/routing/action.ts +0 -122
- package/src/client/routing/error-boundary.tsx +0 -43
- package/src/client/routing/hooks.ts +0 -140
- package/src/client/routing/lazy.ts +0 -77
- package/src/client/routing/loader.ts +0 -381
- package/src/client/routing/match.ts +0 -47
- package/src/client/routing/mount.tsx +0 -90
- package/src/client/routing/params-context.ts +0 -10
- package/src/client/routing/slot-context.ts +0 -7
- package/src/client/rpc.ts +0 -97
- package/src/client/search/search.ts +0 -189
- package/src/client/search/use-page-search.ts +0 -73
- package/src/client/ssr/markers.tsx +0 -172
- package/src/client/stream/client.ts +0 -237
- package/src/client/types.ts +0 -73
- package/src/compiler/auth-emails.ts +0 -312
- package/src/compiler/config.ts +0 -369
- package/src/compiler/docs.ts +0 -127
- package/src/compiler/email-preview.ts +0 -314
- package/src/compiler/emails.ts +0 -404
- package/src/compiler/fonts.ts +0 -89
- package/src/compiler/generate.ts +0 -471
- package/src/compiler/image-blur.ts +0 -42
- package/src/compiler/image-report.ts +0 -88
- package/src/compiler/index.ts +0 -1362
- package/src/compiler/pages.ts +0 -49
- package/src/compiler/plugin.ts +0 -391
- package/src/compiler/prerender.ts +0 -263
- package/src/compiler/routes.ts +0 -170
- package/src/compiler/seo.ts +0 -415
- package/src/compiler/sri.ts +0 -144
- package/src/compiler/ssg.ts +0 -177
- package/src/compiler/ssr-codegen.ts +0 -85
- package/src/compiler/template-build.ts +0 -676
- package/src/compiler/template.ts +0 -262
- package/src/compiler/toil-docs.generated.ts +0 -77
- package/src/compiler/vite.ts +0 -245
- package/src/devserver/analytics/index.ts +0 -317
- package/src/devserver/config/dotenv.ts +0 -102
- package/src/devserver/config/env.ts +0 -30
- package/src/devserver/config/ratelimit.ts +0 -123
- package/src/devserver/daemon/catalog.ts +0 -120
- package/src/devserver/daemon/cron.ts +0 -87
- package/src/devserver/daemon/host.ts +0 -134
- package/src/devserver/daemon/index.ts +0 -369
- package/src/devserver/daemon/runtime.ts +0 -48
- package/src/devserver/db/catalog.ts +0 -109
- package/src/devserver/db/database.ts +0 -1834
- package/src/devserver/db/derives.ts +0 -121
- package/src/devserver/db/index.ts +0 -20
- package/src/devserver/db/routeKinds.ts +0 -191
- package/src/devserver/db/types.ts +0 -168
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +0 -141
- package/src/devserver/email/console.ts +0 -348
- package/src/devserver/email/index.ts +0 -155
- package/src/devserver/email/providers.ts +0 -131
- package/src/devserver/email/status.ts +0 -23
- package/src/devserver/email/validate.ts +0 -37
- package/src/devserver/email/wire.ts +0 -55
- package/src/devserver/http/cache.ts +0 -0
- package/src/devserver/http/envelope.ts +0 -157
- package/src/devserver/http/proxy.ts +0 -172
- package/src/devserver/http/runtime.ts +0 -387
- package/src/devserver/index.ts +0 -43
- package/src/devserver/production-ipc.ts +0 -63
- package/src/devserver/production-worker.ts +0 -83
- package/src/devserver/production.ts +0 -718
- package/src/devserver/runtime/crypto.ts +0 -587
- package/src/devserver/runtime/host.ts +0 -427
- package/src/devserver/runtime/module.ts +0 -442
- package/src/devserver/server.ts +0 -251
- package/src/devserver/ssr.ts +0 -208
- package/src/devserver/stream/catalog.ts +0 -106
- package/src/devserver/stream/host.ts +0 -42
- package/src/devserver/stream/index.ts +0 -301
- package/src/devserver/stream/manager.ts +0 -167
- package/src/devserver/stream/router.ts +0 -101
- package/src/devserver/stream/wire.ts +0 -58
- package/src/devserver/stream/ws.ts +0 -78
- package/src/devserver/wasm/sections.ts +0 -59
- package/src/devserver/wasm/surface.ts +0 -115
- package/src/io/FastMap.ts +0 -151
- package/src/io/FastSet.ts +0 -113
- package/src/io/codec.ts +0 -362
- package/src/io/index.ts +0 -10
- package/src/io/lengths.ts +0 -14
- package/src/io/types.ts +0 -19
- package/src/logger/index.ts +0 -22
- package/src/shared/index.ts +0 -46
- package/test/analytics-dev.test.ts +0 -113
- package/test/assembly/aspect-shim.ts +0 -33
- package/test/assembly/cookie.spec.ts +0 -303
- package/test/assembly/example.spec.ts +0 -22
- package/test/assembly/ssr.spec.ts +0 -95
- package/test/assembly.test.ts +0 -59
- package/test/auth-emails.test.ts +0 -118
- package/test/built-ssr.test.ts +0 -148
- package/test/channel.test.ts +0 -21
- package/test/config-host-port.test.ts +0 -59
- package/test/configure.test.ts +0 -94
- package/test/daemon-build.test.ts +0 -278
- package/test/daemon-catalog.test.ts +0 -349
- package/test/daemon-emulation.test.ts +0 -256
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +0 -1102
- package/test/devserver-email.test.ts +0 -306
- package/test/devserver-pqauth.test.ts +0 -93
- package/test/devserver-secrets.test.ts +0 -63
- package/test/devserver.test.ts +0 -259
- package/test/doctor.test.ts +0 -298
- package/test/dom/Image.test.tsx +0 -144
- package/test/dom/Link.test.tsx +0 -67
- package/test/dom/NavLink.test.tsx +0 -37
- package/test/dom/Script.test.tsx +0 -48
- package/test/dom/action.test.tsx +0 -146
- package/test/dom/error-overlay.test.tsx +0 -44
- package/test/dom/loader.test.tsx +0 -121
- package/test/dom/navigation.test.ts +0 -59
- package/test/dom/revalidate.test.tsx +0 -38
- package/test/dom/route-head.test.tsx +0 -56
- package/test/dom/router-loading.test.tsx +0 -44
- package/test/dom/scroll.test.ts +0 -56
- package/test/dom/slot.test.tsx +0 -131
- package/test/dom/use-metadata.test.tsx +0 -58
- package/test/dom/view-transitions.test.tsx +0 -53
- package/test/dynamic-routing.test.ts +0 -132
- package/test/email-console.test.ts +0 -86
- package/test/email-preview.test.ts +0 -73
- package/test/emails.test.ts +0 -58
- package/test/errors.test.ts +0 -21
- package/test/example-guestbook.test.ts +0 -123
- package/test/features.test.ts +0 -149
- package/test/fixtures/bignum-wire/spec.ts +0 -25
- package/test/fixtures/daemon-app.ts +0 -65
- package/test/fixtures/stream-echo.ts +0 -26
- package/test/fixtures/stream-gate.ts +0 -28
- package/test/fixtures/stream-trap.ts +0 -18
- package/test/fixtures/stream-typed.ts +0 -41
- package/test/fonts.test.ts +0 -28
- package/test/global-setup.ts +0 -17
- package/test/head.test.ts +0 -42
- package/test/image-blur.test.ts +0 -50
- package/test/io.test.ts +0 -117
- package/test/metadata.test.ts +0 -40
- package/test/navlink.test.ts +0 -28
- package/test/pages.test.ts +0 -105
- package/test/placeholder.test.ts +0 -9
- package/test/pqauth-e2e.test.ts +0 -216
- package/test/pqauth-email.test.ts +0 -801
- package/test/prerender.test.ts +0 -116
- package/test/prettier-plugin.test.ts +0 -46
- package/test/routes.test.ts +0 -76
- package/test/rpc-bignum-wire.test.ts +0 -164
- package/test/rpc-dispatch.test.ts +0 -132
- package/test/rpc-kinds.test.ts +0 -18
- package/test/rpc.test.ts +0 -66
- package/test/search.test.ts +0 -114
- package/test/seo.test.ts +0 -175
- package/test/slot-layouts.test.ts +0 -69
- package/test/sri.test.ts +0 -129
- package/test/ssg.test.ts +0 -36
- package/test/ssr-hydration.test.tsx +0 -122
- package/test/ssr-render.test.ts +0 -197
- package/test/ssr-template.test.tsx +0 -482
- package/test/stream-emulation.test.ts +0 -479
- package/test/ui.test.ts +0 -40
- package/test/update.test.ts +0 -58
- package/test/validate.test.ts +0 -42
- package/test/version-check.test.ts +0 -130
- package/tests/data-parity/generated-parity.ts +0 -99
- package/tests/data-parity/parity.ts +0 -80
- 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
|
-
}
|