@blamejs/core 0.8.43 → 0.8.50

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 (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/guard-mime.js CHANGED
@@ -1,34 +1,39 @@
1
1
  "use strict";
2
2
  /**
3
- * guard-mime — Media-type identifier-safety primitive (b.guardMime).
3
+ * @module b.guardMime
4
+ * @nav Guards
5
+ * @title Guard Mime
4
6
  *
5
- * Validates user-supplied RFC 6838 media type strings destined for
6
- * Accept-shape comparison, content-type allowlists, and dispatch
7
- * routing. KIND="identifier" consumes ctx.identifier (or ctx.mime).
7
+ * @intro
8
+ * Media-type identifier-safety guard. Validates user-supplied
9
+ * RFC 6838 media-type strings destined for Accept-shape comparison,
10
+ * content-type allowlists, and dispatch routing. KIND="identifier"
11
+ * — the gate consumes `ctx.identifier` (or `ctx.mime`).
8
12
  *
9
- * Threat catalog:
10
- * - Shape malformation not RFC 6838 type/subtype grammar.
11
- * - Bad token characters RFC 6838 §4.2 restricts type and subtype
12
- * to ALPHA / DIGIT / `!#$&-^_.+`. Spaces / quotes / Unicode reject.
13
- * - Parameter injection operators sometimes pass through user-
14
- * supplied parameters (`text/plain; charset=...`); the grammar is
15
- * permissive enough to smuggle multiple parameters or bare values.
16
- * - Wildcard `* / *` and `type / *` only valid in Accept-header
17
- * context; refused as a content-type at strict.
18
- * - Vendor tree without operator opt-in (`application/vnd.<vendor>`)
19
- * flag at strict so operators audit the vendor namespace.
20
- * - Personal tree (`application/prs.*`) and unregistered (`x.*`)
21
- * same flag class.
22
- * - Risky types refuse list — `application/x-msdownload`, `.x-bat`,
23
- * `.x-msdos-program`, `.x-sh`, `.x-csh`, `.javascript`,
24
- * `.x-javascript` (when handed off to a script-host).
25
- * - BIDI / zero-width / control / null-byte universal refuse.
13
+ * Threat catalog: shape malformation (not RFC 6838 type/subtype
14
+ * grammar); bad token characters (RFC 6838 §4.2 restricts type and
15
+ * subtype to ALPHA / DIGIT / `!#$&-^_.+` spaces / quotes /
16
+ * Unicode reject); parameter injection through pass-through
17
+ * `text/plain; charset=...` shapes; wildcard `*‍/‍*` / `type/*`
18
+ * (Accept-only refused as content-type at strict); vendor tree
19
+ * `application/vnd.<vendor>` and personal tree `application/prs.*`
20
+ * plus unregistered `x.*` flagged so operators audit the namespace;
21
+ * risky types refuse list (`application/x-msdownload`,
22
+ * `.x-msdos-program`, `.x-sh`, `.x-csh`, `application/javascript`,
23
+ * `text/javascript`) when handed off to a script-host;
24
+ * BIDI / zero-width / C0-control / null-byte universal-refuse.
26
25
  *
27
- * var rv = b.guardMime.validate("application/json",
28
- * { profile: "strict" });
29
- * var safe = b.guardMime.sanitize("Application/JSON; charset=UTF-8",
30
- * { profile: "balanced" });
31
- * var g = b.guardMime.gate({ profile: "strict" });
26
+ * Magic-byte verification and polyglot rejection are performed by
27
+ * the operator-side fixture pipeline: the gate emits the asserted
28
+ * identifier; downstream content guards (`b.guardSvg` / `b.guardPdf`
29
+ * / `b.guardImage`) compare it against `inspectMagic(buffer)` and
30
+ * refuse mismatches.
31
+ *
32
+ * Profiles: `strict` / `balanced` / `permissive`. Compliance
33
+ * postures: `hipaa` / `pci-dss` / `gdpr` / `soc2`.
34
+ *
35
+ * @card
36
+ * Media-type identifier-safety guard.
32
37
  */
33
38
 
34
39
  var codepointClass = require("./codepoint-class");
@@ -347,6 +352,45 @@ function _detectIssues(input, opts) {
347
352
  return issues;
348
353
  }
349
354
 
355
+ /**
356
+ * @primitive b.guardMime.validate
357
+ * @signature b.guardMime.validate(input, opts?)
358
+ * @since 0.7.47
359
+ * @status stable
360
+ * @compliance hipaa, pci-dss, gdpr, soc2
361
+ * @related b.guardMime.sanitize, b.guardMime.gate
362
+ *
363
+ * Inspect a media-type string against the resolved profile and
364
+ * return `{ ok, issues }`. Each issue carries `kind` / `severity`
365
+ * (`critical` | `high` | `medium` | `low`) / `ruleId` / `snippet`.
366
+ * Non-string input returns a single `mime.bad-input` issue rather
367
+ * than throwing — callers that prefer an exception use
368
+ * `b.guardMime.sanitize`.
369
+ *
370
+ * @opts
371
+ * profile: "strict"|"balanced"|"permissive",
372
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
373
+ * bidiPolicy: "reject"|"strip"|"audit"|"allow",
374
+ * controlPolicy: "reject"|"strip"|"allow",
375
+ * nullBytePolicy: "reject"|"strip"|"allow",
376
+ * zeroWidthPolicy: "reject"|"strip"|"allow",
377
+ * wildcardPolicy: "reject"|"audit"|"allow",
378
+ * vendorTreePolicy: "reject"|"audit"|"allow",
379
+ * personalTreePolicy: "reject"|"audit"|"allow",
380
+ * unregisteredTreePolicy: "reject"|"audit"|"allow",
381
+ * riskyTypesPolicy: "reject"|"audit"|"allow",
382
+ * parameterPolicy: "reject"|"audit"|"allow",
383
+ * maxBytes: number, // default 256 (RFC-recommended cap)
384
+ *
385
+ * @example
386
+ * var rv = b.guardMime.validate("application/json", { profile: "strict" });
387
+ * rv.ok; // → true
388
+ * rv.issues.length; // → 0
389
+ *
390
+ * var bad = b.guardMime.validate("application/x-msdownload", { profile: "strict" });
391
+ * bad.ok; // → false
392
+ * bad.issues[0].ruleId; // → "mime.risky-type"
393
+ */
350
394
  function validate(input, opts) {
351
395
  opts = _resolveOpts(opts);
352
396
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
@@ -363,6 +407,36 @@ function validate(input, opts) {
363
407
  return gateContract.aggregateIssues(_detectIssues(input, opts));
364
408
  }
365
409
 
410
+ /**
411
+ * @primitive b.guardMime.sanitize
412
+ * @signature b.guardMime.sanitize(input, opts?)
413
+ * @since 0.7.47
414
+ * @status stable
415
+ * @related b.guardMime.validate, b.guardMime.gate
416
+ *
417
+ * Lower-case the canonical type/subtype while preserving
418
+ * parameter-value case (some parameter values are case-significant —
419
+ * e.g. multipart `boundary` tokens). Throws `GuardMimeError` when any
420
+ * `critical` or `high` issue fires (risky-type, parameter-injection,
421
+ * BIDI / null-byte / control). Use `validate` to inspect issues
422
+ * without throwing.
423
+ *
424
+ * @opts
425
+ * profile: "strict"|"balanced"|"permissive",
426
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
427
+ * ...: same shape as b.guardMime.validate opts,
428
+ *
429
+ * @example
430
+ * var safe = b.guardMime.sanitize("Application/JSON; charset=UTF-8",
431
+ * { profile: "balanced" });
432
+ * safe; // → "application/json; charset=UTF-8"
433
+ *
434
+ * try {
435
+ * b.guardMime.sanitize("application/javascript", { profile: "strict" });
436
+ * } catch (e) {
437
+ * e.code; // → "mime.risky-type"
438
+ * }
439
+ */
366
440
  function sanitize(input, opts) {
367
441
  opts = _resolveOpts(opts);
368
442
  if (typeof input !== "string") {
@@ -385,6 +459,35 @@ function sanitize(input, opts) {
385
459
  }, canonical);
386
460
  }
387
461
 
462
+ /**
463
+ * @primitive b.guardMime.gate
464
+ * @signature b.guardMime.gate(opts?)
465
+ * @since 0.7.47
466
+ * @status stable
467
+ * @compliance hipaa, pci-dss, gdpr, soc2
468
+ * @related b.guardMime.validate, b.guardMime.sanitize, b.guardAll.gate
469
+ *
470
+ * Build an async gate `(ctx) -> { ok, action, issues }` consumable
471
+ * by `b.guardAll`, `b.staticServe`, `b.fileUpload`, and any other
472
+ * host that integrates the guard contract. The gate reads
473
+ * `ctx.identifier` (or `ctx.mime`), runs `validate`, and maps
474
+ * severity to action: zero issues `serve`; only low/medium
475
+ * `audit-only`; any high/critical `refuse`.
476
+ *
477
+ * @opts
478
+ * name: string, // gate label for audit / observability
479
+ * profile: "strict"|"balanced"|"permissive",
480
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
481
+ * ...: same shape as b.guardMime.validate opts,
482
+ *
483
+ * @example
484
+ * var g = b.guardMime.gate({ profile: "strict" });
485
+ * var rv = await g({ identifier: "application/json" });
486
+ * rv.action; // → "serve"
487
+ *
488
+ * var bad = await g({ identifier: "application/x-msdownload" });
489
+ * bad.action; // → "refuse"
490
+ */
388
491
  function gate(opts) {
389
492
  opts = _resolveOpts(opts);
390
493
  return gateContract.buildGuardGate(
@@ -408,14 +511,75 @@ function gate(opts) {
408
511
  });
409
512
  }
410
513
 
514
+ /**
515
+ * @primitive b.guardMime.buildProfile
516
+ * @signature b.guardMime.buildProfile(opts)
517
+ * @since 0.7.47
518
+ * @status stable
519
+ * @related b.guardMime.gate, b.guardMime.compliancePosture
520
+ *
521
+ * Compose a derived profile from one or more named bases plus
522
+ * inline overrides. `opts.extends` is a profile name or array of
523
+ * names (later entries shadow earlier ones); inline keys win last.
524
+ *
525
+ * @opts
526
+ * extends: string|string[], // base profile name(s) to compose
527
+ * ...: any guard-mime key, // inline override of resolved keys
528
+ *
529
+ * @example
530
+ * var custom = b.guardMime.buildProfile({
531
+ * extends: "balanced",
532
+ * vendorTreePolicy: "audit",
533
+ * });
534
+ * custom.bidiPolicy; // → "reject"
535
+ * custom.vendorTreePolicy; // → "audit"
536
+ */
411
537
  var buildProfile = gateContract.makeProfileBuilder(PROFILES);
412
538
 
539
+ /**
540
+ * @primitive b.guardMime.compliancePosture
541
+ * @signature b.guardMime.compliancePosture(name)
542
+ * @since 0.7.47
543
+ * @status stable
544
+ * @compliance hipaa, pci-dss, gdpr, soc2
545
+ * @related b.guardMime.gate, b.guardMime.buildProfile
546
+ *
547
+ * Look up a compliance-posture overlay by name (`"hipaa"` /
548
+ * `"pci-dss"` / `"gdpr"` / `"soc2"`). Returns a shallow clone of
549
+ * the posture object — the caller may mutate freely. Throws
550
+ * `GuardMimeError("mime.bad-posture")` on unknown name.
551
+ *
552
+ * @example
553
+ * var posture = b.guardMime.compliancePosture("pci-dss");
554
+ * posture.riskyTypesPolicy; // → "reject"
555
+ */
413
556
  function compliancePosture(name) {
414
557
  return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
415
558
  _err, "mime");
416
559
  }
417
560
 
418
561
  var _mimeRulePacks = gateContract.makeRulePackLoader(GuardMimeError, "mime");
562
+ /**
563
+ * @primitive b.guardMime.loadRulePack
564
+ * @signature b.guardMime.loadRulePack(pack)
565
+ * @since 0.7.47
566
+ * @status stable
567
+ * @related b.guardMime.gate
568
+ *
569
+ * Register an operator-supplied rule pack with the guard-mime
570
+ * registry. The pack is identified by `pack.id` (non-empty
571
+ * string) and stored for later inspection / dispatch by gates
572
+ * that opt in via `opts.rulePackId`. Returns the pack object
573
+ * unchanged on success; throws `GuardMimeError("mime.bad-opt")`
574
+ * when `pack` is missing or `pack.id` is not a non-empty string.
575
+ *
576
+ * @example
577
+ * var pack = b.guardMime.loadRulePack({
578
+ * id: "operator-deny-flash",
579
+ * deny: ["application/x-shockwave-flash"],
580
+ * });
581
+ * pack.id; // → "operator-deny-flash"
582
+ */
419
583
  var loadRulePack = _mimeRulePacks.load;
420
584
 
421
585
  module.exports = {
@@ -1,36 +1,82 @@
1
1
  "use strict";
2
2
  /**
3
- * guard-oauth — OAuth flow-shape safety primitive (b.guardOauth).
3
+ * @module b.guardOauth
4
+ * @nav Guards
5
+ * @title Guard Oauth
4
6
  *
5
- * Validates user-supplied OAuth 2.x / OIDC authorization-code-flow
6
- * parameter bundles before the framework's b.auth.oauth client
7
- * exchanges them. KIND="oauth-flow" consumes ctx.oauthFlow.
7
+ * @intro
8
+ * OAuth 2.x / OIDC authorization-code-flow shape guard
9
+ * validates user-supplied parameter bundles BEFORE the
10
+ * framework's `b.auth.oauth` client exchanges them with the IdP.
11
+ * KIND is `oauth-flow`; the gate consumes `ctx.oauthFlow` (or
12
+ * `ctx.flow`) shape `{ response_type, redirect_uri, state,
13
+ * code_challenge, code_challenge_method, scope, code, iss,
14
+ * _isCallback }`. The guard runs the spec-mandated refuse list
15
+ * so misconfigured callers can't downgrade the flow.
8
16
  *
9
- * Threat catalog:
10
- * - PKCE missing or non-S256 RFC 7636 mandates code_verifier;
11
- * OAuth 2.1 mandates S256 (no plain). The plaintext "plain"
12
- * method is downgrade-attack class.
13
- * - state missing / replayed — RFC 6749 §10.12 + §10.14; without
14
- * state-binding the flow is open to CSRF.
15
- * - redirect_uri not in allowlist — RFC 6749 §3.1.2 + OAuth 2.1
16
- * mandate exact-match (no prefix / wildcard / scheme drift).
17
- * - response_type not in allowlist — refuse "token" implicit flow
18
- * (deprecated in OAuth 2.1) and "id_token" outside OIDC; require
19
- * operator-allowed types.
20
- * - scope tampering — refuse scope values containing whitespace
21
- * other than space (RFC 6749 §3.3) or non-printable bytes.
22
- * - issuer (iss) missing on callback — RFC 9207 mandates iss
23
- * parameter on authorization response to defeat the IdP-mix-up
24
- * attack.
25
- * - code reuse — operator-supplied seenCodeStore detects
26
- * authorization-code replay (RFC 6749 §10.5).
27
- * - excessive parameter / value length — defense against parser
28
- * DoS and decompression-bomb-shaped clients.
29
- * - BIDI / null / control / zero-width universal refuse.
17
+ * PKCE enforcement: `strict` requires `S256` (RFC 7636 + OAuth
18
+ * 2.1; the `plain` method is a downgrade-attack class). `balanced`
19
+ * accepts S256 or plain. `permissive` audits without enforcing.
20
+ * Missing `code_verifier` AND missing `code_challenge` always
21
+ * surfaces as `oauth.pkce-missing` because OAuth 2.1 mandates
22
+ * PKCE for every client class.
30
23
  *
31
- * var rv = b.guardOauth.validate({ redirect_uri, state, ... },
32
- * { profile: "strict" });
33
- * var g = b.guardOauth.gate({ profile: "strict" });
24
+ * `state` enforcement: required at strict / balanced. Without
25
+ * state-binding the authorization callback is open to CSRF (RFC
26
+ * 6749 §10.12). The guard refuses missing `state`; operator-side
27
+ * replay defense (rotating + comparing) is the responsibility of
28
+ * the caller's session layer.
29
+ *
30
+ * `nonce` is OIDC-specific replay defense — the guard's required-
31
+ * claims parity is enforced via the operator's
32
+ * `b.auth.jwt.verifyExternal` config, not in the flow shape, so
33
+ * nonce is documented here but checked by the verifier.
34
+ *
35
+ * `redirect_uri` exact-match: when the operator supplies
36
+ * `allowedRedirectUris`, every callback must be a byte-for-byte
37
+ * match. RFC 6749 §3.1.2 + OAuth 2.1 forbid prefix, wildcard, or
38
+ * scheme drift — the canonical CVE-class for this is the
39
+ * "redirect_uri loose-match" account-takeover bug. When no
40
+ * allowlist is configured the gate skips the check (operator-side
41
+ * misconfiguration warning lives in the startup audit, not in
42
+ * per-request issue lists).
43
+ *
44
+ * `response_type` allowlist: `strict` allows only `code`.
45
+ * `balanced` adds `code id_token`. `permissive` skips. Implicit-
46
+ * flow `token` and `id_token` outside OIDC are deprecated in
47
+ * OAuth 2.1 and refused under the strict / balanced allowlists.
48
+ *
49
+ * Scope-token discipline: every space-separated scope must
50
+ * conform to the RFC 6749 §3.3 charset (`%x21 / %x23-5B /
51
+ * %x5D-7E`). Whitespace-other-than-space, control bytes, and
52
+ * non-printable bytes in scope tokens are refused under strict
53
+ * / balanced and audited under permissive.
54
+ *
55
+ * RFC 9207 issuer-on-callback: when the request bundle is
56
+ * marked `_isCallback: true`, the `iss` parameter MUST be
57
+ * present at strict — defeats the IdP-mix-up attack class.
58
+ * `balanced` audits, `permissive` skips.
59
+ *
60
+ * Token-introspection bounds: `maxParamBytes` (default 2 KiB at
61
+ * strict / balanced) and `maxBytes` (default 8 KiB) cap each
62
+ * parameter and the total flow JSON. Decompression-bomb-shaped
63
+ * clients can't push the introspection / metadata layer past
64
+ * these bounds.
65
+ *
66
+ * Code-reuse defense: when the operator wires a `seenCodeStore`
67
+ * with `hasSeen(code)`, the guard refuses any authorization code
68
+ * already exchanged (RFC 6749 §10.5). The store implementation
69
+ * is the operator's responsibility — typically a short-TTL
70
+ * `b.cache` entry.
71
+ *
72
+ * Profiles: `strict` / `balanced` / `permissive`. Compliance
73
+ * postures: `hipaa` / `pci-dss` / `gdpr` / `soc2`. BIDI / null /
74
+ * control / zero-width universal-refuse applies on every string-
75
+ * valued top-level parameter at every profile so trojan-source
76
+ * codepoints can't ride a state or scope value.
77
+ *
78
+ * @card
79
+ * OAuth 2.x / OIDC authorization-code-flow shape guard — validates user-supplied parameter bundles BEFORE the framework's `b.auth.oauth` client exchanges them with the IdP.
34
80
  */
35
81
 
36
82
  var codepointClass = require("./codepoint-class");
@@ -297,6 +343,66 @@ function _detectIssues(flow, opts) {
297
343
  return issues;
298
344
  }
299
345
 
346
+ /**
347
+ * @primitive b.guardOauth.validate
348
+ * @signature b.guardOauth.validate(input, opts?)
349
+ * @since 0.7.49
350
+ * @status stable
351
+ * @compliance hipaa, pci-dss, gdpr, soc2
352
+ * @related b.guardOauth.sanitize, b.guardOauth.gate, b.auth.oauth
353
+ *
354
+ * Apply the full guard-oauth threat catalog to a flow bundle.
355
+ * Returns `{ ok, issues, refusal? }` per
356
+ * `gateContract.aggregateIssues`. Detected classes include
357
+ * `pkce-missing`, `pkce-method` (e.g. plain under require-s256),
358
+ * `state-missing`, `redirect-uri-not-allowed`,
359
+ * `response-type-not-allowed`, `scope-token-shape`,
360
+ * `issuer-missing`, `code-reused` (always critical), plus per-
361
+ * parameter `param-cap` and total-flow `flow-cap` bounds and
362
+ * codepoint-class issues on every string parameter. Operator-
363
+ * supplied opts are bounds-checked; bad opts throw
364
+ * `GuardOauthError("oauth.bad-opt")`.
365
+ *
366
+ * @opts
367
+ * profile: "strict"|"balanced"|"permissive",
368
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
369
+ * pkcePolicy: "require-s256"|"require-any"|"audit"|"allow",
370
+ * statePolicy: "require"|"audit"|"allow",
371
+ * redirectUriPolicy: "require-exact-allowlist"|"audit"|"allow",
372
+ * responseTypePolicy: "require-allowlist"|"audit"|"allow",
373
+ * scopeTamperingPolicy: "reject"|"audit"|"allow",
374
+ * issuerOnCallbackPolicy: "require"|"audit"|"allow",
375
+ * codeReusePolicy: "reject"|"allow",
376
+ * allowedRedirectUris: string[],
377
+ * allowedResponseTypes: string[],
378
+ * seenCodeStore: { hasSeen: function(code): boolean },
379
+ * maxParamBytes: number,
380
+ * maxBytes: number,
381
+ *
382
+ * @example
383
+ * var hostile = {
384
+ * response_type: "code",
385
+ * redirect_uri: "https://attacker.example/callback",
386
+ * scope: "openid",
387
+ * };
388
+ * var rv = b.guardOauth.validate(hostile, { profile: "strict" });
389
+ * rv.ok; // → false
390
+ * rv.issues[0].ruleId; // → "oauth.pkce-missing"
391
+ *
392
+ * var benign = {
393
+ * response_type: "code",
394
+ * redirect_uri: "https://app.example.com/callback",
395
+ * state: "csrf-rand-1",
396
+ * scope: "openid profile",
397
+ * code_challenge: "abc123def456ghi789jkl012mno345pqr678",
398
+ * code_challenge_method: "S256",
399
+ * };
400
+ * var ok = b.guardOauth.validate(benign, {
401
+ * profile: "strict",
402
+ * allowedRedirectUris: ["https://app.example.com/callback"],
403
+ * });
404
+ * ok.ok; // → true
405
+ */
300
406
  function validate(input, opts) {
301
407
  opts = _resolveOpts(opts);
302
408
  numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
@@ -305,6 +411,36 @@ function validate(input, opts) {
305
411
  return gateContract.aggregateIssues(_detectIssues(input, opts));
306
412
  }
307
413
 
414
+ /**
415
+ * @primitive b.guardOauth.sanitize
416
+ * @signature b.guardOauth.sanitize(input, opts?)
417
+ * @since 0.7.49
418
+ * @status stable
419
+ * @related b.guardOauth.validate, b.guardOauth.gate
420
+ *
421
+ * Pass-through-or-throw form of `validate`. OAuth flow bundles
422
+ * can't be partially repaired — a missing `state` or wrong
423
+ * `redirect_uri` is a refuse-class outcome, not something the
424
+ * guard can patch up safely. Returns the input unchanged when
425
+ * the issue list contains no `critical` / `high` entries; throws
426
+ * `GuardOauthError` carrying the offending `ruleId` otherwise.
427
+ *
428
+ * @opts
429
+ * profile: "strict"|"balanced"|"permissive",
430
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
431
+ * ...: every guardOauth.validate opt is honored,
432
+ *
433
+ * @example
434
+ * try {
435
+ * b.guardOauth.sanitize({
436
+ * response_type: "code",
437
+ * redirect_uri: "https://app.example.com/callback",
438
+ * scope: "openid",
439
+ * }, { profile: "strict" });
440
+ * } catch (e) {
441
+ * e.code; // → "oauth.pkce-missing"
442
+ * }
443
+ */
308
444
  function sanitize(input, opts) {
309
445
  opts = _resolveOpts(opts);
310
446
  // OAuth flows can't be repaired — sanitize either passes through
@@ -319,6 +455,47 @@ function sanitize(input, opts) {
319
455
  return input;
320
456
  }
321
457
 
458
+ /**
459
+ * @primitive b.guardOauth.gate
460
+ * @signature b.guardOauth.gate(opts?)
461
+ * @since 0.7.49
462
+ * @status stable
463
+ * @compliance hipaa, pci-dss, gdpr, soc2
464
+ * @related b.guardOauth.validate, b.guardOauth.sanitize, b.auth.oauth
465
+ *
466
+ * Build a `gateContract.buildGuardGate`-shaped gate that pulls
467
+ * `ctx.oauthFlow` (or `ctx.flow`) and dispatches to `validate`.
468
+ * Returns `{ ok: true, action: "serve" }` when the issue list is
469
+ * empty, `{ ok: true, action: "audit-only", issues }` when only
470
+ * low-severity issues fire, and `{ ok: false, action: "refuse",
471
+ * issues }` on any `critical` / `high` issue. Compose into the
472
+ * authorization-callback handler before exchanging the code with
473
+ * the IdP — refusal on a hostile callback prevents the token
474
+ * exchange entirely.
475
+ *
476
+ * @opts
477
+ * profile: "strict"|"balanced"|"permissive",
478
+ * compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
479
+ * name: string, // gate label for audit trails
480
+ * ...: every guardOauth.validate opt is honored,
481
+ *
482
+ * @example
483
+ * var oauthGate = b.guardOauth.gate({
484
+ * profile: "strict",
485
+ * allowedRedirectUris: ["https://app.example.com/callback"],
486
+ * });
487
+ * var rv = await oauthGate.run({
488
+ * oauthFlow: {
489
+ * response_type: "code",
490
+ * redirect_uri: "https://attacker.example/callback",
491
+ * state: "csrf-rand-1",
492
+ * scope: "openid",
493
+ * code_challenge: "abc123def456ghi789jkl012mno345pqr678",
494
+ * code_challenge_method: "S256",
495
+ * },
496
+ * });
497
+ * rv.action; // → "refuse"
498
+ */
322
499
  function gate(opts) {
323
500
  opts = _resolveOpts(opts);
324
501
  return gateContract.buildGuardGate(
@@ -342,14 +519,86 @@ function gate(opts) {
342
519
  });
343
520
  }
344
521
 
522
+ /**
523
+ * @primitive b.guardOauth.buildProfile
524
+ * @signature b.guardOauth.buildProfile(opts)
525
+ * @since 0.7.49
526
+ * @status stable
527
+ * @related b.guardOauth.gate, b.guardOauth.compliancePosture
528
+ *
529
+ * Compose a derived profile from one or more named bases plus
530
+ * inline overrides. `opts.extends` is a profile name (`"strict"` /
531
+ * `"balanced"` / `"permissive"`) or an array of names; later
532
+ * entries shadow earlier ones, and inline `opts` keys win last.
533
+ * Operators stage profile overlays here so the final shape is
534
+ * traceable to a baseline rather than a hand-typed dictionary.
535
+ *
536
+ * @opts
537
+ * extends: string|string[], // base profile name(s) to compose
538
+ * ...: any guardOauth key, // inline override of resolved keys
539
+ *
540
+ * @example
541
+ * var custom = b.guardOauth.buildProfile({
542
+ * extends: "balanced",
543
+ * pkcePolicy: "require-s256",
544
+ * allowedResponseTypes: ["code"],
545
+ * });
546
+ * custom.pkcePolicy; // → "require-s256"
547
+ * custom.allowedResponseTypes.length; // → 1
548
+ */
345
549
  var buildProfile = gateContract.makeProfileBuilder(PROFILES);
346
550
 
551
+ /**
552
+ * @primitive b.guardOauth.compliancePosture
553
+ * @signature b.guardOauth.compliancePosture(name)
554
+ * @since 0.7.49
555
+ * @status stable
556
+ * @compliance hipaa, pci-dss, gdpr, soc2
557
+ * @related b.guardOauth.gate, b.guardOauth.buildProfile
558
+ *
559
+ * Look up a compliance-posture overlay by name (`"hipaa"` /
560
+ * `"pci-dss"` / `"gdpr"` / `"soc2"`). Returns a shallow clone of
561
+ * the posture object — the caller may mutate freely. Throws
562
+ * `GuardOauthError("oauth.bad-posture")` on unknown name.
563
+ * Postures extend the strict profile (or balanced for `gdpr`)
564
+ * with a `forensicSnippetBytes` cap appropriate to the regime.
565
+ *
566
+ * @example
567
+ * var posture = b.guardOauth.compliancePosture("pci-dss");
568
+ * posture.pkcePolicy; // → "require-s256"
569
+ * posture.forensicSnippetBytes; // → 256
570
+ */
347
571
  function compliancePosture(name) {
348
572
  return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
349
573
  _err, "oauth");
350
574
  }
351
575
 
352
576
  var _oauthRulePacks = gateContract.makeRulePackLoader(GuardOauthError, "oauth");
577
+ /**
578
+ * @primitive b.guardOauth.loadRulePack
579
+ * @signature b.guardOauth.loadRulePack(pack)
580
+ * @since 0.7.49
581
+ * @status stable
582
+ * @related b.guardOauth.gate
583
+ *
584
+ * Register an operator-supplied rule pack with the guard-oauth
585
+ * registry. The pack is identified by `pack.id` (non-empty
586
+ * string) and stored for later inspection / dispatch by gates
587
+ * that opt in via `opts.rulePackId`. Returns the pack object
588
+ * unchanged on success; throws `GuardOauthError("oauth.bad-opt")`
589
+ * when `pack` is missing or `pack.id` is not a non-empty string.
590
+ *
591
+ * @example
592
+ * var pack = b.guardOauth.loadRulePack({
593
+ * id: "scope-narrow",
594
+ * rules: [
595
+ * { id: "no-admin", severity: "high",
596
+ * detect: function (flow) { return /\badmin\b/.test(flow.scope || ""); },
597
+ * reason: "tenant forbids admin scope on user-flow callbacks" },
598
+ * ],
599
+ * });
600
+ * pack.id; // → "scope-narrow"
601
+ */
353
602
  var loadRulePack = _oauthRulePacks.load;
354
603
 
355
604
  module.exports = {