@blamejs/core 0.9.49 → 0.10.1

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 (77) hide show
  1. package/CHANGELOG.md +951 -908
  2. package/index.js +25 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +67 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/compliance.js +176 -8
  25. package/lib/crypto-field.js +114 -14
  26. package/lib/crypto.js +216 -20
  27. package/lib/db.js +1 -0
  28. package/lib/guard-jmap.js +321 -0
  29. package/lib/guard-managesieve-command.js +566 -0
  30. package/lib/guard-pop3-command.js +317 -0
  31. package/lib/guard-smtp-command.js +58 -3
  32. package/lib/mail-agent.js +20 -7
  33. package/lib/mail-arc-sign.js +12 -8
  34. package/lib/mail-auth.js +323 -34
  35. package/lib/mail-crypto-pgp.js +934 -0
  36. package/lib/mail-crypto-smime.js +340 -0
  37. package/lib/mail-crypto.js +108 -0
  38. package/lib/mail-dav.js +1224 -0
  39. package/lib/mail-deploy.js +492 -0
  40. package/lib/mail-dkim.js +431 -26
  41. package/lib/mail-journal.js +435 -0
  42. package/lib/mail-scan.js +502 -0
  43. package/lib/mail-server-imap.js +64 -26
  44. package/lib/mail-server-jmap.js +488 -0
  45. package/lib/mail-server-managesieve.js +853 -0
  46. package/lib/mail-server-mx.js +40 -30
  47. package/lib/mail-server-pop3.js +836 -0
  48. package/lib/mail-server-rate-limit.js +13 -0
  49. package/lib/mail-server-submission.js +70 -24
  50. package/lib/mail-server-tls.js +445 -0
  51. package/lib/mail-sieve.js +557 -0
  52. package/lib/mail-spam-score.js +284 -0
  53. package/lib/mail.js +99 -0
  54. package/lib/metrics.js +80 -3
  55. package/lib/middleware/dpop.js +58 -3
  56. package/lib/middleware/idempotency-key.js +255 -42
  57. package/lib/middleware/protected-resource-metadata.js +114 -2
  58. package/lib/network-dns-resolver.js +33 -0
  59. package/lib/network-tls.js +46 -0
  60. package/lib/outbox.js +62 -12
  61. package/lib/pqc-agent.js +13 -5
  62. package/lib/retry.js +23 -9
  63. package/lib/router.js +23 -1
  64. package/lib/safe-ical.js +634 -0
  65. package/lib/safe-icap.js +502 -0
  66. package/lib/safe-mime.js +15 -0
  67. package/lib/safe-sieve.js +684 -0
  68. package/lib/safe-smtp.js +57 -0
  69. package/lib/safe-url.js +37 -0
  70. package/lib/safe-vcard.js +473 -0
  71. package/lib/self-update-standalone-verifier.js +32 -3
  72. package/lib/self-update.js +153 -33
  73. package/lib/vendor/MANIFEST.json +161 -156
  74. package/lib/vendor-data.js +127 -9
  75. package/lib/vex.js +324 -59
  76. package/package.json +1 -1
  77. package/sbom.cdx.json +6 -6
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.guardJmap
4
+ * @nav Guards
5
+ * @title Guard JMAP Request
6
+ * @order 452
7
+ *
8
+ * @intro
9
+ * JMAP request-envelope validator (RFC 8620 JMAP Core). Validates
10
+ * the shape of an HTTP request body posted to `/jmap/api` and
11
+ * refuses requests that exceed operator caps, omit required
12
+ * capability declarations, or contain malformed back-references.
13
+ *
14
+ * ## Request shape (RFC 8620 §3.3)
15
+ *
16
+ * ```json
17
+ * {
18
+ * "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
19
+ * "methodCalls": [
20
+ * ["Mailbox/get", { "accountId": "A1", "ids": null }, "c0"],
21
+ * ["Email/query", { "filter": { "inMailbox": "#c0/list/0" } }, "c1"]
22
+ * ],
23
+ * "createdIds": null
24
+ * }
25
+ * ```
26
+ *
27
+ * `using` is the set of capability URIs the request invokes; the
28
+ * server's `urn:ietf:params:jmap:core` is implicit. `methodCalls`
29
+ * is an array of 3-tuples `[methodName, args, clientId]` where
30
+ * `clientId` echoes back on the response for client-side
31
+ * correlation.
32
+ *
33
+ * ## Back-reference resolution (RFC 8620 §3.7)
34
+ *
35
+ * Subsequent `methodCalls` reference earlier results via
36
+ * `{ "resultOf": <prior-clientId>, "name": <methodName>, "path": <JSONPath> }`
37
+ * placeholders inside the `args` object. The validator detects
38
+ * back-references and caps the chain depth so a pathological
39
+ * chain doesn't degrade into a O(2^N) blowup.
40
+ *
41
+ * ## Caps
42
+ *
43
+ * - `maxCallsInRequest` — default 32 (RFC 8620 §3.6)
44
+ * - `maxObjectsInGet` — default 500
45
+ * - `maxObjectsInSet` — default 500
46
+ * - `maxSizeRequest` — default 10 MiB
47
+ * - `maxBackRefDepth` — default 8 (we add this; spec doesn't)
48
+ * - `maxUsingCapabilities` — default 32 (refuses oversize `using`)
49
+ *
50
+ * Refusals emit a `urn:ietf:params:jmap:error:*` URI per
51
+ * RFC 8620 §3.6.1.
52
+ *
53
+ * @card
54
+ * JMAP request-envelope validator (RFC 8620 §3.3). Refuses oversize
55
+ * requests, capability-unknown / malformed back-reference / pipeline-
56
+ * bomb shapes per RFC 8620 §3.6.1 error vocabulary.
57
+ */
58
+
59
+ var { defineClass } = require("./framework-error");
60
+ var safeJson = require("./safe-json");
61
+ var validateOpts = require("./validate-opts");
62
+
63
+ var GuardJmapError = defineClass("GuardJmapError", { alwaysPermanent: true });
64
+
65
+ var DEFAULT_PROFILE = "strict";
66
+
67
+ var PROFILES = Object.freeze({
68
+ strict: {
69
+ maxCallsInRequest: 32, // allow:raw-byte-literal — RFC 8620 §3.6 default
70
+ maxObjectsInGet: 500, // allow:raw-byte-literal — RFC 8620 §3.6 default
71
+ maxObjectsInSet: 500, // allow:raw-byte-literal — RFC 8620 §3.6 default
72
+ maxSizeRequest: 10485760, // allow:raw-byte-literal — 10 MiB request body cap
73
+ maxBackRefDepth: 8,
74
+ maxUsingCapabilities: 32, // allow:raw-byte-literal — `using` array length cap
75
+ },
76
+ balanced: {
77
+ maxCallsInRequest: 128, // allow:raw-byte-literal — balanced call cap
78
+ maxObjectsInGet: 1000, // allow:raw-byte-literal — balanced object cap
79
+ maxObjectsInSet: 1000, // allow:raw-byte-literal — balanced object cap
80
+ maxSizeRequest: 52428800, // allow:raw-byte-literal — 50 MiB balanced
81
+ maxBackRefDepth: 16, // allow:raw-byte-literal — balanced depth
82
+ maxUsingCapabilities: 64, // allow:raw-byte-literal — balanced using cap
83
+ },
84
+ permissive: {
85
+ maxCallsInRequest: 512, // allow:raw-byte-literal — permissive call cap
86
+ maxObjectsInGet: 5000, // allow:raw-byte-literal — permissive object cap
87
+ maxObjectsInSet: 5000, // allow:raw-byte-literal — permissive object cap
88
+ maxSizeRequest: 104857600, // allow:raw-byte-literal — 100 MiB permissive
89
+ maxBackRefDepth: 32, // allow:raw-byte-literal — permissive depth
90
+ maxUsingCapabilities: 128, // allow:raw-byte-literal — permissive using cap
91
+ },
92
+ });
93
+
94
+ var COMPLIANCE_POSTURES = Object.freeze({
95
+ hipaa: "strict",
96
+ "pci-dss": "strict",
97
+ gdpr: "strict",
98
+ soc2: "strict",
99
+ });
100
+
101
+ // Capability URIs the server's core JMAP implementation always supports.
102
+ // Additional capabilities (mail / contacts / calendars / submission)
103
+ // the operator opts into via opts.serverCapabilities.
104
+ var CORE_CAPABILITIES = Object.freeze({
105
+ "urn:ietf:params:jmap:core": true,
106
+ });
107
+
108
+ /**
109
+ * @primitive b.guardJmap.validate
110
+ * @signature b.guardJmap.validate(rawBody, opts?)
111
+ * @since 0.9.50
112
+ * @status stable
113
+ * @related b.guardImapCommand.validate, b.safeJson.parse
114
+ *
115
+ * Validate a JMAP request envelope. Accepts either a raw JSON string
116
+ * (bytes) or a pre-parsed object. Returns
117
+ * `{ using, methodCalls, createdIds }` on success; throws
118
+ * `GuardJmapError` with the matching `urn:ietf:params:jmap:error:*`
119
+ * URI on refusal.
120
+ *
121
+ * @opts
122
+ * profile: "strict" | "balanced" | "permissive",
123
+ * posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
124
+ * serverCapabilities: { "urn:ietf:params:jmap:mail": true, ... },
125
+ * // capability URIs the server has wired; `using`
126
+ * // entries not in this set are refused with
127
+ * // urn:ietf:params:jmap:error:unknownCapability
128
+ *
129
+ * @example
130
+ * var parsed = b.guardJmap.validate(rawBody, {
131
+ * serverCapabilities: { "urn:ietf:params:jmap:mail": true },
132
+ * });
133
+ * // → { using: [...], methodCalls: [[methodName, args, clientId], ...] }
134
+ */
135
+ function validate(rawBody, opts) {
136
+ opts = opts || {};
137
+ var profileName = typeof opts.profile === "string" ? opts.profile : DEFAULT_PROFILE;
138
+ if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
139
+ profileName = COMPLIANCE_POSTURES[opts.posture];
140
+ }
141
+ var caps = PROFILES[profileName];
142
+ if (!caps) {
143
+ throw new GuardJmapError("guard-jmap/bad-profile",
144
+ "guardJmap.validate: unknown profile '" + profileName + "'");
145
+ }
146
+ // Clone serverCapabilities before injecting the core capability so we
147
+ // never mutate the operator-supplied object. mail.server.jmap.create
148
+ // passes its shared `serverCapabilities` into every validate() call;
149
+ // pre-fix this rewrote the operator's `urn:ietf:params:jmap:core`
150
+ // entry to `true` after the first request, breaking the Session
151
+ // resource's RFC 8620 §2 capability-object shape.
152
+ var serverCaps = Object.assign({}, opts.serverCapabilities || {});
153
+ // Always allow the core capability — the server provides it inherently.
154
+ serverCaps["urn:ietf:params:jmap:core"] = true;
155
+
156
+ // Parse if rawBody is a string. Cap the byte size before parsing.
157
+ var body;
158
+ if (typeof rawBody === "string" || Buffer.isBuffer(rawBody)) {
159
+ var s = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
160
+ // Wire-protocol size cap MUST be measured in UTF-8 bytes, not
161
+ // JavaScript UTF-16 code units. A 1 MiB cap interpreted as code
162
+ // units lets non-ASCII payloads (emoji, CJK) past the gate at
163
+ // 2-4× the actual byte budget — directly weakens the DoS cap.
164
+ var byteLen = typeof rawBody === "string" ? Buffer.byteLength(s, "utf8") : rawBody.length;
165
+ if (byteLen > caps.maxSizeRequest) {
166
+ throw new GuardJmapError("urn:ietf:params:jmap:error:requestTooLarge",
167
+ "guardJmap.validate: request body " + byteLen +
168
+ " bytes exceeds cap " + caps.maxSizeRequest);
169
+ }
170
+ try {
171
+ body = safeJson.parse(s);
172
+ } catch (e) {
173
+ throw new GuardJmapError("guard-jmap/bad-json",
174
+ "guardJmap.validate: body is not valid JSON: " + (e && e.message ? e.message : String(e)));
175
+ }
176
+ } else if (rawBody && typeof rawBody === "object") {
177
+ body = rawBody;
178
+ } else {
179
+ throw new GuardJmapError("guard-jmap/bad-input",
180
+ "guardJmap.validate: rawBody must be a JSON string, Buffer, or pre-parsed object");
181
+ }
182
+
183
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
184
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
185
+ "guardJmap.validate: request body must be a JSON object");
186
+ }
187
+
188
+ if (!Array.isArray(body.using)) {
189
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
190
+ "guardJmap.validate: `using` must be an array of capability URIs");
191
+ }
192
+ if (body.using.length > caps.maxUsingCapabilities) {
193
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
194
+ "guardJmap.validate: `using` length " + body.using.length +
195
+ " exceeds cap " + caps.maxUsingCapabilities);
196
+ }
197
+ for (var ui = 0; ui < body.using.length; ui += 1) {
198
+ var cap = body.using[ui];
199
+ if (typeof cap !== "string") {
200
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
201
+ "guardJmap.validate: `using[" + ui + "]` must be a string capability URI");
202
+ }
203
+ if (!CORE_CAPABILITIES[cap] && !serverCaps[cap]) {
204
+ throw new GuardJmapError("urn:ietf:params:jmap:error:unknownCapability",
205
+ "guardJmap.validate: capability '" + cap + "' not advertised by this server");
206
+ }
207
+ }
208
+
209
+ if (!Array.isArray(body.methodCalls)) {
210
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
211
+ "guardJmap.validate: `methodCalls` must be an array");
212
+ }
213
+ if (body.methodCalls.length === 0) {
214
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
215
+ "guardJmap.validate: `methodCalls` must contain at least one call");
216
+ }
217
+ if (body.methodCalls.length > caps.maxCallsInRequest) {
218
+ throw new GuardJmapError("urn:ietf:params:jmap:error:limit/maxCallsInRequest",
219
+ "guardJmap.validate: " + body.methodCalls.length +
220
+ " methodCalls exceeds cap " + caps.maxCallsInRequest);
221
+ }
222
+
223
+ var seenClientIds = Object.create(null);
224
+ for (var ci = 0; ci < body.methodCalls.length; ci += 1) {
225
+ var call = body.methodCalls[ci];
226
+ if (!Array.isArray(call) || call.length !== 3) {
227
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
228
+ "guardJmap.validate: methodCalls[" + ci + "] must be a 3-tuple [name, args, clientId]");
229
+ }
230
+ if (typeof call[0] !== "string") {
231
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
232
+ "guardJmap.validate: methodCalls[" + ci + "][0] (method name) must be a string");
233
+ }
234
+ if (typeof call[1] !== "object" || call[1] === null || Array.isArray(call[1])) {
235
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
236
+ "guardJmap.validate: methodCalls[" + ci + "][1] (args) must be an object");
237
+ }
238
+ if (typeof call[2] !== "string") {
239
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
240
+ "guardJmap.validate: methodCalls[" + ci + "][2] (clientId) must be a string");
241
+ }
242
+ if (call[2].length === 0 || call[2].length > 256) { // allow:raw-byte-literal — clientId length cap
243
+ throw new GuardJmapError("urn:ietf:params:jmap:error:invalidArguments",
244
+ "guardJmap.validate: methodCalls[" + ci + "][2] (clientId) length must be 1..256");
245
+ }
246
+ // Back-reference depth cap: count `resultOf` occurrences in the
247
+ // args subtree. Pathological depth would let a client chain
248
+ // hundreds of resolutions per call.
249
+ var refCount = _countBackRefs(call[1], 0, caps.maxBackRefDepth);
250
+ if (refCount === -1) {
251
+ throw new GuardJmapError("urn:ietf:params:jmap:error:limit/maxBackRefDepth",
252
+ "guardJmap.validate: methodCalls[" + ci + "] back-reference depth exceeds cap " +
253
+ caps.maxBackRefDepth);
254
+ }
255
+ seenClientIds[call[2]] = true;
256
+ }
257
+
258
+ validateOpts.optionalPlainObject(body.createdIds,
259
+ "guardJmap.validate: `createdIds`",
260
+ GuardJmapError, "urn:ietf:params:jmap:error:invalidArguments",
261
+ "null or an object");
262
+
263
+ return {
264
+ using: body.using,
265
+ methodCalls: body.methodCalls,
266
+ createdIds: body.createdIds || null,
267
+ };
268
+ }
269
+
270
+ // Walk args looking for back-reference markers
271
+ // (`#name`-prefixed keys per RFC 8620 §3.7 or a `resultOf` shape).
272
+ // Returns the maximum depth seen, or -1 if it exceeds maxDepth.
273
+ function _countBackRefs(node, depth, maxDepth) {
274
+ if (depth > maxDepth) return -1;
275
+ if (node === null || typeof node !== "object") return depth;
276
+ if (Array.isArray(node)) {
277
+ var maxA = depth;
278
+ for (var i = 0; i < node.length; i += 1) {
279
+ var d = _countBackRefs(node[i], depth + 1, maxDepth);
280
+ if (d === -1) return -1;
281
+ if (d > maxA) maxA = d;
282
+ }
283
+ return maxA;
284
+ }
285
+ var keys = Object.keys(node);
286
+ if (keys.length > 1000) return -1; // allow:raw-byte-literal — per-object key cap
287
+ var maxO = depth;
288
+ for (var k = 0; k < keys.length; k += 1) {
289
+ var key = keys[k];
290
+ var inc = (key === "resultOf" || key.charCodeAt(0) === 0x23) ? 1 : 0; // allow:raw-byte-literal — `#` (0x23) is the JMAP back-ref prefix
291
+ var d2 = _countBackRefs(node[key], depth + inc, maxDepth);
292
+ if (d2 === -1) return -1;
293
+ if (d2 > maxO) maxO = d2;
294
+ }
295
+ return maxO;
296
+ }
297
+
298
+ /**
299
+ * @primitive b.guardJmap.compliancePosture
300
+ * @signature b.guardJmap.compliancePosture(posture)
301
+ * @since 0.9.50
302
+ * @status stable
303
+ *
304
+ * Return the effective profile for a compliance posture, or `null`
305
+ * for unknown names.
306
+ *
307
+ * @example
308
+ * b.guardJmap.compliancePosture("hipaa"); // → "strict"
309
+ */
310
+ function compliancePosture(posture) {
311
+ return COMPLIANCE_POSTURES[posture] || null;
312
+ }
313
+
314
+ module.exports = {
315
+ validate: validate,
316
+ compliancePosture: compliancePosture,
317
+ PROFILES: PROFILES,
318
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
319
+ CORE_CAPABILITIES: CORE_CAPABILITIES,
320
+ GuardJmapError: GuardJmapError,
321
+ };