castrum 0.9.6 → 0.9.8

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 (88) hide show
  1. package/Cargo.lock +4 -3
  2. package/Cargo.toml +1 -1
  3. package/castrum.darwin-arm64.node +0 -0
  4. package/castrum.darwin-x64.node +0 -0
  5. package/castrum.linux-arm64-gnu.node +0 -0
  6. package/castrum.linux-arm64-musl.node +0 -0
  7. package/castrum.linux-x64-gnu.node +0 -0
  8. package/castrum.linux-x64-musl.node +0 -0
  9. package/castrum.linux-x64-v3-gnu.node +0 -0
  10. package/castrum.win32-x64-msvc.node +0 -0
  11. package/dist/index.d.ts +501 -5
  12. package/dist/index.js +1658 -1314
  13. package/docs/ARCHITECTURE.md +1 -1
  14. package/docs/BENCHMARKS.md +13 -0
  15. package/docs/CASE_STUDY.md +2 -2
  16. package/docs/ENVIRONMENT.md +1 -1
  17. package/docs/FFI_BUN_GUIDE.md +13 -1
  18. package/docs/HOW_TO_ADD_AN_OP.md +24 -0
  19. package/docs/INGRESS.md +4 -4
  20. package/docs/REPO_MAP.md +8 -6
  21. package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
  22. package/index.ts +1 -0
  23. package/package.json +3 -2
  24. package/rust/crypto/aead.rs +44 -0
  25. package/rust/crypto/argon2.rs +32 -2
  26. package/rust/crypto/jwt/tests.rs +19 -0
  27. package/rust/crypto/jwt/token.rs +43 -28
  28. package/rust/crypto/session.rs +170 -7
  29. package/rust/ffi/crypto.rs +182 -61
  30. package/rust/ffi/hashing.rs +48 -33
  31. package/rust/ffi/http.rs +62 -36
  32. package/rust/ffi/metrics.rs +6 -2
  33. package/rust/ffi/payload.rs +47 -27
  34. package/rust/ffi/task.rs +51 -32
  35. package/rust/ffi/tests.rs +197 -0
  36. package/rust/ffi/util.rs +40 -0
  37. package/rust/http/headers.rs +13 -0
  38. package/rust/http/multipart.rs +139 -0
  39. package/rust/ingress/native_route.rs +21 -3
  40. package/rust/task/completion.rs +7 -0
  41. package/rust/task/mod.rs +1 -1
  42. package/rust/task/tests.rs +30 -0
  43. package/src/ingress/decode/baked-result.ts +13 -5
  44. package/src/ingress/decode/fast-result.ts +1 -1
  45. package/src/ingress/decode/result-base.ts +16 -2
  46. package/src/ingress/handlers.ts +103 -31
  47. package/src/ingress/headers/memoized-headers.ts +40 -0
  48. package/src/ingress/native-route.ts +25 -6
  49. package/src/ingress/options.ts +26 -2
  50. package/src/ingress/packing/gather-raw-headers.ts +24 -8
  51. package/src/ingress/response/baked-response.ts +118 -28
  52. package/src/ingress/router.ts +14 -2
  53. package/src/ingress/routes/common.ts +26 -10
  54. package/src/ingress/routes/echo.ts +50 -7
  55. package/src/ingress/routes/json-write.ts +21 -8
  56. package/src/ingress/routes/responder.ts +4 -4
  57. package/src/ingress/server-node.ts +33 -14
  58. package/src/ingress/server.ts +3 -4
  59. package/src/ingress/shared.ts +1 -1
  60. package/src/ingress/types.ts +15 -0
  61. package/src/loader/ops.ts +1 -1
  62. package/src/native/ffi/build/codecs-selftest.ts +305 -0
  63. package/src/native/ffi/build/codecs.ts +0 -296
  64. package/src/native/ffi/build/instances-selftest.ts +264 -0
  65. package/src/native/ffi/build/instances.ts +39 -241
  66. package/src/native/ffi/build/metrics.ts +0 -7
  67. package/src/native/ffi/build/parse-selftest.ts +238 -0
  68. package/src/native/ffi/build/parse.ts +11 -234
  69. package/src/native/ffi/build.ts +5 -1
  70. package/src/native/ffi/selftest.ts +3 -3
  71. package/src/native/ffi/symbols.ts +460 -0
  72. package/src/native/ffi/types.ts +17 -0
  73. package/src/native/ffi.ts +2 -422
  74. package/src/native/ingress-binding.ts +39 -0
  75. package/src/runtime/codec.ts +24 -0
  76. package/src/runtime/types.ts +11 -0
  77. package/src/rust-ffi/batch/build.ts +18 -8
  78. package/src/rust-ffi/batch/types.ts +15 -0
  79. package/src/rust-ffi/options.ts +19 -0
  80. package/src/rust-ffi/scalar/crypto.ts +8 -0
  81. package/src/rust-ffi/scalar/factories.ts +47 -20
  82. package/src/rust-ffi/scalar/http.ts +60 -10
  83. package/src/rust-ffi/scalar/interface.ts +4 -1
  84. package/src/rust-ffi/scalar/payload.ts +27 -1
  85. package/src/shared/buffer-pool.ts +44 -9
  86. package/src/shared/codec.ts +26 -0
  87. package/src/shared/packed/parsers.ts +64 -7
  88. package/src/task/runtime.ts +29 -28
@@ -75,11 +75,7 @@ export interface TaskRuntime {
75
75
  /** gzip-compress `data` on a pool thread. */
76
76
  gzipCompress(data: Uint8Array, options?: TaskRunOptions): Promise<Uint8Array>
77
77
  /** Verify a password against a PHC string on a pool thread (10-200 ms CPU). */
78
- argon2Verify(
79
- password: Uint8Array,
80
- phc: Uint8Array,
81
- options?: TaskRunOptions,
82
- ): Promise<boolean>
78
+ argon2Verify(password: Uint8Array, phc: Uint8Array, options?: TaskRunOptions): Promise<boolean>
83
79
  /** PBKDF2-HMAC-SHA256 on a pool thread (10-200 ms CPU, 1-64 B output). */
84
80
  pbkdf2Sha256(
85
81
  password: Uint8Array,
@@ -345,9 +341,7 @@ export function createTaskRuntime(options: TaskRuntimeOptions = {}): TaskRuntime
345
341
  } else if (status === TASK_STATUS_CANCELLED) {
346
342
  deferred.reject(cancelledError())
347
343
  } else {
348
- deferred.reject(
349
- new Error(decodeUtf8(bodyOf(buf, bodyOff, len, pooled)) || 'task failed'),
350
- )
344
+ deferred.reject(new Error(decodeUtf8(bodyOf(buf, bodyOff, len, pooled)) || 'task failed'))
351
345
  }
352
346
  completed++
353
347
  }
@@ -361,11 +355,8 @@ export function createTaskRuntime(options: TaskRuntimeOptions = {}): TaskRuntime
361
355
  queueMicrotask(drainNow)
362
356
  }
363
357
 
364
- function submit(
365
- op: number,
366
- args: Uint8Array,
367
- runOptions?: TaskRunOptions,
368
- ): Promise<Uint8Array> {
358
+ function submit(op: number, args: Uint8Array, runOptions?: TaskRunOptions): Promise<Uint8Array> {
359
+ ensureDoorbell()
369
360
  const id = nextId++
370
361
  return new Promise<Uint8Array>((resolve, reject) => {
371
362
  inflight.set(id, { out: null, keep: null, resolve, reject })
@@ -398,6 +389,7 @@ export function createTaskRuntime(options: TaskRuntimeOptions = {}): TaskRuntime
398
389
  output: Uint8Array,
399
390
  runOptions?: TaskRunOptions,
400
391
  ): Promise<Uint8Array> {
392
+ ensureDoorbell()
401
393
  const id = nextId++
402
394
  return new Promise<Uint8Array>((resolve, reject) => {
403
395
  inflight.set(id, { out: output, keep: null, resolve, reject })
@@ -431,6 +423,7 @@ export function createTaskRuntime(options: TaskRuntimeOptions = {}): TaskRuntime
431
423
  data: Uint8Array,
432
424
  runOptions?: TaskRunOptions,
433
425
  ): Promise<Uint8Array> {
426
+ ensureDoorbell()
434
427
  const id = nextId++
435
428
  return new Promise<Uint8Array>((resolve, reject) => {
436
429
  inflight.set(id, { out: null, keep: data, resolve, reject })
@@ -453,19 +446,26 @@ export function createTaskRuntime(options: TaskRuntimeOptions = {}): TaskRuntime
453
446
  }
454
447
 
455
448
  // One process-wide trampoline; Bun marshals calls from pool threads onto the
456
- // JS thread. Rust coalesces so a burst rings it once.
449
+ // JS thread. Rust coalesces so a burst rings it once. Created lazily (and
450
+ // re-created after a shutdown) so the native side NEVER holds the address of
451
+ // a closed `JSCallback` — calling a freed trampoline is a use-after-free.
457
452
  // eslint-disable-next-line @typescript-eslint/no-require-imports
458
453
  const { JSCallback } = require('bun:ffi') as typeof import('bun:ffi')
459
- const cb = new JSCallback(
460
- () => {
461
- drainNow()
462
- },
463
- { args: [], returns: 'void', threadsafe: true } as unknown as ConstructorParameters<
464
- typeof JSCallback
465
- >[1],
466
- )
467
- doorbell = cb
468
- ffi.taskSetDoorbell(Number(cb.ptr))
454
+ function ensureDoorbell(): void {
455
+ if (doorbell !== null) return
456
+ const cb = new JSCallback(
457
+ () => {
458
+ drainNow()
459
+ },
460
+ { args: [], returns: 'void', threadsafe: true } as unknown as ConstructorParameters<
461
+ typeof JSCallback
462
+ >[1],
463
+ )
464
+ doorbell = cb
465
+ ffi.taskSetDoorbell(Number(cb.ptr))
466
+ }
467
+
468
+ ensureDoorbell()
469
469
 
470
470
  // A zero-copy result handoff via `toArrayBuffer(ptr, 0, len, dealloc)` was
471
471
  // prototyped here and REVERTED: Bun 1.4.2's deallocator hook segfaults
@@ -492,8 +492,7 @@ export function createTaskRuntime(options: TaskRuntimeOptions = {}): TaskRuntime
492
492
  // UNSUPPORTED from an older addon) fall back, so behavior can't regress.
493
493
  // Args are re-submitted on the retry step, so they own their memory.
494
494
  const args = encodeGzipDecompressArgs(data, runOptions?.maxDecompressed)
495
- const copyPath = (): Promise<Uint8Array> =>
496
- submit(TASK_OP.gzipDecompress, args, runOptions)
495
+ const copyPath = (): Promise<Uint8Array> => submit(TASK_OP.gzipDecompress, args, runOptions)
497
496
  const zeroCopyOnce = (cap: number): Promise<Uint8Array> =>
498
497
  submitInto(TASK_OP.gzipDecompressInto, args, Buffer.allocUnsafe(cap), runOptions)
499
498
  return zeroCopyOnce(size)
@@ -521,8 +520,7 @@ export function createTaskRuntime(options: TaskRuntimeOptions = {}): TaskRuntime
521
520
  },
522
521
  brotliDecompress(data, runOptions) {
523
522
  const args = encodeBrotliDecompressArgs(data, runOptions?.maxDecompressed)
524
- const copyPath = (): Promise<Uint8Array> =>
525
- submit(TASK_OP.brotliDecompress, args, runOptions)
523
+ const copyPath = (): Promise<Uint8Array> => submit(TASK_OP.brotliDecompress, args, runOptions)
526
524
  // Brotli has no ISIZE trailer, so there is no exact size hint: start from
527
525
  // a ratio guess and let the needed-size retry land it exactly (at most
528
526
  // one extra pass; on anything but TOO_SMALL, fall back to the copy op).
@@ -557,6 +555,9 @@ export function createTaskRuntime(options: TaskRuntimeOptions = {}): TaskRuntime
557
555
  deferred.reject(cancelledError())
558
556
  }
559
557
  inflight.clear()
558
+ // Zero the native doorbell BEFORE closing the JS trampoline: a worker
559
+ // completing after this point must never call a freed callback.
560
+ ffi.taskSetDoorbell(0)
560
561
  ffi.taskShutdown()
561
562
  release()
562
563
  doorbell?.close()