@blamejs/core 0.8.43 → 0.8.49

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 +92 -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
@@ -1,56 +1,31 @@
1
1
  "use strict";
2
2
  /**
3
- * b.contentCredentials — California SB-942 / AB-853 + C2PA 2.1
4
- * content-provenance manifest builder for AI-generated assets.
5
- *
6
- * California SB-942 (Cal. Bus. & Prof. Code §22757) + AB-853, both
7
- * effective 2026-08-02, require providers of generative AI systems
8
- * to embed a latent (machine-readable) provenance disclosure in
9
- * every AI-generated image / video / audio asset distributed in
10
- * California. The disclosure MUST carry:
11
- *
12
- * - Provider name
13
- * - System (model) identifier + version
14
- * - Content timestamp (when generated)
15
- * - Unique content ID
16
- *
17
- * SB-942 specifically cites C2PA (Coalition for Content Provenance
18
- * and Authenticity) as an acceptable disclosure format. C2PA 2.1+
19
- * manifests carry signed assertions with the same fields.
20
- *
21
- * The framework can't embed the manifest into image/video/audio
22
- * bytes directly (that requires format-specific muxers — JPEG XMP /
23
- * PNG iTXt / MP4 ContentBoxes / etc. that vary per codec). What it
24
- * CAN do:
25
- *
26
- * - Build a C2PA-shaped manifest carrying the required fields.
27
- * - Sign the manifest with the framework's audit-sign keypair
28
- * (ML-DSA-87 — or operator-supplied SigStore key).
29
- * - Emit a tamper-evident audit row recording the disclosure.
30
- * - Validate inbound manifests presented by upstream content
31
- * pipelines (the receiver side of the same chain).
32
- *
33
- * Operator workflow:
3
+ * @module b.contentCredentials
4
+ * @featured true
5
+ * @nav AI
6
+ * @title Content Credentials
34
7
  *
35
- * var manifest = b.contentCredentials.build({
36
- * provider: "Acme AI Inc.",
37
- * system: "acme-image-v3",
38
- * systemVersion: "3.2.1",
39
- * contentId: "img-2026-05-08-abc123",
40
- * contentType: "image/png",
41
- * contentSha3: hashHex,
42
- * // operator's display attribution + machine-readable fields
43
- * });
44
- * var signed = b.contentCredentials.sign(manifest, { signWith: ... });
45
- * // operator hands `signed.manifest` to their muxer for embedding
8
+ * @intro
9
+ * C2PA 2.1 content provenance — sign assets with a manifest
10
+ * declaring origin, edits, AI involvement.
11
+ *
12
+ * California SB-942 (Cal. Bus. & Prof. Code §22757) + AB-853,
13
+ * effective 2026-08-02, require generative-AI providers to embed a
14
+ * latent disclosure carrying provider name, system identifier,
15
+ * system version, content timestamp, and a unique content ID in
16
+ * every AI-generated image / video / audio asset distributed in
17
+ * California. SB-942 names C2PA as an acceptable format.
46
18
  *
47
- * Public API:
19
+ * The framework can't push bytes into format-specific muxers (JPEG
20
+ * XMP / PNG iTXt / MP4 boxes vary per codec). What it does ship:
21
+ * build a C2PA-shaped manifest with the SB-942 required fields,
22
+ * sign it with the audit-sign keypair (ML-DSA-87 by default),
23
+ * record a tamper-evident audit row, and verify inbound manifests
24
+ * on the receive side. Operators hand the signed manifest to their
25
+ * format-specific embedder.
48
26
  *
49
- * contentCredentials.build(opts) -> manifest (unsigned)
50
- * contentCredentials.sign(manifest, opts) -> { manifest, signature }
51
- * contentCredentials.verify(envelope, publicKeyPem) -> { valid, claims }
52
- * contentCredentials.required(opts) -> array of missing-field errors
53
- * (returns [] when the operator's input satisfies SB-942 minimums)
27
+ * @card
28
+ * C2PA 2.1 content provenance — sign assets with a manifest declaring origin, edits, AI involvement.
54
29
  */
55
30
 
56
31
  var crypto = require("./crypto");
@@ -112,6 +87,44 @@ function _validateBuildOpts(opts) {
112
87
  }
113
88
  }
114
89
 
90
+ /**
91
+ * @primitive b.contentCredentials.build
92
+ * @signature b.contentCredentials.build(opts)
93
+ * @since 0.8.44
94
+ * @related b.contentCredentials.sign, b.contentCredentials.verify, b.contentCredentials.required
95
+ *
96
+ * Build an unsigned C2PA 2.1-shaped manifest carrying the SB-942
97
+ * §22757(a) required fields (provider, system, system version,
98
+ * content ID) plus optional content type, SHA3-512 digest, and a
99
+ * visible-disclosure string. Returns a frozen object so downstream
100
+ * code can't mutate the claims before signing. `generatedAt`
101
+ * defaults to `Date.now()` so the manifest carries a real timestamp
102
+ * unless the operator pins one for testing.
103
+ *
104
+ * @opts
105
+ * provider: string, // e.g. "Acme AI Inc."
106
+ * providerContact: string, // optional contact URL
107
+ * system: string, // model id, e.g. "acme-image-v3"
108
+ * systemVersion: string, // semver
109
+ * contentId: string, // unique per asset
110
+ * contentType: string, // IANA media type (optional)
111
+ * contentSha3: string, // SHA3-512 hex (optional)
112
+ * generatedAt: number, // ms epoch (optional)
113
+ * visibleDisclosure: string, // operator display text (optional)
114
+ *
115
+ * @example
116
+ * var manifest = b.contentCredentials.build({
117
+ * provider: "Acme AI Inc.",
118
+ * system: "acme-image-v3",
119
+ * systemVersion: "3.2.1",
120
+ * contentId: "img-2026-05-08-abc123",
121
+ * contentType: "image/png",
122
+ * generatedAt: Date.UTC(2026, 4, 8),
123
+ * });
124
+ * manifest.aiGenerated; // → true
125
+ * manifest.system.id; // → "acme-image-v3"
126
+ * manifest.content.id; // → "img-2026-05-08-abc123"
127
+ */
115
128
  function build(opts) {
116
129
  _validateBuildOpts(opts);
117
130
  var generatedAt = typeof opts.generatedAt === "number" ? opts.generatedAt : Date.now();
@@ -141,6 +154,36 @@ function build(opts) {
141
154
  return Object.freeze(manifest);
142
155
  }
143
156
 
157
+ /**
158
+ * @primitive b.contentCredentials.required
159
+ * @signature b.contentCredentials.required(opts)
160
+ * @since 0.8.44
161
+ * @related b.contentCredentials.build, b.contentCredentials.verify
162
+ *
163
+ * Pre-flight check that returns the list of SB-942 §22757(a) fields
164
+ * missing from a candidate input — useful for operator UIs that
165
+ * surface "what's needed before we can disclose" without round-
166
+ * tripping through `build` and catching the throw. Returns `[]`
167
+ * when every required field is present and non-empty.
168
+ *
169
+ * @opts
170
+ * provider: string, // required
171
+ * system: string, // required
172
+ * systemVersion: string, // required
173
+ * contentId: string, // required
174
+ *
175
+ * @example
176
+ * b.contentCredentials.required({
177
+ * provider: "Acme AI Inc.",
178
+ * system: "acme-image-v3",
179
+ * systemVersion: "3.2.1",
180
+ * contentId: "img-001",
181
+ * });
182
+ * // → []
183
+ *
184
+ * b.contentCredentials.required({ provider: "Acme AI Inc." });
185
+ * // → ["missing-system", "missing-systemVersion", "missing-contentId"]
186
+ */
144
187
  function required(opts) {
145
188
  var errors = [];
146
189
  if (!opts || typeof opts !== "object") return ["opts-required"];
@@ -152,6 +195,36 @@ function required(opts) {
152
195
  return errors;
153
196
  }
154
197
 
198
+ /**
199
+ * @primitive b.contentCredentials.sign
200
+ * @signature b.contentCredentials.sign(manifest, opts)
201
+ * @since 0.8.44
202
+ * @related b.contentCredentials.build, b.contentCredentials.verify, b.crypto.sign
203
+ *
204
+ * Canonicalize the manifest (RFC 8785 JCS via `b.canonicalJson`) and
205
+ * sign it with `b.crypto.sign` using the operator's private-key PEM
206
+ * — typically the ML-DSA-87 audit-sign keypair. Returns an envelope
207
+ * with the original manifest plus the base64-encoded signature.
208
+ * Audits the disclosure under `contentcredentials.signed` unless the
209
+ * caller passes `audit:false`.
210
+ *
211
+ * @opts
212
+ * privateKeyPem: string, // PEM-encoded signing key
213
+ * audit: boolean, // default true
214
+ *
215
+ * @example
216
+ * var pair = b.crypto.generateSigningKeyPair("ml-dsa-87");
217
+ * var manifest = b.contentCredentials.build({
218
+ * provider: "Acme AI Inc.",
219
+ * system: "acme-image-v3",
220
+ * systemVersion: "3.2.1",
221
+ * contentId: "img-2026-05-08-abc123",
222
+ * });
223
+ * var envelope = b.contentCredentials.sign(manifest, {
224
+ * privateKeyPem: pair.privateKey,
225
+ * });
226
+ * typeof envelope.signature; // → "string"
227
+ */
155
228
  function sign(manifest, opts) {
156
229
  opts = opts || {};
157
230
  if (!manifest || typeof manifest !== "object") {
@@ -180,6 +253,38 @@ function sign(manifest, opts) {
180
253
  };
181
254
  }
182
255
 
256
+ /**
257
+ * @primitive b.contentCredentials.verify
258
+ * @signature b.contentCredentials.verify(envelope, publicKeyPem, opts)
259
+ * @since 0.8.44
260
+ * @related b.contentCredentials.sign, b.contentCredentials.build, b.crypto.verify
261
+ *
262
+ * Verify a signed envelope produced by `sign`. Re-canonicalizes the
263
+ * manifest, checks the signature with `b.crypto.verify` against the
264
+ * operator-supplied public-key PEM, and re-runs the SB-942 required-
265
+ * field presence check on the verified claims so a manifest with a
266
+ * valid signature but missing fields fails closed. Never throws —
267
+ * returns `{ valid, claims, reason }`. Audits successful
268
+ * verifications under `contentcredentials.verified` unless
269
+ * `audit:false`.
270
+ *
271
+ * @opts
272
+ * audit: boolean, // default true
273
+ *
274
+ * @example
275
+ * var pair = b.crypto.generateSigningKeyPair("ml-dsa-87");
276
+ * var manifest = b.contentCredentials.build({
277
+ * provider: "Acme AI Inc.",
278
+ * system: "acme-image-v3",
279
+ * systemVersion: "3.2.1",
280
+ * contentId: "img-001",
281
+ * });
282
+ * var envelope = b.contentCredentials.sign(manifest, {
283
+ * privateKeyPem: pair.privateKey,
284
+ * });
285
+ * var result = b.contentCredentials.verify(envelope, pair.publicKey);
286
+ * result.valid; // → true
287
+ */
183
288
  function verify(envelope, publicKeyPem, opts) {
184
289
  opts = opts || {};
185
290
  if (!envelope || typeof envelope !== "object" || !envelope.manifest || !envelope.signature) {
package/lib/cookies.js CHANGED
@@ -1,55 +1,50 @@
1
1
  "use strict";
2
2
  /**
3
- * cookies — cookie parse/serialize + access-gated sealed cookies.
3
+ * @module b.cookies
4
+ * @nav HTTP
5
+ * @title Cookies
4
6
  *
5
- * RFC 6265 cookie plumbing the framework was duplicating across
6
- * middleware: a parser in attach-user, ad-hoc Set-Cookie strings in
7
- * route handlers, no shared place for attribute defaults. This is the
8
- * single primitive.
7
+ * @intro
8
+ * RFC 6265 cookie plumbing parse, serialize, and sealed (vault-
9
+ * gated) cookies in one primitive. Replaces the ad-hoc Set-Cookie
10
+ * strings that used to live in middleware and route handlers.
9
11
  *
10
- * Two surfaces:
12
+ * Two surfaces:
11
13
  *
12
- * 1. Module-level (stateless): cookies.parse / cookies.serialize.
13
- * Useful in test fixtures and code that doesn't have a vault.
14
+ * 1. Module-level (stateless): `b.cookies.parse` /
15
+ * `b.cookies.serialize` / `b.cookies.parseSafe`. Useful in test
16
+ * fixtures and code paths that don't have a vault wired.
14
17
  *
15
- * 2. Instance (cookies.create): bound defaults for cookie attributes,
16
- * a wired vault for sealed reads/writes, and req/res helpers.
18
+ * 2. Instance (`b.cookies.create`): bound defaults for cookie
19
+ * attributes, a wired vault for sealed reads/writes, and
20
+ * request/response helpers (`read` / `write` / `clear` /
21
+ * `writeSealed` / `readSealed`).
17
22
  *
18
- * var cookies = b.cookies.create({
19
- * vault: b.vault, // required for sealed* methods
20
- * defaults: {
21
- * httpOnly: true,
22
- * secure: true, // default true; HTTPS expected
23
- * sameSite: "Lax",
24
- * path: "/",
25
- * maxAge: 7 * 86400, // seconds
26
- * },
27
- * });
23
+ * Defaults mirror modern browser expectations: HttpOnly on,
24
+ * Secure on, SameSite=Lax, Path=/. Operators developing locally
25
+ * over plain http opt out of Secure explicitly so the production
26
+ * posture isn't silently weakened.
28
27
  *
29
- * cookies.parse("a=1; b=2") → { a: "1", b: "2" }
30
- * cookies.serialize("name", "v",
31
- * { maxAge: 3600 }) → "name=v; Max-Age=3600; Path=/; HttpOnly; SameSite=Lax; Secure"
28
+ * Cookie-prefix invariants from RFC 6265bis §4.1.3 are enforced at
29
+ * serialize time: `__Secure-*` requires Secure; `__Host-*` requires
30
+ * Secure + Path=/ + no Domain. Operator typos (`__Host-` cookie
31
+ * without Path=/) throw at the source instead of silently failing
32
+ * on the browser side.
32
33
  *
33
- * cookies.read(req, "name") → "v" or null
34
- * cookies.write(res, "name", "v", {}) // appends to existing Set-Cookie
35
- * cookies.clear(res, "name", {}) // expire by Max-Age=0
34
+ * Header-injection defense: cookie names are RFC 6265 tokens; values
35
+ * reject CRLF / NUL / semicolon / comma pre-encoding, then percent-
36
+ * encode on write and percent-decode on read. Domain / Path
37
+ * attributes are CRLF/NUL-scrubbed before they reach Set-Cookie.
36
38
  *
37
- * cookies.writeSealed(res, "session", sid) // vault.seal then write
38
- * cookies.readSealed(req, "session") // read then vault.unseal
39
+ * Sealed cookies wrap the value in a `vault.seal` envelope: without
40
+ * the framework's vault key no client can hand-craft a valid value,
41
+ * so curl-with-arbitrary-cookies (or any tool that hasn't been
42
+ * through the framework's crypto flow) can't reach a sealed-cookie-
43
+ * gated endpoint. The vault prefix is stripped on write and re-added
44
+ * on read so the cookie carries only the compact base64 envelope.
39
45
  *
40
- * Sealed-cookie purpose: the cookie value is a vault.seal of the real
41
- * value. Without the framework's vault key, no client can hand-craft a
42
- * valid cookie value, so the API is unreachable via curl-with-arbitrary-
43
- * cookies or any tool that hasn't been through the framework's crypto
44
- * flow. The vault prefix is stripped on write and re-added on read so
45
- * the cookie carries only the base64 envelope.
46
- *
47
- * Defense in serialize/parse:
48
- * - Cookie name must be a valid token (no CTLs, no separator chars).
49
- * - Cookie value must not contain CRLF, semicolon, or comma.
50
- * - Value is percent-encoded on write, percent-decoded on read.
51
- * - Domain / Path are CRLF-stripped to defeat header injection
52
- * attempts via operator-controlled but improperly-escaped inputs.
46
+ * @card
47
+ * RFC 6265 cookie plumbing parse, serialize, and sealed (vault- gated) cookies in one primitive.
53
48
  */
54
49
 
55
50
  var C = require("./constants");
@@ -116,6 +111,25 @@ function _scrubAttr(s) {
116
111
 
117
112
  }
118
113
 
114
+ /**
115
+ * @primitive b.cookies.parse
116
+ * @signature b.cookies.parse(cookieHeader)
117
+ * @since 0.1.72
118
+ * @status stable
119
+ * @related b.cookies.parseSafe, b.cookies.serialize, b.cookies.create
120
+ *
121
+ * Lenient RFC 6265 Cookie-header parser. Returns a plain object
122
+ * `{ name: value }` with last-write-wins semantics (matching every
123
+ * browser). Surrounding double-quotes are stripped per §5.2 and
124
+ * values are percent-decoded; malformed pairs are silently dropped
125
+ * because that's how browsers behave. For the threat-detecting
126
+ * variant that surfaces issues instead of dropping silently, use
127
+ * `parseSafe`.
128
+ *
129
+ * @example
130
+ * var jar = b.cookies.parse("session=abc; theme=%22dark%22");
131
+ * // → { session: "abc", theme: "dark" }
132
+ */
119
133
  function parse(cookieHeader) {
120
134
  var out = {};
121
135
  if (typeof cookieHeader !== "string" || cookieHeader.length === 0) return out;
@@ -140,6 +154,43 @@ function parse(cookieHeader) {
140
154
  return out;
141
155
  }
142
156
 
157
+ /**
158
+ * @primitive b.cookies.serialize
159
+ * @signature b.cookies.serialize(name, value, attrs)
160
+ * @since 0.1.72
161
+ * @status stable
162
+ * @related b.cookies.parse, b.cookies.create
163
+ *
164
+ * Build a single Set-Cookie header value from a name, value, and
165
+ * attributes object. Validates the name as an RFC 6265 token, the
166
+ * value against CRLF / NUL / `;` / `,`, and enforces the `__Secure-`
167
+ * / `__Host-` prefix invariants from RFC 6265bis §4.1.3. SameSite
168
+ * is normalized to `Strict` / `Lax` / `None`, and SameSite=None
169
+ * implicitly turns Secure on so browsers don't silently drop the
170
+ * cookie. Throws `CookieError` on any invariant break — the operator
171
+ * sees the typo at the call site, not as a silently-missing cookie.
172
+ *
173
+ * @opts
174
+ * maxAge: 3600, // integer seconds; emits `Max-Age=`
175
+ * expires: new Date(), // Date or parseable date string
176
+ * domain: "example.com",
177
+ * path: "/",
178
+ * httpOnly: true,
179
+ * secure: true,
180
+ * sameSite: "Lax", // "Strict" / "Lax" / "None"
181
+ * partitioned: false, // CHIPS partitioning
182
+ * priority: "Medium", // "Low" / "Medium" / "High"
183
+ *
184
+ * @example
185
+ * var header = b.cookies.serialize("__Host-sid", "abc", {
186
+ * httpOnly: true,
187
+ * secure: true,
188
+ * sameSite: "Lax",
189
+ * path: "/",
190
+ * maxAge: 3600,
191
+ * });
192
+ * // → "__Host-sid=abc; Max-Age=3600; Path=/; HttpOnly; SameSite=Lax; Secure"
193
+ */
143
194
  function serialize(name, value, attrs) {
144
195
  _validateName(name);
145
196
  _validateValue(value);
@@ -269,6 +320,40 @@ function _readCookieFromReq(req, name) {
269
320
  return Object.prototype.hasOwnProperty.call(jar, name) ? jar[name] : null;
270
321
  }
271
322
 
323
+ /**
324
+ * @primitive b.cookies.create
325
+ * @signature b.cookies.create(opts)
326
+ * @since 0.1.72
327
+ * @status stable
328
+ * @related b.cookies.parse, b.cookies.serialize, b.vault.seal
329
+ *
330
+ * Build a cookie helper bound to a default attribute set and an
331
+ * optional vault. Returned object exposes `read(req, name)`,
332
+ * `write(res, name, value, attrs)`, `clear(res, name, attrs)`, and
333
+ * (when a vault is wired) `writeSealed` / `readSealed` for vault-
334
+ * gated cookie values. Per-call attrs merge over the bound defaults
335
+ * so callers override piecewise.
336
+ *
337
+ * @opts
338
+ * vault: b.vault, // required for sealed* methods
339
+ * defaults: {
340
+ * httpOnly: true,
341
+ * secure: true,
342
+ * sameSite: "Lax",
343
+ * path: "/",
344
+ * maxAge: 604800, // seconds (7 days)
345
+ * },
346
+ *
347
+ * @example
348
+ * var cookies = b.cookies.create({
349
+ * vault: b.vault,
350
+ * defaults: { httpOnly: true, secure: true, sameSite: "Lax", path: "/" },
351
+ * });
352
+ * cookies.write(res, "theme", "dark", { maxAge: 86400 });
353
+ * cookies.writeSealed(res, "session", "u-1");
354
+ * var sid = cookies.readSealed(req, "session");
355
+ * // → "u-1" or null
356
+ */
272
357
  function create(opts) {
273
358
  opts = opts || {};
274
359
  validateOpts(opts, ["vault", "defaults"], "b.cookies");
@@ -343,27 +428,34 @@ function create(opts) {
343
428
  };
344
429
  }
345
430
 
346
- // parseSafe — threat-detecting inbound-cookie parser. Returns
347
- // { jar, issues } where every detected anomaly surfaces as an issue
348
- // instead of being silently dropped (as the lenient parse() does).
349
- //
350
- // Threat catalog applied to the inbound Cookie header:
351
- // - Oversized header — total bytes exceed maxHeaderBytes (default 8 KiB).
352
- // - Oversized pair — name + value exceeds NAME_LENGTH + VALUE_LENGTH cap.
353
- // - Duplicate cookie name — RFC 6265 last-write-wins is the browser
354
- // behavior, but two pairs with the same name in one Cookie header
355
- // usually indicates cookie-tossing (attacker-set parent-domain
356
- // cookie shadowing the legitimate one).
357
- // - Malformed pair missing `=` or empty name.
358
- // - Forbidden chars in raw header CR / LF / NUL injected through
359
- // a downstream proxy.
360
- // - Empty / non-string input — operator-misuse signal.
361
- //
362
- // Issue shape: { kind, severity: "high"|"warn", snippet, name? }.
363
- //
364
- // Operators wire it through `b.middleware.cookies` (the convenience
365
- // middleware below) or call directly when they want the issues list
366
- // without imposing a request lifecycle.
431
+ /**
432
+ * @primitive b.cookies.parseSafe
433
+ * @signature b.cookies.parseSafe(cookieHeader, opts)
434
+ * @since 0.7.20
435
+ * @status stable
436
+ * @related b.cookies.parse, b.middleware.cookies
437
+ *
438
+ * Threat-detecting inbound-cookie parser. Returns
439
+ * `{ jar, issues }` where every detected anomaly surfaces as an
440
+ * issue instead of being silently dropped (as the lenient `parse`
441
+ * does). Detected issues: oversized header / pair, duplicate cookie
442
+ * name (cookie-tossing class), malformed pair, CR / LF / NUL in the
443
+ * raw header (proxy-side injection vector), and non-string input.
444
+ * Issue shape: `{ kind, severity: "high" | "warn", snippet, name? }`.
445
+ *
446
+ * @opts
447
+ * maxHeaderBytes: 8192, // total Cookie-header byte cap
448
+ * maxNameBytes: 256, // per-name byte cap
449
+ * maxValueBytes: 4096, // per-value byte cap
450
+ *
451
+ * @example
452
+ * var result = b.cookies.parseSafe("session=abc; session=evil", {
453
+ * maxHeaderBytes: 8192,
454
+ * });
455
+ * // → { jar: { session: "evil" },
456
+ * // issues: [{ kind: "duplicate-name", severity: "high",
457
+ * // name: "session", snippet: "..." }] }
458
+ */
367
459
  function parseSafe(cookieHeader, opts) {
368
460
  opts = opts || {};
369
461
  var maxHeaderBytes = opts.maxHeaderBytes || C.BYTES.kib(8);