toiljs 0.0.55 → 0.0.56
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/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +9 -5
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.js +1 -1
- package/build/client/components/Image.d.ts +1 -1
- package/build/client/dev/devtools.js +3 -1
- package/build/client/index.d.ts +2 -2
- package/build/client/index.js +2 -2
- package/build/client/routing/Router.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/docs.js +1 -1
- package/build/compiler/seo.js +1 -3
- package/build/compiler/template-build.js +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/cache.js +0 -0
- package/build/devserver/crypto.js +45 -17
- package/build/devserver/database.js +82 -0
- package/build/devserver/email/caps.js +0 -0
- package/build/devserver/email/config.js +7 -2
- package/build/devserver/email/validate.js +1 -4
- package/build/devserver/index.d.ts +1 -1
- package/build/devserver/index.js +3 -2
- package/build/devserver/module.js +51 -12
- package/build/devserver/proxy.js +2 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/io/codec.d.ts +5 -5
- package/build/io/codec.js +193 -77
- package/examples/basic/client/components/HoneycombBackground.tsx +1 -1
- package/examples/basic/client/public/images/logo.svg +37 -34
- package/examples/basic/client/public/index.html +14 -14
- package/examples/basic/client/routes/auth.tsx +18 -10
- package/examples/basic/client/routes/cookies.tsx +15 -24
- package/examples/basic/client/routes/crypto.tsx +4 -5
- package/examples/basic/client/routes/features/template/template.tsx +1 -1
- package/examples/basic/client/routes/hello.tsx +1 -1
- package/examples/basic/client/routes/pq.tsx +14 -14
- package/examples/basic/client/routes/rest.tsx +1 -3
- package/examples/basic/client/styles/main.css +25 -22
- package/examples/basic/client/toil.tsx +1 -1
- package/examples/basic/server/README.md +8 -8
- package/examples/basic/server/core/AppHandler.ts +4 -7
- package/examples/basic/server/routes/Auth.ts +11 -3
- package/examples/basic/server/routes/EnvDemo.ts +9 -3
- package/package.json +1 -1
- package/src/backend/index.ts +4 -2
- package/src/cli/doctor.ts +10 -3
- package/src/cli/notify.ts +1 -6
- package/src/cli/ui.ts +3 -3
- package/src/cli/version-check.ts +5 -1
- package/src/client/auth.ts +33 -10
- package/src/client/components/Form.tsx +2 -2
- package/src/client/components/Image.tsx +1 -1
- package/src/client/components/Script.tsx +1 -1
- package/src/client/components/Slot.tsx +1 -1
- package/src/client/dev/devtools.tsx +121 -54
- package/src/client/dev/error-overlay.tsx +7 -1
- package/src/client/head/metadata.ts +1 -1
- package/src/client/index.ts +13 -2
- package/src/client/routing/Router.tsx +2 -2
- package/src/client/routing/error-boundary.tsx +1 -1
- package/src/client/routing/loader.ts +2 -2
- package/src/client/routing/mount.tsx +5 -6
- package/src/compiler/docs.ts +1 -1
- package/src/compiler/email-preview.ts +1 -1
- package/src/compiler/generate.ts +1 -1
- package/src/compiler/seo.ts +1 -3
- package/src/compiler/ssg.ts +10 -4
- package/src/compiler/template-build.ts +2 -7
- package/src/compiler/template.ts +1 -4
- package/src/compiler/vite.ts +1 -1
- package/src/devserver/cache.ts +0 -0
- package/src/devserver/crypto.ts +140 -51
- package/src/devserver/database.ts +149 -8
- package/src/devserver/dotenv.ts +10 -2
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +8 -2
- package/src/devserver/email/index.ts +3 -3
- package/src/devserver/email/validate.ts +1 -4
- package/src/devserver/envelope.ts +3 -3
- package/src/devserver/host.ts +14 -5
- package/src/devserver/index.ts +15 -6
- package/src/devserver/module.ts +56 -14
- package/src/devserver/proxy.ts +5 -7
- package/src/io/codec.ts +226 -83
- package/test/devserver-database.test.ts +60 -0
package/src/compiler/ssg.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { createServer } from 'vite';
|
|
|
16
16
|
import { type ResolvedToilConfig } from './config.js';
|
|
17
17
|
import { extractStaticMetadata, loadTypeScript } from './prerender.js';
|
|
18
18
|
import { scanRoutes } from './routes.js';
|
|
19
|
-
import { injectSeoHtml, joinUrl, llmsTxt, routeSeo, sitemapXml
|
|
19
|
+
import { injectSeoHtml, joinUrl, type LlmsPage, llmsTxt, routeSeo, sitemapXml } from './seo.js';
|
|
20
20
|
import { createViteConfig } from './vite.js';
|
|
21
21
|
|
|
22
22
|
/** Reads a string field off a metadata record, or undefined. */
|
|
@@ -91,7 +91,9 @@ export async function prerenderStaticParams(cfg: ResolvedToilConfig): Promise<st
|
|
|
91
91
|
try {
|
|
92
92
|
mod = (await server.ssrLoadModule(route.file)) as RouteModule;
|
|
93
93
|
} catch (err) {
|
|
94
|
-
warn(
|
|
94
|
+
warn(
|
|
95
|
+
`skipped ${route.pattern} (${err instanceof Error ? err.message : String(err)})`,
|
|
96
|
+
);
|
|
95
97
|
continue;
|
|
96
98
|
}
|
|
97
99
|
if (typeof mod.generateStaticParams !== 'function') continue;
|
|
@@ -116,12 +118,16 @@ export async function prerenderStaticParams(cfg: ResolvedToilConfig): Promise<st
|
|
|
116
118
|
typeof mod.loader === 'function'
|
|
117
119
|
? await mod.loader({ params, searchParams })
|
|
118
120
|
: undefined;
|
|
119
|
-
metadata = asMetadata(
|
|
121
|
+
metadata = asMetadata(
|
|
122
|
+
await mod.generateMetadata({ params, searchParams, data }),
|
|
123
|
+
);
|
|
120
124
|
} else if (mod.metadata) {
|
|
121
125
|
metadata = asMetadata(mod.metadata);
|
|
122
126
|
}
|
|
123
127
|
} catch (err) {
|
|
124
|
-
warn(
|
|
128
|
+
warn(
|
|
129
|
+
`metadata failed for ${url} (${err instanceof Error ? err.message : String(err)})`,
|
|
130
|
+
);
|
|
125
131
|
}
|
|
126
132
|
const html = injectSeoHtml(shell, routeSeo(cfg.seo, metadata, url));
|
|
127
133
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -19,12 +19,7 @@
|
|
|
19
19
|
import fs from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
|
|
22
|
-
import {
|
|
23
|
-
createElement,
|
|
24
|
-
type ComponentType,
|
|
25
|
-
type Context,
|
|
26
|
-
type ReactNode,
|
|
27
|
-
} from 'react';
|
|
22
|
+
import { type ComponentType, type Context, createElement, type ReactNode } from 'react';
|
|
28
23
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
29
24
|
import { createServer } from 'vite';
|
|
30
25
|
|
|
@@ -36,8 +31,8 @@ import {
|
|
|
36
31
|
assignSlotIds,
|
|
37
32
|
coherenceHash,
|
|
38
33
|
encodeSlots,
|
|
39
|
-
extractFromHtml,
|
|
40
34
|
type Extracted,
|
|
35
|
+
extractFromHtml,
|
|
41
36
|
} from './template.js';
|
|
42
37
|
import { createViteConfig } from './vite.js';
|
|
43
38
|
|
package/src/compiler/template.ts
CHANGED
|
@@ -249,10 +249,7 @@ export function reactEscapeHtml(s: string): string {
|
|
|
249
249
|
* any tooling that needs to materialise a full page from a template + values).
|
|
250
250
|
* `values` maps a byte offset to the bytes inserted there (offsets may repeat
|
|
251
251
|
* is not allowed; pass them in `slots` order). */
|
|
252
|
-
export function spliceTemplate(
|
|
253
|
-
tmpl: Buffer,
|
|
254
|
-
inserts: { offset: number; value: Buffer }[],
|
|
255
|
-
): Buffer {
|
|
252
|
+
export function spliceTemplate(tmpl: Buffer, inserts: { offset: number; value: Buffer }[]): Buffer {
|
|
256
253
|
const parts: Buffer[] = [];
|
|
257
254
|
let prev = 0;
|
|
258
255
|
for (const ins of inserts) {
|
package/src/compiler/vite.ts
CHANGED
|
@@ -7,7 +7,7 @@ import pc from 'picocolors';
|
|
|
7
7
|
import react from '@vitejs/plugin-react';
|
|
8
8
|
import { imagetools } from 'vite-imagetools';
|
|
9
9
|
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
|
10
|
-
import { createLogger,
|
|
10
|
+
import { createLogger, type InlineConfig, type Logger, mergeConfig, type PluginOption } from 'vite';
|
|
11
11
|
|
|
12
12
|
import { type ResolvedToilConfig } from './config.js';
|
|
13
13
|
import { fontPreloadPlugin } from './fonts.js';
|
package/src/devserver/cache.ts
CHANGED
|
Binary file
|
package/src/devserver/crypto.ts
CHANGED
|
@@ -25,9 +25,24 @@ import type { MemoryRef } from './host.js';
|
|
|
25
25
|
|
|
26
26
|
// --- ABI id tables (must match the std + Rust backend) ----------------------
|
|
27
27
|
const ALG = {
|
|
28
|
-
SHA1: 1,
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
SHA1: 1,
|
|
29
|
+
SHA256: 2,
|
|
30
|
+
SHA384: 3,
|
|
31
|
+
SHA512: 4,
|
|
32
|
+
SHA3_256: 5,
|
|
33
|
+
SHA3_384: 6,
|
|
34
|
+
SHA3_512: 7,
|
|
35
|
+
AES_GCM: 10,
|
|
36
|
+
AES_CBC: 11,
|
|
37
|
+
AES_CTR: 12,
|
|
38
|
+
AES_KW: 13,
|
|
39
|
+
HMAC: 20,
|
|
40
|
+
ECDSA: 32,
|
|
41
|
+
ED25519: 33,
|
|
42
|
+
ECDH: 50,
|
|
43
|
+
X25519: 51,
|
|
44
|
+
HKDF: 52,
|
|
45
|
+
PBKDF2: 53,
|
|
31
46
|
} as const;
|
|
32
47
|
const FMT = { RAW: 0, PKCS8: 1, SPKI: 2, JWK: 3 } as const;
|
|
33
48
|
|
|
@@ -77,7 +92,9 @@ function readBytes(ref: MemoryRef, ptr: number, len: number): Buffer {
|
|
|
77
92
|
function writeBytes(ref: MemoryRef, ptr: number, bytes: Buffer | Uint8Array): void {
|
|
78
93
|
const m = memBuf(ref);
|
|
79
94
|
if (ptr < 0 || ptr + bytes.length > m.length)
|
|
80
|
-
throw new Error(
|
|
95
|
+
throw new Error(
|
|
96
|
+
`crypto write out of bounds: ptr=${String(ptr)} len=${String(bytes.length)}`,
|
|
97
|
+
);
|
|
81
98
|
m.set(bytes, ptr);
|
|
82
99
|
}
|
|
83
100
|
|
|
@@ -85,25 +102,21 @@ function writeBytes(ref: MemoryRef, ptr: number, bytes: Buffer | Uint8Array): vo
|
|
|
85
102
|
class ParamReader {
|
|
86
103
|
private pos = 0;
|
|
87
104
|
constructor(private readonly buf: Buffer) {}
|
|
88
|
-
|
|
89
|
-
* error (trap-equivalent, caught by the dispatcher) rather than a raw
|
|
90
|
-
* Node RangeError. */
|
|
91
|
-
private need(n: number): void {
|
|
92
|
-
if (n < 0 || this.pos + n > this.buf.length)
|
|
93
|
-
throw new Error('crypto: malformed params buffer (truncated)');
|
|
94
|
-
}
|
|
105
|
+
|
|
95
106
|
readI32(): number {
|
|
96
107
|
this.need(4);
|
|
97
108
|
const v = this.buf.readInt32LE(this.pos);
|
|
98
109
|
this.pos += 4;
|
|
99
110
|
return v;
|
|
100
111
|
}
|
|
112
|
+
|
|
101
113
|
readU32(): number {
|
|
102
114
|
this.need(4);
|
|
103
115
|
const v = this.buf.readUInt32LE(this.pos);
|
|
104
116
|
this.pos += 4;
|
|
105
117
|
return v;
|
|
106
118
|
}
|
|
119
|
+
|
|
107
120
|
readBlob(): Buffer {
|
|
108
121
|
const n = this.readU32();
|
|
109
122
|
this.need(n);
|
|
@@ -111,18 +124,34 @@ class ParamReader {
|
|
|
111
124
|
this.pos += n;
|
|
112
125
|
return s;
|
|
113
126
|
}
|
|
127
|
+
|
|
128
|
+
/** Bounds-check before a read so a malformed buffer throws a controlled
|
|
129
|
+
* error (trap-equivalent, caught by the dispatcher) rather than a raw
|
|
130
|
+
* Node RangeError. */
|
|
131
|
+
private need(n: number): void {
|
|
132
|
+
if (n < 0 || this.pos + n > this.buf.length)
|
|
133
|
+
throw new Error('crypto: malformed params buffer (truncated)');
|
|
134
|
+
}
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
function hashName(id: number): string {
|
|
117
138
|
switch (id) {
|
|
118
|
-
case ALG.SHA1:
|
|
119
|
-
|
|
120
|
-
case ALG.
|
|
121
|
-
|
|
122
|
-
case ALG.
|
|
123
|
-
|
|
124
|
-
case ALG.
|
|
125
|
-
|
|
139
|
+
case ALG.SHA1:
|
|
140
|
+
return 'sha1';
|
|
141
|
+
case ALG.SHA256:
|
|
142
|
+
return 'sha256';
|
|
143
|
+
case ALG.SHA384:
|
|
144
|
+
return 'sha384';
|
|
145
|
+
case ALG.SHA512:
|
|
146
|
+
return 'sha512';
|
|
147
|
+
case ALG.SHA3_256:
|
|
148
|
+
return 'sha3-256';
|
|
149
|
+
case ALG.SHA3_384:
|
|
150
|
+
return 'sha3-384';
|
|
151
|
+
case ALG.SHA3_512:
|
|
152
|
+
return 'sha3-512';
|
|
153
|
+
default:
|
|
154
|
+
throw new Error(`crypto: bad hash id ${String(id)}`);
|
|
126
155
|
}
|
|
127
156
|
}
|
|
128
157
|
|
|
@@ -151,8 +180,7 @@ export function buildCryptoImports(
|
|
|
151
180
|
|
|
152
181
|
'crypto.take_result': (outPtr: number, outLen: number): number => {
|
|
153
182
|
const r = cs.lastResult;
|
|
154
|
-
if (!r || r.length !== outLen)
|
|
155
|
-
throw new Error('crypto.take_result: length mismatch');
|
|
183
|
+
if (!r || r.length !== outLen) throw new Error('crypto.take_result: length mismatch');
|
|
156
184
|
writeBytes(ref, outPtr, r);
|
|
157
185
|
cs.lastResult = null;
|
|
158
186
|
return r.length;
|
|
@@ -191,9 +219,15 @@ export function buildCryptoImports(
|
|
|
191
219
|
if (e.raw && format === FMT.RAW) return stash(cs, e.raw);
|
|
192
220
|
if (e.keyObject) {
|
|
193
221
|
if (format === FMT.PKCS8 && e.isPrivate)
|
|
194
|
-
return stash(
|
|
222
|
+
return stash(
|
|
223
|
+
cs,
|
|
224
|
+
e.keyObject.export({ format: 'der', type: 'pkcs8' }) as Buffer,
|
|
225
|
+
);
|
|
195
226
|
if (format === FMT.SPKI && !e.isPrivate)
|
|
196
|
-
return stash(
|
|
227
|
+
return stash(
|
|
228
|
+
cs,
|
|
229
|
+
e.keyObject.export({ format: 'der', type: 'spki' }) as Buffer,
|
|
230
|
+
);
|
|
197
231
|
}
|
|
198
232
|
return ERR_UNSUPPORTED;
|
|
199
233
|
} catch {
|
|
@@ -210,7 +244,13 @@ export function buildCryptoImports(
|
|
|
210
244
|
signOp(cs, ref, h, pp, pl, dp, dl),
|
|
211
245
|
|
|
212
246
|
'crypto.verify': (
|
|
213
|
-
h: number,
|
|
247
|
+
h: number,
|
|
248
|
+
pp: number,
|
|
249
|
+
pl: number,
|
|
250
|
+
sp: number,
|
|
251
|
+
sl: number,
|
|
252
|
+
dp: number,
|
|
253
|
+
dl: number,
|
|
214
254
|
): number => verifyOp(cs, ref, h, pp, pl, sp, sl, dp, dl),
|
|
215
255
|
|
|
216
256
|
'crypto.derive_bits': (h: number, pp: number, pl: number, lengthBits: number): number =>
|
|
@@ -220,10 +260,14 @@ export function buildCryptoImports(
|
|
|
220
260
|
// host (`mldsa_verify_import.rs`): same size asserts, 1/0/neg result.
|
|
221
261
|
// Backed by the same noble lib the client signs with, so dev == prod.
|
|
222
262
|
'crypto.mldsa_verify': (
|
|
223
|
-
pkPtr: number,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
263
|
+
pkPtr: number,
|
|
264
|
+
pkLen: number,
|
|
265
|
+
msgPtr: number,
|
|
266
|
+
msgLen: number,
|
|
267
|
+
sigPtr: number,
|
|
268
|
+
sigLen: number,
|
|
269
|
+
ctxPtr: number,
|
|
270
|
+
ctxLen: number,
|
|
227
271
|
): number => {
|
|
228
272
|
if (pkLen !== 1312 || sigLen !== 2420 || ctxLen > 255) return -4;
|
|
229
273
|
try {
|
|
@@ -243,8 +287,10 @@ export function buildCryptoImports(
|
|
|
243
287
|
// the server's static secret key, write it to `outPtr`, return 0 / neg.
|
|
244
288
|
// Backed by the same noble lib the client encapsulates with (dev == prod).
|
|
245
289
|
'crypto.mlkem_decapsulate': (
|
|
246
|
-
ctPtr: number,
|
|
247
|
-
|
|
290
|
+
ctPtr: number,
|
|
291
|
+
ctLen: number,
|
|
292
|
+
skPtr: number,
|
|
293
|
+
skLen: number,
|
|
248
294
|
outPtr: number,
|
|
249
295
|
): number => {
|
|
250
296
|
if (ctLen !== 1088 || skLen !== 2400) return -4;
|
|
@@ -267,9 +313,12 @@ export function buildCryptoImports(
|
|
|
267
313
|
// `@noble/curves` ristretto255_oprf, which matches the edge byte-for-byte
|
|
268
314
|
// (both RFC 9497), so dev == prod.
|
|
269
315
|
'crypto.voprf_evaluate': (
|
|
270
|
-
seedPtr: number,
|
|
271
|
-
|
|
272
|
-
|
|
316
|
+
seedPtr: number,
|
|
317
|
+
seedLen: number,
|
|
318
|
+
infoPtr: number,
|
|
319
|
+
infoLen: number,
|
|
320
|
+
blindedPtr: number,
|
|
321
|
+
blindedLen: number,
|
|
273
322
|
outPtr: number,
|
|
274
323
|
): number => {
|
|
275
324
|
// seedLen MUST be exactly 32 (RFC 9497 Ns; noble deriveKeyPair rejects
|
|
@@ -309,8 +358,13 @@ function importKey(
|
|
|
309
358
|
try {
|
|
310
359
|
// Symmetric / MAC / KDF: raw bytes.
|
|
311
360
|
if (
|
|
312
|
-
alg === ALG.AES_GCM ||
|
|
313
|
-
alg === ALG.
|
|
361
|
+
alg === ALG.AES_GCM ||
|
|
362
|
+
alg === ALG.AES_CBC ||
|
|
363
|
+
alg === ALG.AES_CTR ||
|
|
364
|
+
alg === ALG.AES_KW ||
|
|
365
|
+
alg === ALG.HMAC ||
|
|
366
|
+
alg === ALG.PBKDF2 ||
|
|
367
|
+
alg === ALG.HKDF
|
|
314
368
|
) {
|
|
315
369
|
if (format !== FMT.RAW) return ERR_UNSUPPORTED;
|
|
316
370
|
return newEntry({ raw: key, keyObject: null, alg, hash, isPrivate: false });
|
|
@@ -339,8 +393,14 @@ function aesAlgName(keyLen: number, mode: 'gcm' | 'cbc' | 'ctr'): string {
|
|
|
339
393
|
}
|
|
340
394
|
|
|
341
395
|
function aesOp(
|
|
342
|
-
cs: CryptoState,
|
|
343
|
-
|
|
396
|
+
cs: CryptoState,
|
|
397
|
+
ref: MemoryRef,
|
|
398
|
+
encrypt: boolean,
|
|
399
|
+
handle: number,
|
|
400
|
+
pp: number,
|
|
401
|
+
pl: number,
|
|
402
|
+
dp: number,
|
|
403
|
+
dl: number,
|
|
344
404
|
): number {
|
|
345
405
|
const e = cs.keys.get(handle);
|
|
346
406
|
if (!e || !e.raw) throw new Error('crypto: invalid AES key handle');
|
|
@@ -356,7 +416,9 @@ function aesOp(
|
|
|
356
416
|
if (tagBits !== 0 && tagBits !== 128) return ERR_INVALID_PARAMS;
|
|
357
417
|
if (encrypt) {
|
|
358
418
|
const c = nodeCrypto.createCipheriv(
|
|
359
|
-
aesAlgName(e.raw.length, 'gcm'),
|
|
419
|
+
aesAlgName(e.raw.length, 'gcm'),
|
|
420
|
+
e.raw,
|
|
421
|
+
iv,
|
|
360
422
|
) as nodeCrypto.CipherGCM;
|
|
361
423
|
if (aad.length) c.setAAD(aad);
|
|
362
424
|
const ct = Buffer.concat([c.update(data), c.final()]);
|
|
@@ -366,7 +428,9 @@ function aesOp(
|
|
|
366
428
|
// never authenticate.
|
|
367
429
|
if (data.length < 16) return ERR_OPERATION_FAILED;
|
|
368
430
|
const d = nodeCrypto.createDecipheriv(
|
|
369
|
-
aesAlgName(e.raw.length, 'gcm'),
|
|
431
|
+
aesAlgName(e.raw.length, 'gcm'),
|
|
432
|
+
e.raw,
|
|
433
|
+
iv,
|
|
370
434
|
) as nodeCrypto.DecipherGCM;
|
|
371
435
|
if (aad.length) d.setAAD(aad);
|
|
372
436
|
const tag = data.subarray(data.length - 16);
|
|
@@ -395,8 +459,13 @@ function aesOp(
|
|
|
395
459
|
}
|
|
396
460
|
|
|
397
461
|
function signOp(
|
|
398
|
-
cs: CryptoState,
|
|
399
|
-
|
|
462
|
+
cs: CryptoState,
|
|
463
|
+
ref: MemoryRef,
|
|
464
|
+
handle: number,
|
|
465
|
+
pp: number,
|
|
466
|
+
pl: number,
|
|
467
|
+
dp: number,
|
|
468
|
+
dl: number,
|
|
400
469
|
): number {
|
|
401
470
|
const e = cs.keys.get(handle);
|
|
402
471
|
if (!e) throw new Error('crypto.sign: invalid handle');
|
|
@@ -426,8 +495,15 @@ function signOp(
|
|
|
426
495
|
}
|
|
427
496
|
|
|
428
497
|
function verifyOp(
|
|
429
|
-
cs: CryptoState,
|
|
430
|
-
|
|
498
|
+
cs: CryptoState,
|
|
499
|
+
ref: MemoryRef,
|
|
500
|
+
handle: number,
|
|
501
|
+
pp: number,
|
|
502
|
+
pl: number,
|
|
503
|
+
sp: number,
|
|
504
|
+
sl: number,
|
|
505
|
+
dp: number,
|
|
506
|
+
dl: number,
|
|
431
507
|
): number {
|
|
432
508
|
const e = cs.keys.get(handle);
|
|
433
509
|
if (!e) throw new Error('crypto.verify: invalid handle');
|
|
@@ -442,10 +518,15 @@ function verifyOp(
|
|
|
442
518
|
return mac.length === sig.length && nodeCrypto.timingSafeEqual(mac, sig) ? 1 : 0;
|
|
443
519
|
}
|
|
444
520
|
if (e.alg === ALG.ECDSA && e.keyObject) {
|
|
445
|
-
const ok = nodeCrypto.verify(
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
521
|
+
const ok = nodeCrypto.verify(
|
|
522
|
+
hashName(hash),
|
|
523
|
+
data,
|
|
524
|
+
{
|
|
525
|
+
key: e.keyObject,
|
|
526
|
+
dsaEncoding: 'ieee-p1363',
|
|
527
|
+
},
|
|
528
|
+
sig,
|
|
529
|
+
);
|
|
449
530
|
return ok ? 1 : 0;
|
|
450
531
|
}
|
|
451
532
|
if (e.alg === ALG.ED25519 && e.keyObject) {
|
|
@@ -458,8 +539,12 @@ function verifyOp(
|
|
|
458
539
|
}
|
|
459
540
|
|
|
460
541
|
function deriveBitsOp(
|
|
461
|
-
cs: CryptoState,
|
|
462
|
-
|
|
542
|
+
cs: CryptoState,
|
|
543
|
+
ref: MemoryRef,
|
|
544
|
+
handle: number,
|
|
545
|
+
pp: number,
|
|
546
|
+
pl: number,
|
|
547
|
+
lengthBits: number,
|
|
463
548
|
): number {
|
|
464
549
|
if (lengthBits < 0 || lengthBits % 8 !== 0) return ERR_INVALID_PARAMS;
|
|
465
550
|
const outLen = lengthBits / 8;
|
|
@@ -473,7 +558,10 @@ function deriveBitsOp(
|
|
|
473
558
|
if (alg === ALG.PBKDF2 && e.raw) {
|
|
474
559
|
const iterations = pr.readU32();
|
|
475
560
|
const salt = pr.readBlob();
|
|
476
|
-
return stash(
|
|
561
|
+
return stash(
|
|
562
|
+
cs,
|
|
563
|
+
nodeCrypto.pbkdf2Sync(e.raw, salt, iterations, outLen, hashName(hash)),
|
|
564
|
+
);
|
|
477
565
|
}
|
|
478
566
|
if (alg === ALG.HKDF && e.raw) {
|
|
479
567
|
const salt = pr.readBlob();
|
|
@@ -484,7 +572,8 @@ function deriveBitsOp(
|
|
|
484
572
|
if ((alg === ALG.ECDH || alg === ALG.X25519) && e.keyObject) {
|
|
485
573
|
const peerHandle = pr.readI32();
|
|
486
574
|
const peer = cs.keys.get(peerHandle);
|
|
487
|
-
if (!peer || !peer.keyObject)
|
|
575
|
+
if (!peer || !peer.keyObject)
|
|
576
|
+
throw new Error('crypto.derive_bits: invalid peer handle');
|
|
488
577
|
const shared = nodeCrypto.diffieHellman({
|
|
489
578
|
privateKey: e.keyObject,
|
|
490
579
|
publicKey: peer.keyObject,
|
|
@@ -27,6 +27,37 @@ const MEMBERS = new Map<string, Map<string, Buffer>>();
|
|
|
27
27
|
const COUNTERS = new Map<string, bigint>();
|
|
28
28
|
/** Events family: `"collection\0key"` -> append-ordered event blobs (oldest first). */
|
|
29
29
|
const EVENTS = new Map<string, Buffer[]>();
|
|
30
|
+
/** Capacity family: `"collection\0key"` -> an escrow ledger (total/holds/confirmed). */
|
|
31
|
+
const CAPACITY = new Map<string, CapLedger>();
|
|
32
|
+
|
|
33
|
+
/** A finite-resource escrow: a ceiling, in-flight holds, and confirmed consumes. */
|
|
34
|
+
interface CapLedger {
|
|
35
|
+
total: bigint;
|
|
36
|
+
confirmed: bigint;
|
|
37
|
+
holds: Map<bigint, { amount: bigint; expiresMs: number }>;
|
|
38
|
+
nextId: bigint;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function capLedger(sk: string): CapLedger {
|
|
42
|
+
let l = CAPACITY.get(sk);
|
|
43
|
+
if (l === undefined) {
|
|
44
|
+
l = { total: 0n, confirmed: 0n, holds: new Map(), nextId: 1n };
|
|
45
|
+
CAPACITY.set(sk, l);
|
|
46
|
+
}
|
|
47
|
+
return l;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Drop holds whose TTL has elapsed (self-heal, mirrors the edge's now-based prune). */
|
|
51
|
+
function capPrune(l: CapLedger, nowMs: number): void {
|
|
52
|
+
for (const [id, h] of l.holds) if (h.expiresMs <= nowMs) l.holds.delete(id);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Units currently held (un-expired, unconfirmed). */
|
|
56
|
+
function capHeld(l: CapLedger): bigint {
|
|
57
|
+
let sum = 0n;
|
|
58
|
+
for (const h of l.holds.values()) sum += h.amount;
|
|
59
|
+
return sum;
|
|
60
|
+
}
|
|
30
61
|
|
|
31
62
|
const MAX_NAME = 512;
|
|
32
63
|
const MAX_KEY = 4096;
|
|
@@ -81,8 +112,13 @@ export function buildDatabaseImports(
|
|
|
81
112
|
db: DbDevState,
|
|
82
113
|
): Record<string, (...args: number[]) => number> {
|
|
83
114
|
return {
|
|
84
|
-
'data.resolve_collection': (
|
|
85
|
-
|
|
115
|
+
'data.resolve_collection': (
|
|
116
|
+
namePtr: number,
|
|
117
|
+
nameLen: number,
|
|
118
|
+
outHandlePtr: number,
|
|
119
|
+
): number => {
|
|
120
|
+
if (nameLen < 0 || nameLen > MAX_NAME)
|
|
121
|
+
throw new Error('data: collection name too long');
|
|
86
122
|
const name = readCopy(ref, namePtr, nameLen).toString('utf8');
|
|
87
123
|
const handle = db.handles.length;
|
|
88
124
|
db.handles.push(name);
|
|
@@ -153,7 +189,8 @@ export function buildDatabaseImports(
|
|
|
153
189
|
): number => {
|
|
154
190
|
const coll = collOf(db, handle);
|
|
155
191
|
if (coll === null) return INVALID_HANDLE;
|
|
156
|
-
if (keyLen > MAX_KEY || valLen > MAX_VALUE)
|
|
192
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE)
|
|
193
|
+
throw new Error('data: key/value too large');
|
|
157
194
|
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
158
195
|
if (STORE.has(sk)) return PRODUCT_ERR; // AlreadyExists
|
|
159
196
|
STORE.set(sk, readCopy(ref, valPtr, valLen));
|
|
@@ -170,7 +207,8 @@ export function buildDatabaseImports(
|
|
|
170
207
|
): number => {
|
|
171
208
|
const coll = collOf(db, handle);
|
|
172
209
|
if (coll === null) return INVALID_HANDLE;
|
|
173
|
-
if (keyLen > MAX_KEY || patchLen > MAX_VALUE)
|
|
210
|
+
if (keyLen > MAX_KEY || patchLen > MAX_VALUE)
|
|
211
|
+
throw new Error('data: key/patch too large');
|
|
174
212
|
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
175
213
|
if (!STORE.has(sk)) return PRODUCT_ERR; // NotFound
|
|
176
214
|
const v = readCopy(ref, patchPtr, patchLen);
|
|
@@ -230,7 +268,8 @@ export function buildDatabaseImports(
|
|
|
230
268
|
): number => {
|
|
231
269
|
const coll = collOf(db, handle);
|
|
232
270
|
if (coll === null) return INVALID_HANDLE;
|
|
233
|
-
if (keyLen > MAX_KEY || valLen > MAX_VALUE)
|
|
271
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE)
|
|
272
|
+
throw new Error('data: key/value too large');
|
|
234
273
|
const sk = storeKey(coll, readCopy(ref, keyPtr, keyLen));
|
|
235
274
|
const owner = readCopy(ref, valPtr, valLen);
|
|
236
275
|
const existing = STORE.get(sk);
|
|
@@ -287,7 +326,8 @@ export function buildDatabaseImports(
|
|
|
287
326
|
): number => {
|
|
288
327
|
const coll = collOf(db, handle);
|
|
289
328
|
if (coll === null) return INVALID_HANDLE;
|
|
290
|
-
if (setLen > MAX_KEY || memberLen > MAX_VALUE)
|
|
329
|
+
if (setLen > MAX_KEY || memberLen > MAX_VALUE)
|
|
330
|
+
throw new Error('data: set/member too large');
|
|
291
331
|
const sk = storeKey(coll, readCopy(ref, setPtr, setLen));
|
|
292
332
|
const member = readCopy(ref, memberPtr, memberLen);
|
|
293
333
|
let set = MEMBERS.get(sk);
|
|
@@ -310,13 +350,19 @@ export function buildDatabaseImports(
|
|
|
310
350
|
const coll = collOf(db, handle);
|
|
311
351
|
if (coll === null) return INVALID_HANDLE;
|
|
312
352
|
const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
|
|
313
|
-
if (set !== undefined)
|
|
353
|
+
if (set !== undefined)
|
|
354
|
+
set.delete(readCopy(ref, memberPtr, memberLen).toString('latin1'));
|
|
314
355
|
return 0;
|
|
315
356
|
},
|
|
316
357
|
|
|
317
358
|
// Frame the members (sorted by bytes, matching the edge BTreeMap) as
|
|
318
359
|
// `u32 count` + per member `u32 len + bytes`; stash + return the length.
|
|
319
|
-
'data.membership_list': (
|
|
360
|
+
'data.membership_list': (
|
|
361
|
+
handle: number,
|
|
362
|
+
setPtr: number,
|
|
363
|
+
setLen: number,
|
|
364
|
+
limit: number,
|
|
365
|
+
): number => {
|
|
320
366
|
const coll = collOf(db, handle);
|
|
321
367
|
if (coll === null) return INVALID_HANDLE;
|
|
322
368
|
const set = MEMBERS.get(storeKey(coll, readCopy(ref, setPtr, setLen)));
|
|
@@ -434,6 +480,100 @@ export function buildDatabaseImports(
|
|
|
434
480
|
return out.length;
|
|
435
481
|
},
|
|
436
482
|
|
|
483
|
+
// --- capacity family (escrow: set_total / available / reserve / confirm / cancel) ---
|
|
484
|
+
|
|
485
|
+
// Set the ceiling (restock / reduce). Job/derive only (kind-gated upstream).
|
|
486
|
+
// A ceiling is never negative.
|
|
487
|
+
'data.capacity_set_total': (
|
|
488
|
+
handle: number,
|
|
489
|
+
keyPtr: number,
|
|
490
|
+
keyLen: number,
|
|
491
|
+
total: number | bigint,
|
|
492
|
+
_idemPtr: number,
|
|
493
|
+
): number => {
|
|
494
|
+
const coll = collOf(db, handle);
|
|
495
|
+
if (coll === null) return INVALID_HANDLE;
|
|
496
|
+
const l = capLedger(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
497
|
+
const t = BigInt(total);
|
|
498
|
+
l.total = satI64(t < 0n ? 0n : t);
|
|
499
|
+
return 0;
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
// Stash the i64 available (total - confirmed - active holds, floored at 0).
|
|
503
|
+
'data.capacity_available': (handle: number, keyPtr: number, keyLen: number): number => {
|
|
504
|
+
const coll = collOf(db, handle);
|
|
505
|
+
if (coll === null) return INVALID_HANDLE;
|
|
506
|
+
const l = capLedger(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
507
|
+
capPrune(l, Date.now());
|
|
508
|
+
const avail = l.total - l.confirmed - capHeld(l);
|
|
509
|
+
const out = Buffer.alloc(8);
|
|
510
|
+
out.writeBigInt64LE(avail < 0n ? 0n : avail);
|
|
511
|
+
db.lastResult = out;
|
|
512
|
+
return out.length;
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
// Hold `amount` for `ttlMs`: stash the u64 reservation id (8 bytes) on
|
|
516
|
+
// success, or return ABSENT (-2) when there is not enough available (the
|
|
517
|
+
// guest maps that to reservation 0 = no oversell). `now` is the HOST clock.
|
|
518
|
+
'data.capacity_reserve': (
|
|
519
|
+
handle: number,
|
|
520
|
+
keyPtr: number,
|
|
521
|
+
keyLen: number,
|
|
522
|
+
amount: number | bigint,
|
|
523
|
+
ttlMs: number | bigint,
|
|
524
|
+
_idemPtr: number,
|
|
525
|
+
): number => {
|
|
526
|
+
const coll = collOf(db, handle);
|
|
527
|
+
if (coll === null) return INVALID_HANDLE;
|
|
528
|
+
const want = BigInt(amount);
|
|
529
|
+
if (want <= 0n) return ABSENT;
|
|
530
|
+
const l = capLedger(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
531
|
+
const now = Date.now();
|
|
532
|
+
capPrune(l, now);
|
|
533
|
+
if (l.total - l.confirmed - capHeld(l) < want) return ABSENT; // never oversell
|
|
534
|
+
const id = l.nextId++;
|
|
535
|
+
l.holds.set(id, { amount: want, expiresMs: now + Math.max(0, Number(ttlMs)) });
|
|
536
|
+
const out = Buffer.alloc(8);
|
|
537
|
+
out.writeBigUInt64LE(id);
|
|
538
|
+
db.lastResult = out;
|
|
539
|
+
return out.length;
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
// Finalize a hold into a permanent consume. 1 if the id was a live hold,
|
|
543
|
+
// 0 if it was unknown / expired / already settled.
|
|
544
|
+
'data.capacity_confirm': (
|
|
545
|
+
handle: number,
|
|
546
|
+
keyPtr: number,
|
|
547
|
+
keyLen: number,
|
|
548
|
+
reservationId: number | bigint,
|
|
549
|
+
_idemPtr: number,
|
|
550
|
+
): number => {
|
|
551
|
+
const coll = collOf(db, handle);
|
|
552
|
+
if (coll === null) return INVALID_HANDLE;
|
|
553
|
+
const l = capLedger(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
554
|
+
capPrune(l, Date.now());
|
|
555
|
+
const h = l.holds.get(BigInt(reservationId));
|
|
556
|
+
if (h === undefined) return 0;
|
|
557
|
+
l.holds.delete(BigInt(reservationId));
|
|
558
|
+
l.confirmed = satI64(l.confirmed + h.amount);
|
|
559
|
+
return 1;
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
// Release a hold back to available (a confirmed sale cannot be cancelled).
|
|
563
|
+
'data.capacity_cancel': (
|
|
564
|
+
handle: number,
|
|
565
|
+
keyPtr: number,
|
|
566
|
+
keyLen: number,
|
|
567
|
+
reservationId: number | bigint,
|
|
568
|
+
_idemPtr: number,
|
|
569
|
+
): number => {
|
|
570
|
+
const coll = collOf(db, handle);
|
|
571
|
+
if (coll === null) return INVALID_HANDLE;
|
|
572
|
+
const l = capLedger(storeKey(coll, readCopy(ref, keyPtr, keyLen)));
|
|
573
|
+
capPrune(l, Date.now());
|
|
574
|
+
return l.holds.delete(BigInt(reservationId)) ? 1 : 0;
|
|
575
|
+
},
|
|
576
|
+
|
|
437
577
|
// Drain the last stashed variable-length result into the caller buffer.
|
|
438
578
|
'data.take_result': (outPtr: number, outCap: number): number => {
|
|
439
579
|
const v = db.lastResult;
|
|
@@ -456,4 +596,5 @@ export function __resetDbForTests(): void {
|
|
|
456
596
|
MEMBERS.clear();
|
|
457
597
|
COUNTERS.clear();
|
|
458
598
|
EVENTS.clear();
|
|
599
|
+
CAPACITY.clear();
|
|
459
600
|
}
|
package/src/devserver/dotenv.ts
CHANGED
|
@@ -39,7 +39,11 @@ function parseValue(rest: string): string {
|
|
|
39
39
|
* Parse dotenv text into `plain` (non-reserved) and `reserved` (`TOIL_*`):
|
|
40
40
|
* `KEY=value`, `#` comments, optional `export`, optional surrounding quotes.
|
|
41
41
|
*/
|
|
42
|
-
function parseDotenv(
|
|
42
|
+
function parseDotenv(
|
|
43
|
+
text: string,
|
|
44
|
+
plain: Map<string, string>,
|
|
45
|
+
reserved: Map<string, string>,
|
|
46
|
+
): void {
|
|
43
47
|
for (const raw of text.split('\n')) {
|
|
44
48
|
let line = raw.trim();
|
|
45
49
|
if (line.length === 0 || line.startsWith('#')) continue;
|
|
@@ -53,7 +57,11 @@ function parseDotenv(text: string, plain: Map<string, string>, reserved: Map<str
|
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
function readFileInto(
|
|
60
|
+
function readFileInto(
|
|
61
|
+
file: string,
|
|
62
|
+
plain: Map<string, string>,
|
|
63
|
+
reserved: Map<string, string>,
|
|
64
|
+
): void {
|
|
57
65
|
try {
|
|
58
66
|
parseDotenv(fs.readFileSync(file, 'utf8'), plain, reserved);
|
|
59
67
|
} catch {
|
|
Binary file
|