toiljs 0.0.43 → 0.0.44

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.
@@ -0,0 +1,97 @@
1
+ # Environment variables & secrets
2
+
3
+ `Environment` gives a tenant **per-app environment variables and secrets**, set
4
+ out of band (a dashboard, like GitHub Actions) so the deployed `.wasm` carries
5
+ **no credentials**. It is read-only from app code — there is no `set`; values are
6
+ configured on the deployment side, never from the module.
7
+
8
+ ```ts
9
+ import { Response, RouteContext } from 'toiljs/server/runtime';
10
+
11
+ @rest('cfg')
12
+ class Cfg {
13
+ @get('/')
14
+ show(ctx: RouteContext): Response {
15
+ const base = Environment.get('PUBLIC_API_BASE'); // plain var, or null
16
+ const key = Environment.getSecure('STRIPE_KEY'); // secret, or null
17
+ // Use `key` to call a third party; never log it or return it to a client.
18
+ return Response.text(base != null ? base : 'unset');
19
+ }
20
+ }
21
+ ```
22
+
23
+ `Environment` is a global — no import needed (like `EmailService` / `AuthService`).
24
+
25
+ ## Two disjoint buckets
26
+
27
+ Just like GitHub Actions' `vars` vs `secrets`:
28
+
29
+ - **`Environment.get(key)`** reads **plain vars** — non-sensitive config (a public
30
+ API base URL, a feature flag, a region). Returns the string, or `null`.
31
+ - **`Environment.getSecure(key)`** reads **secrets** — sensitive values (a
32
+ third-party API key). Returns the string, or `null`.
33
+
34
+ The buckets are **disjoint**: a secret is **never** returned by `get()`, and a
35
+ plain var is never returned by `getSecure()`. That keeps a secret from leaking
36
+ through a code path that logs the result of a `get()`. Keys are case-sensitive,
37
+ exact-match.
38
+
39
+ > Secrets you read with `getSecure` are plaintext in your module at runtime
40
+ > (that's the point — you need them to call out). Don't log them, don't put them
41
+ > in a response, and don't copy them into a client bundle.
42
+
43
+ ## What is NOT here
44
+
45
+ Framework-reserved namespaces (today: **email** provider config) are **host-only**
46
+ — resolved and used in Rust where the framework needs them, and **never exposed to
47
+ the `.wasm`**. There is no `Environment.email`; you configure email in the
48
+ `[email]` block of the same env file and the platform uses it for you (see
49
+ [Email](./email.md)). The env imports only ever see your own `vars` / `secrets`.
50
+
51
+ ## Where values live
52
+
53
+ Vars and secrets live in **two separate dotenv (`.env`) files**, so the disjoint
54
+ split is structural and the secrets file can be locked down on its own. On the
55
+ edge they are per host, **out of `hosts/`** (so the config watcher never sees a
56
+ credential) — the dashboard / edge database replaces them later:
57
+
58
+ ```bash
59
+ # $TOIL_ENV_DIR/<host>.env (default dir /run/toil/env)
60
+ PUBLIC_API_BASE=https://api.example.com # -> Environment.get("PUBLIC_API_BASE")
61
+ REGION=eu
62
+
63
+ # $TOIL_ENV_DIR/<host>.env.secrets (mode 0600)
64
+ STRIPE_KEY=sk_live_xxx # -> Environment.getSecure("STRIPE_KEY")
65
+
66
+ # host-only email config — reserved TOIL_EMAIL_* keys, NEVER exposed to the .wasm
67
+ TOIL_EMAIL_ENABLED=true
68
+ TOIL_EMAIL_PROVIDER=resend
69
+ TOIL_EMAIL_FROM=noreply@example.com
70
+ TOIL_EMAIL_API_KEY=re_xxx
71
+ ```
72
+
73
+ Each file is plain dotenv: `KEY=value` per line, `#` comments, optional `export`,
74
+ optional quotes. Keys with the reserved **`TOIL_`** prefix are framework/host-only
75
+ and are stripped from BOTH guest buckets — a tenant can never read them via
76
+ `get`/`getSecure` (see [Email](./email.md) for `TOIL_EMAIL_*`).
77
+
78
+ On the edge, env is loaded **lazily** (the first time your code reads it) into a
79
+ **bounded, shared, read-only cache** with idle eviction: the data lives in one
80
+ place and is never copied per request, a host that never reads env costs nothing,
81
+ secrets are wiped when a host goes cold, and a flood of requests to many distinct
82
+ hosts can never grow memory without bound.
83
+
84
+ ## In dev
85
+
86
+ `toiljs dev` reads `.env` (vars) and `.env.secrets` (secrets) at your project
87
+ root, and overlays `process.env` as plain vars for convenience. Both are
88
+ gitignored by the scaffold. The ABI is identical to the edge, so code that runs
89
+ in dev runs on the edge.
90
+
91
+ ```bash
92
+ # .env (vars)
93
+ PUBLIC_API_BASE=http://localhost:4000
94
+
95
+ # .env.secrets (secrets; 0600; gitignored)
96
+ STRIPE_KEY=sk_test_xxx
97
+ ```
@@ -11,6 +11,7 @@ import './routes/Players';
11
11
  import './routes/Leaderboard';
12
12
  import './routes/Session';
13
13
  import './routes/PqDemo';
14
+ import './routes/EnvDemo';
14
15
  import './services/Stats';
15
16
  import './services/remotes';
16
17
 
@@ -0,0 +1,42 @@
1
+ import { Response, RouteContext } from 'toiljs/server/runtime';
2
+
3
+ /**
4
+ * Environment demo: per-tenant config + secrets, read from server code with no
5
+ * import (`Environment` is a server global).
6
+ *
7
+ * Values come from out-of-band dotenv files the edge loads lazily and never
8
+ * bundles into the `.wasm`: plain vars from `<host>.env`, secrets from
9
+ * `<host>.env.secrets`. In `toiljs dev` those are `.env` / `.env.secrets` at the
10
+ * project root — copy `.env.example` / `.env.secrets.example` to see this populate.
11
+ *
12
+ * Environment.get("KEY") -> a plain var, or null
13
+ * Environment.getSecure("KEY") -> a secret, or null (a secret is NEVER
14
+ * returned by get(), the buckets are disjoint)
15
+ *
16
+ * Reserved `TOIL_*` keys (e.g. the `TOIL_EMAIL_*` mailer config) are host-only:
17
+ * the edge reads them, but they are unreachable through get()/getSecure().
18
+ */
19
+ @rest('env')
20
+ class EnvDemo {
21
+ /** GET /env/show -> the public vars, plus WHETHER the demo secret is set.
22
+ * We return the secret's presence, not its value: getSecure() hands the guest
23
+ * the real secret, but a demo must never echo a secret back over the wire. */
24
+ @get('/show')
25
+ public show(_ctx: RouteContext): Response {
26
+ let greeting = '(unset)';
27
+ const g = Environment.get('PUBLIC_GREETING');
28
+ if (g != null) greeting = g;
29
+
30
+ let region = '(unset)';
31
+ const r = Environment.get('REGION');
32
+ if (r != null) region = r;
33
+
34
+ const apiKeySet = Environment.getSecure('DEMO_API_KEY') != null;
35
+
36
+ const body =
37
+ 'PUBLIC_GREETING=' + greeting + '\n' +
38
+ 'REGION=' + region + '\n' +
39
+ 'DEMO_API_KEY set=' + (apiKeySet ? 'yes' : 'no') + '\n';
40
+ return Response.text(body, 200);
41
+ }
42
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.43",
4
+ "version": "0.0.44",
5
5
  "author": "Dacely",
6
6
  "description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
7
7
  "repository": {
@@ -116,8 +116,8 @@
116
116
  "setup": "npm i && npm run build"
117
117
  },
118
118
  "dependencies": {
119
- "@btc-vision/post-quantum": "^0.5.3",
120
119
  "@dacely/hyper-express": "6.17.4",
120
+ "@dacely/noble-post-quantum": "^0.6.1",
121
121
  "@dacely/toilscript-loader": "^0.1.0",
122
122
  "@eslint-react/eslint-plugin": "^5.8.8",
123
123
  "@eslint/js": "^10.0.1",
@@ -0,0 +1,82 @@
1
+ // Environment: the per-tenant environment variables + secrets a tenant
2
+ // configures OUT OF BAND (a dashboard today, backed by a file; the edge DB
3
+ // later) so the deployed `.wasm` carries no credentials — the GitHub-Actions
4
+ // model. Available as a no-import global (the toilscript `--lib` mechanism, like
5
+ // `AuthService` / `EmailService`).
6
+ //
7
+ // const base = Environment.get("PUBLIC_API_BASE"); // plain var, or null
8
+ // const key = Environment.getSecure("STRIPE_KEY"); // secret, or null
9
+ //
10
+ // Two DISJOINT buckets (like GitHub `vars.*` vs `secrets.*`): `get` reads ONLY
11
+ // plain vars, `getSecure` reads ONLY secrets, so a secret can never come back
12
+ // through `get()` (and leak into a log of a `get`). READ-ONLY — there is no
13
+ // `set`; a tenant sets values on their dashboard, never from the `.wasm`.
14
+ //
15
+ // Framework-reserved namespaces (e.g. the email provider config) are HOST-ONLY:
16
+ // they are resolved and consumed in Rust where the framework uses them and are
17
+ // NEVER reachable here — these imports only see the tenant's own vars/secrets.
18
+ //
19
+ // Backed by the `env_get` / `env_get_secure` host imports (toil-backend
20
+ // `env_get_import.rs`, reading the lazy + bounded `env_cache`, plus the toiljs
21
+ // dev-server mock). A tenant that never reads env imports neither, so
22
+ // AssemblyScript tree-shakes this away.
23
+
24
+ // Host imports: copy one value into `outPtr[0..outCap]`. Return the value's byte
25
+ // length (`0` = present but empty), `-1` if `outCap` is too small (retry with a
26
+ // bigger buffer), `-2` if the key is absent. Resolved from the trusted request
27
+ // host, never anything the guest passes.
28
+ // @ts-ignore: decorator
29
+ @external('env', 'env_get')
30
+ declare function __toilEnvGet(keyPtr: usize, keyLen: i32, outPtr: usize, outCap: i32): i32;
31
+ // @ts-ignore: decorator
32
+ @external('env', 'env_get_secure')
33
+ declare function __toilEnvGetSecure(keyPtr: usize, keyLen: i32, outPtr: usize, outCap: i32): i32;
34
+
35
+ export namespace Environment {
36
+ /** The key is absent from the bucket. */
37
+ const ABSENT: i32 = -2;
38
+ /** The output buffer was too small; retry with a bigger one. */
39
+ const TOO_SMALL: i32 = -1;
40
+ /** First attempt buffer; doubles on `TOO_SMALL`. */
41
+ const INITIAL_CAP: i32 = 256;
42
+ /** Upper bound on a value, matching the host's per-value cap. */
43
+ const MAX_CAP: i32 = 64 * 1024;
44
+
45
+ /** Shared reader for both buckets; `secure` picks the secret import. */
46
+ function read(key: string, secure: bool): string | null {
47
+ const keyB = Uint8Array.wrap(String.UTF8.encode(key));
48
+ let cap = INITIAL_CAP;
49
+ while (cap <= MAX_CAP) {
50
+ const buf = new Uint8Array(cap);
51
+ const n = secure
52
+ ? __toilEnvGetSecure(keyB.dataStart, keyB.length, buf.dataStart, cap)
53
+ : __toilEnvGet(keyB.dataStart, keyB.length, buf.dataStart, cap);
54
+ if (n == ABSENT) return null;
55
+ if (n == TOO_SMALL) {
56
+ cap = cap * 2;
57
+ continue;
58
+ }
59
+ if (n < 0) return null; // unknown negative: fail closed
60
+ return String.UTF8.decodeUnsafe(buf.dataStart, n);
61
+ }
62
+ return null; // value larger than MAX_CAP: treat as absent
63
+ }
64
+
65
+ /**
66
+ * The plain environment variable `key` (a tenant var, set on the dashboard),
67
+ * or `null` if it is not set. NEVER returns a secret — use {@link getSecure}
68
+ * for those.
69
+ */
70
+ export function get(key: string): string | null {
71
+ return read(key, false);
72
+ }
73
+
74
+ /**
75
+ * The secret `key` (a tenant secret, set on the dashboard), or `null` if it
76
+ * is not set. Disjoint from {@link get}: a plain var is never returned here.
77
+ * The value is sensitive — do not log it.
78
+ */
79
+ export function getSecure(key: string): string | null {
80
+ return read(key, true);
81
+ }
82
+ }
package/src/cli/create.ts CHANGED
@@ -165,7 +165,7 @@ function scaffold(
165
165
  '.prettierignore':
166
166
  'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\n',
167
167
  '.gitignore':
168
- 'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\n',
168
+ 'node_modules\nbuild\n.toil\nshared/server.ts\ntoil-env.d.ts\ntoil-routes.d.ts\n# Local dev env vars/secrets (never commit)\n.env\n.env.secrets\n',
169
169
  // Use the project's pinned TypeScript (node_modules) instead of VS Code's bundled version.
170
170
  '.vscode/settings.json':
171
171
  JSON.stringify({ 'typescript.tsdk': 'node_modules/typescript/lib' }, null, 4) + '\n',
@@ -304,6 +304,7 @@ declare class RenderedEmail { subject: string; body: string; html: string; const
304
304
  declare class EmailTemplate { constructor(subject: string, body: string, html?: string); render(vars: Map<string, string>): RenderedEmail; send(to: string, vars: Map<string, string>, purpose?: string): EmailStatus; }
305
305
  declare enum RateLimit { FixedWindow, SlidingWindow, TokenBucket }
306
306
  declare namespace RateLimitService { function guard(routeId: i32, strategy: i32, limit: i32, window: i32): import('toiljs/server/runtime/response').Response | null; function guardKeyed(routeId: i32, strategy: i32, limit: i32, window: i32, key: string): import('toiljs/server/runtime/response').Response | null; }
307
+ declare namespace Environment { function get(key: string): string | null; function getSecure(key: string): string | null; }
307
308
  declare class TwoFactorIssue { code: string; token: string; constructor(code: string, token: string); }
308
309
  declare class TwoFactorChallenge { token: string; status: EmailStatus; constructor(token: string, status: EmailStatus); }
309
310
  declare namespace TwoFactor { const DEFAULT_TTL_SECS: u64; const DEFAULT_DIGITS: i32; function setSecret(secret: Uint8Array): void; function issue(recipient: string, purpose: string, ttlSecs?: u64, digits?: i32): TwoFactorIssue; function send(recipient: string, purpose: string, ttlSecs?: u64, digits?: i32): TwoFactorChallenge; function verify(token: string, recipient: string, code: string): bool; }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { argon2id, sha256 } from 'hash-wasm';
15
- import { ml_dsa44 } from '@btc-vision/post-quantum/ml-dsa.js';
15
+ import { ml_dsa44 } from '@dacely/noble-post-quantum/ml-dsa.js';
16
16
 
17
17
  import { DataReader, DataWriter } from 'toiljs/io';
18
18
 
@@ -117,6 +117,7 @@ export const TOIL_SERVER_ENV_DTS =
117
117
  `declare class EmailTemplate { constructor(subject: string, body: string, html?: string); render(vars: Map<string, string>): RenderedEmail; send(to: string, vars: Map<string, string>, purpose?: string): EmailStatus; }\n` +
118
118
  `declare enum RateLimit { FixedWindow, SlidingWindow, TokenBucket }\n` +
119
119
  `declare namespace RateLimitService { function guard(routeId: i32, strategy: i32, limit: i32, window: i32): import('toiljs/server/runtime/response').Response | null; function guardKeyed(routeId: i32, strategy: i32, limit: i32, window: i32, key: string): import('toiljs/server/runtime/response').Response | null; }\n` +
120
+ `declare namespace Environment { function get(key: string): string | null; function getSecure(key: string): string | null; }\n` +
120
121
  `declare class TwoFactorIssue { code: string; token: string; constructor(code: string, token: string); }\n` +
121
122
  `declare class TwoFactorChallenge { token: string; status: EmailStatus; constructor(token: string, status: EmailStatus); }\n` +
122
123
  `declare namespace TwoFactor { const DEFAULT_TTL_SECS: u64; const DEFAULT_DIGITS: i32; function setSecret(secret: Uint8Array): void; function issue(recipient: string, purpose: string, ttlSecs?: u64, digits?: i32): TwoFactorIssue; function send(recipient: string, purpose: string, ttlSecs?: u64, digits?: i32): TwoFactorChallenge; function verify(token: string, recipient: string, code: string): bool; }\n` +
@@ -17,7 +17,7 @@
17
17
 
18
18
  import * as nodeCrypto from 'node:crypto';
19
19
 
20
- import { ml_dsa44 } from '@btc-vision/post-quantum/ml-dsa.js';
20
+ import { ml_dsa44 } from '@dacely/noble-post-quantum/ml-dsa.js';
21
21
 
22
22
  import type { MemoryRef } from './host.js';
23
23
 
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Dev-only source for `Environment.get` / `getSecure`, mirroring the edge's
3
+ * per-tenant env store. Reads two optional dotenv files at the project root:
4
+ *
5
+ * - `.env` -> plain vars (`Environment.get`); `process.env` overlays
6
+ * - `.env.secrets` -> secrets (`Environment.getSecure`); keep gitignored
7
+ *
8
+ * DISJOINT like the edge: `get` sees only vars, `getSecure` only secrets, so a
9
+ * secret never comes back through `get`. Framework-reserved `TOIL_*` keys are
10
+ * host-only and stripped from BOTH buckets (a tenant can't read them).
11
+ *
12
+ * Cached after first read; restart `toiljs dev` to pick up edits. The edge
13
+ * resolves this PER TENANT from `$TOIL_ENV_DIR/<host>.env` + `<host>.env.secrets`
14
+ * (and the edge DB later) through a lazy, bounded cache; dev has a single
15
+ * project, so two files are enough and no eviction is needed.
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ /** Keys with this prefix are framework-reserved/host-only (never exposed). */
21
+ const RESERVED_PREFIX = 'TOIL_';
22
+
23
+ interface DevEnv {
24
+ vars: Map<string, string>;
25
+ secrets: Map<string, string>;
26
+ }
27
+
28
+ let cache: DevEnv | null = null;
29
+
30
+ /** Parse one dotenv value: take inside matching quotes, else cut an inline ` #`. */
31
+ function parseValue(rest: string): string {
32
+ const q = rest[0];
33
+ if (q === '"' || q === "'") {
34
+ const end = rest.indexOf(q, 1);
35
+ return end < 0 ? rest.slice(1) : rest.slice(1, end);
36
+ }
37
+ const hash = rest.indexOf(' #');
38
+ return (hash < 0 ? rest : rest.slice(0, hash)).trimEnd();
39
+ }
40
+
41
+ /** Minimal dotenv parser: `KEY=value`, `#` comments, optional `export`, quotes. */
42
+ function parseDotenv(text: string, into: Map<string, string>): void {
43
+ for (const raw of text.split('\n')) {
44
+ let line = raw.trim();
45
+ if (line.length === 0 || line.startsWith('#')) continue;
46
+ if (line.startsWith('export ')) line = line.slice('export '.length);
47
+ const eq = line.indexOf('=');
48
+ if (eq < 0) continue;
49
+ const key = line.slice(0, eq).trim();
50
+ if (key.length === 0 || key.startsWith(RESERVED_PREFIX)) continue; // reserved/host-only
51
+ into.set(key, parseValue(line.slice(eq + 1).trim()));
52
+ }
53
+ }
54
+
55
+ function readFileInto(file: string, into: Map<string, string>): void {
56
+ try {
57
+ parseDotenv(fs.readFileSync(path.join(process.cwd(), file), 'utf8'), into);
58
+ } catch {
59
+ /* file absent: skip */
60
+ }
61
+ }
62
+
63
+ function load(): DevEnv {
64
+ if (cache) return cache;
65
+ const vars = new Map<string, string>();
66
+ const secrets = new Map<string, string>();
67
+ // process.env overlays as plain vars (convenient in dev); never as secrets.
68
+ for (const [k, v] of Object.entries(process.env)) {
69
+ if (typeof v === 'string' && !k.startsWith(RESERVED_PREFIX)) vars.set(k, v);
70
+ }
71
+ readFileInto('.env', vars);
72
+ readFileInto('.env.secrets', secrets);
73
+ cache = { vars, secrets };
74
+ return cache;
75
+ }
76
+
77
+ /** A plain var by exact key, or `null`. Reads ONLY the `.env` (vars) bucket. */
78
+ export function devEnvGet(key: string): string | null {
79
+ const e = load();
80
+ return e.vars.has(key) ? (e.vars.get(key) as string) : null;
81
+ }
82
+
83
+ /** A secret by exact key, or `null`. Reads ONLY the `.env.secrets` bucket. */
84
+ export function devEnvGetSecure(key: string): string | null {
85
+ const e = load();
86
+ return e.secrets.has(key) ? (e.secrets.get(key) as string) : null;
87
+ }
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import { buildCryptoImports, freshCryptoState, type CryptoState } from './crypto.js';
21
+ import { devEnvGet, devEnvGetSecure } from './env.js';
21
22
  import { ratelimitCheck } from './ratelimit.js';
22
23
 
23
24
  /** Limits identical to the edge's `set_header` / `respond_file` bounds. */
@@ -105,6 +106,32 @@ function readGuestString(ref: MemoryRef, ptr: number): string {
105
106
  return m.toString('utf16le', ptr, ptr + byteLen);
106
107
  }
107
108
 
109
+ /**
110
+ * Resolve one `Environment.get`/`getSecure` lookup against the dev env source
111
+ * and write it into the guest buffer, with the edge's return protocol: the value
112
+ * byte length (`0` = present-but-empty), `-1` if `outCap` is too small (the guest
113
+ * retries with a bigger buffer), `-2` if the key is absent.
114
+ */
115
+ function envLookup(
116
+ ref: MemoryRef,
117
+ keyPtr: number,
118
+ keyLen: number,
119
+ outPtr: number,
120
+ outCap: number,
121
+ secure: boolean,
122
+ ): number {
123
+ const key = readBytes(ref, keyPtr, keyLen).toString('utf8');
124
+ const val = secure ? devEnvGetSecure(key) : devEnvGet(key);
125
+ if (val === null) return -2; // ABSENT
126
+ const bytes = Buffer.from(val, 'utf8');
127
+ if (bytes.length > outCap) return -1; // TOO_SMALL
128
+ const m = mem(ref);
129
+ if (outPtr < 0 || outPtr + bytes.length > m.length)
130
+ throw new Error('env_get write out of bounds');
131
+ bytes.copy(m, outPtr);
132
+ return bytes.length;
133
+ }
134
+
108
135
  /**
109
136
  * Build the `env` import object for one instance. `state` collects what the
110
137
  * imperative imports produce during a dispatch; bind a fresh state per request.
@@ -200,6 +227,21 @@ export function buildHostImports(ref: MemoryRef, state: DispatchState): WebAssem
200
227
  return 0; // EmailStatus.Sent
201
228
  },
202
229
 
230
+ // `Environment.get` / `getSecure`: copy one tenant env value into the
231
+ // guest buffer. Returns the byte length (0 = present-but-empty), -1 if
232
+ // the buffer is too small (the guest retries bigger), -2 if absent.
233
+ // Disjoint buckets: `env_get` reads vars, `env_get_secure` reads
234
+ // secrets. Mirrors the edge's `env_get_import.rs`; the dev source is
235
+ // `.env` (+ process.env vars) and `.env.secrets` (see ./env.ts).
236
+ env_get: (keyPtr: number, keyLen: number, outPtr: number, outCap: number): number =>
237
+ envLookup(ref, keyPtr, keyLen, outPtr, outCap, false),
238
+ env_get_secure: (
239
+ keyPtr: number,
240
+ keyLen: number,
241
+ outPtr: number,
242
+ outCap: number,
243
+ ): number => envLookup(ref, keyPtr, keyLen, outPtr, outCap, true),
244
+
203
245
  thread_spawn: (_startArg: number): number => -1,
204
246
 
205
247
  // `Date.now()` -> wall-clock milliseconds, matching the edge host.
@@ -53,7 +53,7 @@ interface HandleExports {
53
53
  /** Host functions the dev server provides under `env` (see `host.ts`). */
54
54
  const PROVIDED_IMPORTS = new Set([
55
55
  'abort', 'set_status', 'set_header', 'respond_file', 'thread_spawn', 'Date.now',
56
- 'client_ip', 'ratelimit_check', 'email_send',
56
+ 'client_ip', 'ratelimit_check', 'email_send', 'env_get', 'env_get_secure',
57
57
  // Web Crypto host functions (see ./crypto.ts).
58
58
  'crypto.fill_random', 'crypto.random_uuid', 'crypto.take_result', 'crypto.digest',
59
59
  'crypto.import_key', 'crypto.export_key', 'crypto.encrypt', 'crypto.decrypt',