@blamejs/core 0.12.61 → 0.12.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +2 -1
- package/index.js +2 -0
- package/lib/backup/manifest.js +1 -1
- package/lib/cloud-events.js +455 -0
- package/lib/csp.js +3 -2
- package/lib/jtd.js +223 -0
- package/lib/rfc3339.js +37 -0
- package/lib/safe-buffer.js +8 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.63 (2026-05-25) — **`b.cloudEvents` gains the JSON event format, batch, and the HTTP binding.** b.cloudEvents grows beyond wrap / parse into a full CloudEvents 1.0.2 surface. b.cloudEvents.validate / isValid check an envelope against the spec without throwing (the non-throwing companion to parse). toJSON / fromJSON serialize and parse the JSON event format, and toJSONBatch / fromJSONBatch handle the JSON batch format; untrusted bodies parse through the framework's bounded, prototype-pollution-safe JSON reader. The new http.* binding speaks both content modes the spec defines — binary mode spreads context attributes across percent-encoded ce-* headers with the data in the body, structured mode carries the whole event as application/cloudevents+json — plus the batch mode, and http.decode auto-detects the incoming mode from Content-Type exactly as a conformant receiver does. Verified against the spec's normative example events. **Added:** *`b.cloudEvents` JSON event format, batch, and HTTP binding* — `validate` / `isValid` report spec violations without throwing (the non-throwing companion to `parse`). `toJSON` / `fromJSON` and `toJSONBatch` / `fromJSONBatch` serialize and parse the JSON event and batch formats over the existing envelope shape. `http.encodeBinary` / `http.encodeStructured` / `http.encodeBatch` render the three HTTP content modes — binary spreads attributes across percent-encoded `ce-*` headers, structured and batched carry the event(s) as `application/cloudevents+json` / `application/cloudevents-batch+json` — and `http.decode` parses a request back into an envelope (or array) by auto-detecting the mode from `Content-Type`. `b.jtd` or `b.safeSchema` still validate the event's `data` payload. **Fixed:** *`b.csp.build` accepts `fenced-frame-src` and `webrtc`* — The CSP3 `fenced-frame-src` directive — which the default security-headers policy emits to block `<fencedframe>` embeds — was missing from the builder's recognized-directive set, so the default policy could not round-trip through `b.csp.build` (it threw `csp/unknown-directive`). Both `fenced-frame-src` and the CSP3 `webrtc` directive are now recognized.
|
|
12
|
+
|
|
13
|
+
- v0.12.62 (2026-05-26) — **`b.jtd` — JSON Type Definition validation (RFC 8927).** Validate JSON against a JSON Type Definition schema (RFC 8927) — a small, portable, cross-implementation schema language, the interop-friendly companion to the framework's fluent b.safeSchema builder. b.jtd.validate(schema, instance) returns an array of { instancePath, schemaPath } errors (empty = valid); b.jtd.isValid is the boolean form. All eight schema forms are supported — empty, type, enum, elements, properties (with optional / additional properties and nullable), values, discriminator (with mapping), and ref (with definitions) — including the integer-range and RFC 3339 timestamp types. A malformed schema is rejected at compile time with jtd/bad-schema rather than silently mis-validating. Verified against the official json-typedef-spec suites: all 316 validation cases and all 49 invalid-schema cases. **Added:** *`b.jtd.validate(schema, instance)` / `b.jtd.isValid(schema, instance)`* — `validate` returns the RFC 8927 error list — each `{ instancePath, schemaPath }` naming the offending value and the broken schema rule — and `isValid` is the boolean convenience form. Supports every JTD form and type: the numeric types enforce their exact ranges (int8 … uint32, float32 / float64), `timestamp` requires an RFC 3339 date-time, `properties` honours `optionalProperties` / `additionalProperties` / `nullable`, and `discriminator` selects a `mapping` schema by a tag property. The schema is checked for well-formedness before validation, so unknown keywords, multiple forms, bad refs, or a discriminator over a non-properties mapping all throw `jtd/bad-schema`. Use JTD for schemas you share across implementations or generate code from; use `b.safeSchema` for in-process fluent validation.
|
|
14
|
+
|
|
11
15
|
- v0.12.61 (2026-05-26) — **`b.jsonPath` — JSONPath query (RFC 9535).** A full RFC 9535 JSONPath query evaluator, complementing the framework's JSONPath guards (which screen path strings). b.jsonPath.query(doc, path) compiles a path and returns the matched node values; b.jsonPath.paths returns their normalized locations. The complete surface is implemented: name / wildcard / index / slice selectors, descendant segments (..), filter selectors (?) with comparison and logical operators, relative (@) and absolute ($) embedded queries, and the five standard functions length / count / match / search / value — with the spec's well-typedness rules enforced at compile time so a malformed or ill-typed query is rejected rather than silently mis-evaluated. Descendant walks are node-capped to bound work on hostile input. Verified against all 703 cases of the official jsonpath-compliance-test-suite. **Added:** *`b.jsonPath.query(doc, path)` / `b.jsonPath.paths(doc, path)`* — `query` returns the array of node values selected by an RFC 9535 JSONPath; `paths` returns the normalized-path string of each match (e.g. `$['a'][1]['p']`). Supports every selector (name, wildcard `*`, index incl. negative, slice `start:end:step` incl. negative step, comma-separated selections), child and descendant (`..`) segments, and filter expressions with `==` / `!=` / `<` / `<=` / `>` / `>=`, `&&` / `||` / `!`, existence tests, and the standard functions. The well-typedness rules are checked when the path is compiled — a non-singular query used as a comparison operand, an ill-typed function argument, or a value-typed function used as a test all throw `json-path/invalid`. Pairs with `b.guardJsonPath`, which screens operator-supplied path strings before they reach the evaluator.
|
|
12
16
|
|
|
13
17
|
- v0.12.60 (2026-05-25) — **`b.jsonMergePatch` — JSON Merge Patch (RFC 7396).** The simpler companion to JSON Patch: b.jsonMergePatch.merge applies an RFC 7396 merge patch (application/merge-patch+json), the partial-document PATCH body. A member present in the patch replaces or — for nested objects — merges into the target, a member whose value is null removes that key, and a patch that is itself an array, scalar, or null replaces the target wholesale. The inputs are never mutated (the merge runs on a deep copy) and member keys are written as literal own properties, so a "__proto__" member cannot reach any prototype. Verified against every RFC 7396 Appendix A test case. **Added:** *`b.jsonMergePatch.merge(target, patch)`* — Applies a JSON Merge Patch to a target document and returns the result without mutating either input. When the patch is an object it overlays the target (a null member deletes the key, a nested object merges recursively, any other value replaces); when the patch is an array, scalar, or null it replaces the whole target. Member keys are set via `Object.defineProperty`, so a `"__proto__"` member becomes a literal own key rather than altering a prototype. Pairs with `b.jsonPatch` — merge patch reads like the resource you want, JSON Patch expresses precise operations (array reordering, literal-null values).
|
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
99
99
|
- **Signed webhooks + API encryption** — SLH-DSA-SHAKE-256f default; ML-DSA-65 opt-in; ECIES API encryption (`b.webhook`, `b.crypto`)
|
|
100
100
|
- **HPKE / HTTP signatures** — RFC 9180 HPKE with ML-KEM-1024 + HKDF-SHA3-512 + ChaCha20-Poly1305 (`b.crypto.hpke`); RFC 9421 HTTP Message Signatures with derived components and ed25519 / ML-DSA-65 (`b.crypto.httpSig`); RFC 9530 Content-Digest / Repr-Digest body-integrity fields (SHA-256 / SHA-512, legacy algorithms refused — `b.contentDigest`) to sign the digest rather than the whole body
|
|
101
101
|
- **Link header** — RFC 8288 Web Linking codec (`b.linkHeader.parse` / `serialize`): parse and build `Link: <uri>; rel="next"` relations, the standard REST pagination mechanism; quote-aware (a comma inside a quoted parameter never splits the list)
|
|
102
|
+
- **JSON Type Definition** — RFC 8927 validation (`b.jtd.validate` / `isValid`): portable, cross-implementation schema validation (all eight forms — type / enum / elements / properties / values / discriminator / ref / empty), returning instancePath / schemaPath errors; validated against the official 316-case suite. Interop companion to the fluent `b.safeSchema` builder
|
|
102
103
|
- **JSONPath** — full RFC 9535 query evaluator (`b.jsonPath.query` / `paths`): name / wildcard / index / slice / descendant selectors, `?filter` expressions, and the five standard functions, with compile-time well-typedness checks (validated against the official 703-case compliance suite); complements the JSONPath guards
|
|
103
104
|
- **JSON Pointer / Patch** — RFC 6901 `b.jsonPointer.get` (reference a value by `/foo/0/bar`) + RFC 6902 `b.jsonPatch.apply` (atomic add / remove / replace / move / copy / test for HTTP PATCH; the input document is never mutated, structural `test` comparison) + RFC 7396 `b.jsonMergePatch.merge` (the `merge-patch+json` partial-document format; both PATCH formats are prototype-pollution-safe)
|
|
104
105
|
- **Canonical JSON** — RFC 8785 JSON Canonicalization Scheme (`b.canonicalJson.stringifyJcs`): the deterministic, sorted-key byte form to hash or sign (custom credentials, receipts, deterministic request signing); UTF-16 key ordering + ECMAScript number formatting, with a lenient `stringify` variant for Buffers / Dates / BigInts
|
|
@@ -157,7 +158,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
157
158
|
- **WebSockets (server)** — channel/room fan-out across cluster replicas; RFC 6455 §5.5 control-frame size + FIN enforcement on inbound (defends 1 MiB-PING-as-PONG amplification) (`b.websocket`, `b.websocketChannels`)
|
|
158
159
|
- **WebSockets (client)** — `b.wsClient` with PQC-TLS handshake, permessage-deflate negotiation with decompression-bomb cap, fatal UTF-8 validation, permanent-error classifier (skips reconnect on 4xx / accept mismatch / bad-subprotocol), exponential-backoff with full jitter
|
|
159
160
|
- **Pub/sub + events** — distributed pub/sub with cluster-table / Redis PUB/SUB / custom backends (`b.pubsub`); framework-emitted signal bus for breach / integrity events (`b.events`)
|
|
160
|
-
- **CloudEvents + SSE** — CloudEvents 1.0
|
|
161
|
+
- **CloudEvents + SSE** — CloudEvents 1.0.2 for AWS EventBridge / Knative / Azure Event Grid / Google Eventarc / CNCF: `wrap` / `parse` envelopes, non-throwing `validate` / `isValid`, the JSON event + batch formats (`toJSON` / `fromJSON` / `toJSONBatch` / `fromJSONBatch`), and the HTTP binding in both binary and structured content modes with auto-detecting `http.decode` (`b.cloudEvents`); Server-Sent Events with newline-injection refusal in `event:` / `id:` / `data:` / `Last-Event-ID` (CVE-2026-33128 / 29085 / 44217 class) (`b.sse`, `b.middleware.sse`)
|
|
161
162
|
- **Mail (outbound)** — multipart + attachments + DKIM + calendar invites; bounce intake (`b.mail`, `b.mailBounce`)
|
|
162
163
|
- **Mail (outbound delivery)** — turnkey MX-lookup → MTA-STS-fetch → DANE-TLSA → REQUIRETLS handshake → SMTP wire layer → RFC 3464 DSN-on-permanent-failure → deferred-retry scheduling, all wired once (`b.mail.send.deliver`)
|
|
163
164
|
- **Mail (inbound auth)** — SPF / DMARC / ARC verify + ARC chain signing for relays (`b.mail.spf`, `b.mail.dmarc`, `b.mail.arc`)
|
package/index.js
CHANGED
|
@@ -402,6 +402,7 @@ var jsonPointer = require("./lib/json-pointer");
|
|
|
402
402
|
var jsonPatch = require("./lib/json-patch");
|
|
403
403
|
var jsonMergePatch = require("./lib/json-merge-patch");
|
|
404
404
|
var jsonPath = require("./lib/json-path");
|
|
405
|
+
var jtd = require("./lib/jtd");
|
|
405
406
|
var standardWebhooks = require("./lib/standard-webhooks");
|
|
406
407
|
var lro = require("./lib/lro");
|
|
407
408
|
var jsonApi = require("./lib/jsonapi");
|
|
@@ -425,6 +426,7 @@ module.exports = {
|
|
|
425
426
|
jsonPatch: jsonPatch,
|
|
426
427
|
jsonMergePatch: jsonMergePatch,
|
|
427
428
|
jsonPath: jsonPath,
|
|
429
|
+
jtd: jtd,
|
|
428
430
|
standardWebhooks: standardWebhooks,
|
|
429
431
|
lro: lro,
|
|
430
432
|
jsonApi: jsonApi,
|
package/lib/backup/manifest.js
CHANGED
|
@@ -95,7 +95,7 @@ var VALID_KINDS = { "raw": 1, "vault-sealed": 1, "plaintext": 1 };
|
|
|
95
95
|
// in the manifest, so the hex string is 128 chars long.
|
|
96
96
|
var SHA3_512_HEX_LENGTH = 128;
|
|
97
97
|
var HEX_RE = safeBuffer.HEX_RE;
|
|
98
|
-
var BASE64_RE =
|
|
98
|
+
var BASE64_RE = safeBuffer.BASE64_RE;
|
|
99
99
|
|
|
100
100
|
function _isHex(s, evenLength) {
|
|
101
101
|
if (typeof s !== "string" || s.length === 0) return false;
|
package/lib/cloud-events.js
CHANGED
|
@@ -36,11 +36,16 @@
|
|
|
36
36
|
|
|
37
37
|
var nodeCrypto = require("node:crypto");
|
|
38
38
|
var validateOpts = require("./validate-opts");
|
|
39
|
+
var rfc3339 = require("./rfc3339");
|
|
40
|
+
var safeJson = require("./safe-json");
|
|
41
|
+
var safeBuffer = require("./safe-buffer");
|
|
42
|
+
var C = require("./constants");
|
|
39
43
|
var { defineClass } = require("./framework-error");
|
|
40
44
|
|
|
41
45
|
var CloudEventsError = defineClass("CloudEventsError", { alwaysPermanent: true });
|
|
42
46
|
|
|
43
47
|
var SPECVERSION = "1.0";
|
|
48
|
+
var DEFAULT_MAX_BYTES = C.BYTES.mib(1); // fromJSON / http.decode input cap
|
|
44
49
|
|
|
45
50
|
// CloudEvents §3.1 — required string attributes.
|
|
46
51
|
var REQUIRED_ATTRS = ["id", "source", "specversion", "type"];
|
|
@@ -268,9 +273,459 @@ function parse(envelope) {
|
|
|
268
273
|
};
|
|
269
274
|
}
|
|
270
275
|
|
|
276
|
+
// ---- validate / isValid (non-throwing spec check) ----
|
|
277
|
+
|
|
278
|
+
var INT_MIN = -2147483648; // allow:raw-byte-literal — CloudEvents Integer type range
|
|
279
|
+
var INT_MAX = 2147483647; // allow:raw-byte-literal — CloudEvents Integer type range
|
|
280
|
+
// JSON-formatted media type essence (after the parameters are stripped):
|
|
281
|
+
// type/json or type/anything+json. Each run is bounded by the single "/"
|
|
282
|
+
// separator so the match is linear (no overlapping quantifiers → no
|
|
283
|
+
// polynomial backtracking on hostile media-type strings).
|
|
284
|
+
var JSON_MEDIA_RE = /^[^/]+\/(?:[^/]+\+)?json$/i;
|
|
285
|
+
// Extension name MUST be lowercase ASCII alnum (the §3.1 ≤20 length is a
|
|
286
|
+
// SHOULD, so validate enforces only the MUST; wrap is stricter on emit).
|
|
287
|
+
var VALIDATE_EXT_NAME_RE = /^[a-z0-9]+$/;
|
|
288
|
+
|
|
289
|
+
function _isPlainObject(v) { return v !== null && typeof v === "object" && !Array.isArray(v) && !Buffer.isBuffer(v); }
|
|
290
|
+
function _isNonEmptyString(v) { return typeof v === "string" && v.length > 0; }
|
|
291
|
+
function _isCanonicalBase64(s) { return typeof s === "string" && s.length % 4 === 0 && safeBuffer.BASE64_RE.test(s); }
|
|
292
|
+
function _isJsonMedia(ct) {
|
|
293
|
+
if (ct == null) return true; // absent datacontenttype defaults to application/json
|
|
294
|
+
var essence = String(ct).split(";")[0].trim(); // drop media-type parameters
|
|
295
|
+
return JSON_MEDIA_RE.test(essence); // allow:regex-no-length-cap — slash-bounded classes, linear match
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function _extIssue(name, v) {
|
|
299
|
+
if (v === null) return null;
|
|
300
|
+
if (typeof v === "string" || typeof v === "boolean") return null;
|
|
301
|
+
if (typeof v === "number") {
|
|
302
|
+
if (!isFinite(v) || Math.floor(v) !== v) return "extension '" + name + "' must be an integer (CloudEvents has no float type)";
|
|
303
|
+
if (v < INT_MIN || v > INT_MAX) return "extension '" + name + "' integer out of 32-bit range";
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return "extension '" + name + "' must be a string, integer, or boolean";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @primitive b.cloudEvents.validate
|
|
311
|
+
* @signature b.cloudEvents.validate(event)
|
|
312
|
+
* @since 0.12.63
|
|
313
|
+
* @status stable
|
|
314
|
+
* @related b.cloudEvents.parse, b.cloudEvents.fromJSON
|
|
315
|
+
*
|
|
316
|
+
* Check an in-memory CloudEvents v1.0 envelope against the §3.1 spec and
|
|
317
|
+
* return an array of <code>{ attribute, message }</code> issues — an empty
|
|
318
|
+
* array means the event is conformant. Unlike <code>parse</code> (which
|
|
319
|
+
* throws and decodes), this never throws, so it suits inspecting events of
|
|
320
|
+
* unknown provenance before deciding what to do with them.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* b.cloudEvents.validate({ specversion: "1.0", id: "1",
|
|
324
|
+
* source: "/x", type: "com.example.t" });
|
|
325
|
+
* // → []
|
|
326
|
+
*/
|
|
327
|
+
function validate(event) {
|
|
328
|
+
var issues = [];
|
|
329
|
+
function bad(attribute, message) { issues.push({ attribute: attribute, message: message }); }
|
|
330
|
+
if (!_isPlainObject(event)) { bad("", "event must be an object"); return issues; }
|
|
331
|
+
|
|
332
|
+
if (event.specversion !== SPECVERSION) bad("specversion", "specversion must be the string \"" + SPECVERSION + "\"");
|
|
333
|
+
if (!_isNonEmptyString(event.id)) bad("id", "id must be a non-empty string");
|
|
334
|
+
if (!_isNonEmptyString(event.source)) bad("source", "source must be a non-empty URI-reference string");
|
|
335
|
+
if (!_isNonEmptyString(event.type)) bad("type", "type must be a non-empty string");
|
|
336
|
+
|
|
337
|
+
if (event.datacontenttype != null && !_isNonEmptyString(event.datacontenttype)) bad("datacontenttype", "datacontenttype, if present, must be a non-empty string");
|
|
338
|
+
if (event.dataschema != null && !_isNonEmptyString(event.dataschema)) bad("dataschema", "dataschema, if present, must be a non-empty URI string");
|
|
339
|
+
if (event.subject != null && !_isNonEmptyString(event.subject)) bad("subject", "subject, if present, must be a non-empty string");
|
|
340
|
+
if (event.time != null && !rfc3339.isValidDateTime(event.time)) bad("time", "time, if present, must be an RFC 3339 date-time");
|
|
341
|
+
|
|
342
|
+
if (Object.prototype.hasOwnProperty.call(event, "data") && Object.prototype.hasOwnProperty.call(event, "data_base64")) {
|
|
343
|
+
bad("data", "data and data_base64 are mutually exclusive (CloudEvents §3.1.1)");
|
|
344
|
+
}
|
|
345
|
+
if (event.data_base64 != null && !_isCanonicalBase64(event.data_base64)) bad("data_base64", "data_base64 must be canonical RFC 4648 base64");
|
|
346
|
+
|
|
347
|
+
Object.keys(event).forEach(function (k) {
|
|
348
|
+
if (REQUIRED_ATTRS.indexOf(k) !== -1 || KNOWN_OPTIONAL_ATTRS[k]) return;
|
|
349
|
+
if (!VALIDATE_EXT_NAME_RE.test(k)) { bad(k, "attribute name must match [a-z0-9]+ (lower-case letters and digits)"); return; } // allow:regex-no-length-cap — linear class, key bounded by maxBytes
|
|
350
|
+
var ei = _extIssue(k, event[k]);
|
|
351
|
+
if (ei) bad(k, ei);
|
|
352
|
+
});
|
|
353
|
+
return issues;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @primitive b.cloudEvents.isValid
|
|
358
|
+
* @signature b.cloudEvents.isValid(event)
|
|
359
|
+
* @since 0.12.63
|
|
360
|
+
* @status stable
|
|
361
|
+
* @related b.cloudEvents.validate
|
|
362
|
+
*
|
|
363
|
+
* Boolean convenience form of <code>validate</code> — <code>true</code>
|
|
364
|
+
* when the event has zero conformance issues.
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* b.cloudEvents.isValid(evt); // → true
|
|
368
|
+
*/
|
|
369
|
+
function isValid(event) { return validate(event).length === 0; }
|
|
370
|
+
|
|
371
|
+
function _assertValid(event, label) {
|
|
372
|
+
var issues = validate(event);
|
|
373
|
+
if (issues.length) {
|
|
374
|
+
throw new CloudEventsError("cloud-events/invalid",
|
|
375
|
+
label + ": " + issues.map(function (i) { return i.message; }).join("; "));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---- JSON event format ----
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @primitive b.cloudEvents.toJSON
|
|
383
|
+
* @signature b.cloudEvents.toJSON(event, opts?)
|
|
384
|
+
* @since 0.12.63
|
|
385
|
+
* @status stable
|
|
386
|
+
* @related b.cloudEvents.fromJSON, b.cloudEvents.toJSONBatch
|
|
387
|
+
*
|
|
388
|
+
* Serialize a CloudEvents envelope (as produced by <code>wrap</code>) to a
|
|
389
|
+
* JSON event-format string — media type
|
|
390
|
+
* <code>application/cloudevents+json</code>. The envelope is already in
|
|
391
|
+
* wire shape (JSON <code>data</code> inline, binary as a
|
|
392
|
+
* <code>data_base64</code> string), so this validates it and renders the
|
|
393
|
+
* JSON. Throws <code>CloudEventsError</code> on a non-conformant event.
|
|
394
|
+
*
|
|
395
|
+
* @opts
|
|
396
|
+
* space: number | string, // JSON.stringify indentation (default: none)
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* var json = b.cloudEvents.toJSON(b.cloudEvents.wrap({ source: "/x", type: "t" }));
|
|
400
|
+
*/
|
|
401
|
+
function toJSON(event, opts) {
|
|
402
|
+
opts = opts || {};
|
|
403
|
+
_assertValid(event, "cloudEvents.toJSON");
|
|
404
|
+
return JSON.stringify(event, null, opts.space);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function _coerceInput(input, label, maxBytes) {
|
|
408
|
+
if (!Buffer.isBuffer(input) && typeof input !== "string") {
|
|
409
|
+
throw new CloudEventsError("cloud-events/bad-input", label + ": input must be a string or Buffer");
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
return safeJson.parse(input, { maxBytes: maxBytes });
|
|
413
|
+
} catch (e) {
|
|
414
|
+
if (e && e.code === "json/too-large") throw new CloudEventsError("cloud-events/too-large", label + ": input exceeds maxBytes (" + maxBytes + ")");
|
|
415
|
+
throw new CloudEventsError("cloud-events/bad-json", label + ": body is not valid JSON");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @primitive b.cloudEvents.fromJSON
|
|
421
|
+
* @signature b.cloudEvents.fromJSON(input, opts?)
|
|
422
|
+
* @since 0.12.63
|
|
423
|
+
* @status stable
|
|
424
|
+
* @related b.cloudEvents.toJSON, b.cloudEvents.parse
|
|
425
|
+
*
|
|
426
|
+
* Parse a single JSON event-format document (string or Buffer) into a
|
|
427
|
+
* validated CloudEvents envelope. Untrusted bytes route through the
|
|
428
|
+
* framework's bounded, prototype-pollution-safe JSON reader. The envelope
|
|
429
|
+
* is returned in wire shape (binary stays a <code>data_base64</code>
|
|
430
|
+
* string); call <code>parse</code> instead when you want the
|
|
431
|
+
* Buffer-decoded record. Throws <code>CloudEventsError</code> on malformed
|
|
432
|
+
* or non-conformant input.
|
|
433
|
+
*
|
|
434
|
+
* @opts
|
|
435
|
+
* maxBytes: number, // default: 1 MiB — reject larger inputs
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* var evt = b.cloudEvents.fromJSON(req.rawBody);
|
|
439
|
+
*/
|
|
440
|
+
function fromJSON(input, opts) {
|
|
441
|
+
opts = opts || {};
|
|
442
|
+
var maxBytes = opts.maxBytes == null ? DEFAULT_MAX_BYTES : opts.maxBytes;
|
|
443
|
+
var obj = _coerceInput(input, "cloudEvents.fromJSON", maxBytes);
|
|
444
|
+
if (!_isPlainObject(obj)) throw new CloudEventsError("cloud-events/invalid", "cloudEvents.fromJSON: event must be a JSON object");
|
|
445
|
+
_assertValid(obj, "cloudEvents.fromJSON");
|
|
446
|
+
return obj;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* @primitive b.cloudEvents.toJSONBatch
|
|
451
|
+
* @signature b.cloudEvents.toJSONBatch(events, opts?)
|
|
452
|
+
* @since 0.12.63
|
|
453
|
+
* @status stable
|
|
454
|
+
* @related b.cloudEvents.fromJSONBatch, b.cloudEvents.toJSON
|
|
455
|
+
*
|
|
456
|
+
* Serialize an array of CloudEvents envelopes to the JSON batch format
|
|
457
|
+
* (media type <code>application/cloudevents-batch+json</code>) — a JSON
|
|
458
|
+
* array of events, each rendered as by <code>toJSON</code>. An empty array
|
|
459
|
+
* yields <code>"[]"</code>.
|
|
460
|
+
*
|
|
461
|
+
* @opts
|
|
462
|
+
* space: number | string, // JSON.stringify indentation (default: none)
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* var body = b.cloudEvents.toJSONBatch([evtA, evtB]);
|
|
466
|
+
*/
|
|
467
|
+
function toJSONBatch(events, opts) {
|
|
468
|
+
opts = opts || {};
|
|
469
|
+
if (!Array.isArray(events)) throw new CloudEventsError("cloud-events/bad-input", "cloudEvents.toJSONBatch: events must be an array");
|
|
470
|
+
events.forEach(function (event, idx) { _assertValid(event, "cloudEvents.toJSONBatch: event[" + idx + "]"); });
|
|
471
|
+
return JSON.stringify(events, null, opts.space);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* @primitive b.cloudEvents.fromJSONBatch
|
|
476
|
+
* @signature b.cloudEvents.fromJSONBatch(input, opts?)
|
|
477
|
+
* @since 0.12.63
|
|
478
|
+
* @status stable
|
|
479
|
+
* @related b.cloudEvents.toJSONBatch, b.cloudEvents.fromJSON
|
|
480
|
+
*
|
|
481
|
+
* Parse a JSON batch (a JSON array of events) from a string or Buffer into
|
|
482
|
+
* an array of validated CloudEvents envelopes. Each element is validated as
|
|
483
|
+
* by <code>fromJSON</code>; an empty array is valid. A non-array body,
|
|
484
|
+
* over-size input, or any non-conformant element throws
|
|
485
|
+
* <code>CloudEventsError</code>.
|
|
486
|
+
*
|
|
487
|
+
* @opts
|
|
488
|
+
* maxBytes: number, // default: 1 MiB — reject larger inputs
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* var events = b.cloudEvents.fromJSONBatch(req.rawBody);
|
|
492
|
+
*/
|
|
493
|
+
function fromJSONBatch(input, opts) {
|
|
494
|
+
opts = opts || {};
|
|
495
|
+
var maxBytes = opts.maxBytes == null ? DEFAULT_MAX_BYTES : opts.maxBytes;
|
|
496
|
+
var arr = _coerceInput(input, "cloudEvents.fromJSONBatch", maxBytes);
|
|
497
|
+
if (!Array.isArray(arr)) throw new CloudEventsError("cloud-events/invalid", "cloudEvents.fromJSONBatch: body must be a JSON array");
|
|
498
|
+
arr.forEach(function (obj, idx) {
|
|
499
|
+
if (!_isPlainObject(obj)) throw new CloudEventsError("cloud-events/invalid", "cloudEvents.fromJSONBatch: event[" + idx + "] must be a JSON object");
|
|
500
|
+
_assertValid(obj, "cloudEvents.fromJSONBatch: event[" + idx + "]");
|
|
501
|
+
});
|
|
502
|
+
return arr;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ---- HTTP protocol binding ----
|
|
506
|
+
|
|
507
|
+
var STRUCTURED_CT = "application/cloudevents+json; charset=UTF-8";
|
|
508
|
+
var BATCH_CT = "application/cloudevents-batch+json; charset=UTF-8";
|
|
509
|
+
|
|
510
|
+
// Percent-encode a header value: everything outside printable ASCII plus
|
|
511
|
+
// the spec-named space / double-quote / percent (HTTP binding §3.1). `s` is
|
|
512
|
+
// always the already-stringified value from _headerValueFor.
|
|
513
|
+
function _pctEncode(s) {
|
|
514
|
+
var bytes = Buffer.from(s, "utf8");
|
|
515
|
+
var out = "";
|
|
516
|
+
for (var i = 0; i < bytes.length; i += 1) {
|
|
517
|
+
var by = bytes[i];
|
|
518
|
+
if (by < 0x21 || by > 0x7E || by === 0x22 || by === 0x25) { // allow:raw-byte-literal — printable-ASCII bounds + double-quote and percent (HTTP binding header rule)
|
|
519
|
+
out += "%" + bytes[i].toString(16).toUpperCase().padStart(2, "0"); // allow:raw-byte-literal — 16 is the hex radix
|
|
520
|
+
} else {
|
|
521
|
+
out += String.fromCharCode(by);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
function _pctDecode(s) {
|
|
527
|
+
var bytes = [];
|
|
528
|
+
var i = 0;
|
|
529
|
+
while (i < s.length) {
|
|
530
|
+
if (s[i] === "%" && /^[0-9A-Fa-f]{2}$/.test(s.slice(i + 1, i + 3))) {
|
|
531
|
+
bytes.push(parseInt(s.slice(i + 1, i + 3), 16)); // allow:raw-byte-literal — 16 is the hex radix
|
|
532
|
+
i += 3;
|
|
533
|
+
} else {
|
|
534
|
+
var ch = Buffer.from(s[i], "utf8");
|
|
535
|
+
for (var j = 0; j < ch.length; j += 1) bytes.push(ch[j]);
|
|
536
|
+
i += 1;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return Buffer.from(bytes).toString("utf8");
|
|
540
|
+
}
|
|
541
|
+
function _headerValueFor(v) { return typeof v === "boolean" ? (v ? "true" : "false") : String(v); }
|
|
542
|
+
function _lowerHeaders(headers) {
|
|
543
|
+
var out = {};
|
|
544
|
+
Object.keys(headers || {}).forEach(function (k) {
|
|
545
|
+
var v = headers[k];
|
|
546
|
+
out[k.toLowerCase()] = Array.isArray(v) ? v.join(",") : v;
|
|
547
|
+
});
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @primitive b.cloudEvents.http.encodeBinary
|
|
553
|
+
* @signature b.cloudEvents.http.encodeBinary(event)
|
|
554
|
+
* @since 0.12.63
|
|
555
|
+
* @status stable
|
|
556
|
+
* @related b.cloudEvents.http.decode, b.cloudEvents.http.encodeStructured
|
|
557
|
+
*
|
|
558
|
+
* Render a CloudEvents envelope in HTTP <em>binary</em> content mode: each
|
|
559
|
+
* context attribute (and extension) becomes a <code>ce-</code>-prefixed
|
|
560
|
+
* header with a percent-encoded value, <code>datacontenttype</code> maps to
|
|
561
|
+
* the plain <code>Content-Type</code> header (never
|
|
562
|
+
* <code>ce-datacontenttype</code>), and the payload becomes the body.
|
|
563
|
+
* Returns <code>{ headers, body }</code> where <code>body</code> is a
|
|
564
|
+
* Buffer (for <code>data_base64</code> payloads) or a string.
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* var enc = b.cloudEvents.http.encodeBinary(evt);
|
|
568
|
+
* // enc.headers["ce-id"], enc.headers["content-type"], enc.body
|
|
569
|
+
*/
|
|
570
|
+
function encodeBinary(event) {
|
|
571
|
+
_assertValid(event, "cloudEvents.http.encodeBinary");
|
|
572
|
+
var headers = {};
|
|
573
|
+
Object.keys(event).forEach(function (k) {
|
|
574
|
+
if (k === "data" || k === "data_base64" || k === "datacontenttype") return;
|
|
575
|
+
if (event[k] === undefined || event[k] === null) return;
|
|
576
|
+
headers["ce-" + k] = _pctEncode(_headerValueFor(event[k]));
|
|
577
|
+
});
|
|
578
|
+
var body;
|
|
579
|
+
if (event.data_base64 != null) {
|
|
580
|
+
body = Buffer.from(event.data_base64, "base64");
|
|
581
|
+
} else if (Object.prototype.hasOwnProperty.call(event, "data")) {
|
|
582
|
+
// JSON-media payloads (including a bare string under application/json or
|
|
583
|
+
// an absent content type, which defaults to JSON) must be JSON-encoded
|
|
584
|
+
// so the body re-parses; a non-JSON media type carries the string as-is.
|
|
585
|
+
if (_isJsonMedia(event.datacontenttype)) body = JSON.stringify(event.data);
|
|
586
|
+
else body = typeof event.data === "string" ? event.data : JSON.stringify(event.data);
|
|
587
|
+
} else body = "";
|
|
588
|
+
if (event.datacontenttype != null) headers["content-type"] = event.datacontenttype;
|
|
589
|
+
else if (_isPlainObject(event.data) || Array.isArray(event.data)) headers["content-type"] = "application/json";
|
|
590
|
+
return { headers: headers, body: body };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* @primitive b.cloudEvents.http.encodeStructured
|
|
595
|
+
* @signature b.cloudEvents.http.encodeStructured(event)
|
|
596
|
+
* @since 0.12.63
|
|
597
|
+
* @status stable
|
|
598
|
+
* @related b.cloudEvents.http.decode, b.cloudEvents.http.encodeBinary
|
|
599
|
+
*
|
|
600
|
+
* Render a CloudEvents envelope in HTTP <em>structured</em> content mode:
|
|
601
|
+
* the whole event is serialized via the JSON event format into the body,
|
|
602
|
+
* with <code>Content-Type: application/cloudevents+json</code>. Returns
|
|
603
|
+
* <code>{ headers, body }</code>.
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* var enc = b.cloudEvents.http.encodeStructured(evt);
|
|
607
|
+
*/
|
|
608
|
+
function encodeStructured(event) {
|
|
609
|
+
return { headers: { "content-type": STRUCTURED_CT }, body: toJSON(event) };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* @primitive b.cloudEvents.http.encodeBatch
|
|
614
|
+
* @signature b.cloudEvents.http.encodeBatch(events)
|
|
615
|
+
* @since 0.12.63
|
|
616
|
+
* @status stable
|
|
617
|
+
* @related b.cloudEvents.http.decode, b.cloudEvents.toJSONBatch
|
|
618
|
+
*
|
|
619
|
+
* Render an array of CloudEvents in HTTP <em>batched</em> content mode: the
|
|
620
|
+
* JSON batch format in the body with <code>Content-Type:
|
|
621
|
+
* application/cloudevents-batch+json</code>. Returns
|
|
622
|
+
* <code>{ headers, body }</code>.
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
* var enc = b.cloudEvents.http.encodeBatch([evtA, evtB]);
|
|
626
|
+
*/
|
|
627
|
+
function encodeBatch(events) {
|
|
628
|
+
return { headers: { "content-type": BATCH_CT }, body: toJSONBatch(events) };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* @primitive b.cloudEvents.http.decodeBinary
|
|
633
|
+
* @signature b.cloudEvents.http.decodeBinary(headers, body, opts?)
|
|
634
|
+
* @since 0.12.63
|
|
635
|
+
* @status stable
|
|
636
|
+
* @related b.cloudEvents.http.decode, b.cloudEvents.http.encodeBinary
|
|
637
|
+
*
|
|
638
|
+
* Parse an HTTP binary-mode request into a CloudEvents envelope. Headers are
|
|
639
|
+
* matched case-insensitively; each <code>ce-*</code> header is
|
|
640
|
+
* percent-decoded into the matching attribute, <code>Content-Type</code>
|
|
641
|
+
* becomes <code>datacontenttype</code>, and the body becomes the payload
|
|
642
|
+
* (parsed as JSON when the content type is JSON, kept as a
|
|
643
|
+
* <code>data_base64</code> string for opaque bytes). The result is
|
|
644
|
+
* validated. Binary-mode header values are strings, so extension types
|
|
645
|
+
* other than String are not recovered.
|
|
646
|
+
*
|
|
647
|
+
* @opts
|
|
648
|
+
* maxBytes: number, // default: 1 MiB — reject larger bodies
|
|
649
|
+
*
|
|
650
|
+
* @example
|
|
651
|
+
* var evt = b.cloudEvents.http.decodeBinary(req.headers, req.rawBody);
|
|
652
|
+
*/
|
|
653
|
+
function decodeBinary(headers, body, opts) {
|
|
654
|
+
opts = opts || {};
|
|
655
|
+
var maxBytes = opts.maxBytes == null ? DEFAULT_MAX_BYTES : opts.maxBytes;
|
|
656
|
+
var h = _lowerHeaders(headers);
|
|
657
|
+
var event = {};
|
|
658
|
+
Object.keys(h).forEach(function (k) {
|
|
659
|
+
if (k.indexOf("ce-") !== 0) return;
|
|
660
|
+
event[k.slice(3)] = _pctDecode(String(h[k]));
|
|
661
|
+
});
|
|
662
|
+
var ct = h["content-type"] != null ? h["content-type"] : null;
|
|
663
|
+
if (ct != null) event.datacontenttype = ct;
|
|
664
|
+
var raw;
|
|
665
|
+
if (body == null) raw = Buffer.alloc(0);
|
|
666
|
+
else if (Buffer.isBuffer(body)) raw = body;
|
|
667
|
+
else if (typeof body === "string") raw = Buffer.from(body, "utf8");
|
|
668
|
+
else throw new CloudEventsError("cloud-events/bad-input", "cloudEvents.http.decodeBinary: body must be a string or Buffer");
|
|
669
|
+
if (raw.length > maxBytes) throw new CloudEventsError("cloud-events/too-large", "cloudEvents.http.decodeBinary: body exceeds maxBytes (" + maxBytes + ")");
|
|
670
|
+
if (raw.length > 0) {
|
|
671
|
+
if (_isJsonMedia(ct)) {
|
|
672
|
+
try { event.data = safeJson.parse(raw, { maxBytes: maxBytes }); }
|
|
673
|
+
catch (_e) { throw new CloudEventsError("cloud-events/bad-json", "cloudEvents.http.decodeBinary: JSON body is not valid JSON"); }
|
|
674
|
+
} else if (typeof ct === "string" && /^text\//i.test(ct)) {
|
|
675
|
+
event.data = raw.toString("utf8");
|
|
676
|
+
} else {
|
|
677
|
+
event.data_base64 = raw.toString("base64");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
_assertValid(event, "cloudEvents.http.decodeBinary");
|
|
681
|
+
return event;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* @primitive b.cloudEvents.http.decode
|
|
686
|
+
* @signature b.cloudEvents.http.decode(headers, body, opts?)
|
|
687
|
+
* @since 0.12.63
|
|
688
|
+
* @status stable
|
|
689
|
+
* @related b.cloudEvents.http.decodeBinary, b.cloudEvents.http.encodeStructured
|
|
690
|
+
*
|
|
691
|
+
* Parse an HTTP request into a CloudEvents envelope (or array, for a batch),
|
|
692
|
+
* auto-detecting the content mode exactly as a conformant receiver does: a
|
|
693
|
+
* <code>Content-Type</code> beginning
|
|
694
|
+
* <code>application/cloudevents-batch</code> is batched, one beginning
|
|
695
|
+
* <code>application/cloudevents</code> is structured, and anything else is
|
|
696
|
+
* binary mode. Returns a single envelope for binary/structured modes and an
|
|
697
|
+
* array for batched mode.
|
|
698
|
+
*
|
|
699
|
+
* @opts
|
|
700
|
+
* maxBytes: number, // default: 1 MiB — reject larger bodies
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* var evt = b.cloudEvents.http.decode(req.headers, req.rawBody);
|
|
704
|
+
*/
|
|
705
|
+
function decode(headers, body, opts) {
|
|
706
|
+
var h = _lowerHeaders(headers);
|
|
707
|
+
var ct = (h["content-type"] != null ? h["content-type"] : "") || "";
|
|
708
|
+
if (/^application\/cloudevents-batch\b/i.test(ct)) return fromJSONBatch(body, opts);
|
|
709
|
+
if (/^application\/cloudevents\b/i.test(ct)) return fromJSON(body, opts);
|
|
710
|
+
return decodeBinary(headers, body, opts);
|
|
711
|
+
}
|
|
712
|
+
|
|
271
713
|
module.exports = {
|
|
272
714
|
wrap: wrap,
|
|
273
715
|
parse: parse,
|
|
716
|
+
validate: validate,
|
|
717
|
+
isValid: isValid,
|
|
718
|
+
toJSON: toJSON,
|
|
719
|
+
fromJSON: fromJSON,
|
|
720
|
+
toJSONBatch: toJSONBatch,
|
|
721
|
+
fromJSONBatch: fromJSONBatch,
|
|
722
|
+
http: {
|
|
723
|
+
encodeBinary: encodeBinary,
|
|
724
|
+
encodeStructured: encodeStructured,
|
|
725
|
+
encodeBatch: encodeBatch,
|
|
726
|
+
decodeBinary: decodeBinary,
|
|
727
|
+
decode: decode,
|
|
728
|
+
},
|
|
274
729
|
SPECVERSION: SPECVERSION,
|
|
275
730
|
REQUIRED_ATTRS: REQUIRED_ATTRS,
|
|
276
731
|
CloudEventsError: CloudEventsError,
|
package/lib/csp.js
CHANGED
|
@@ -67,9 +67,10 @@ var ALL_DIRECTIVES = [
|
|
|
67
67
|
"default-src", "script-src", "script-src-elem", "script-src-attr",
|
|
68
68
|
"style-src", "style-src-elem", "style-src-attr",
|
|
69
69
|
"img-src", "media-src", "font-src", "connect-src", "object-src",
|
|
70
|
-
"frame-src", "child-src", "worker-src", "
|
|
70
|
+
"frame-src", "child-src", "worker-src", "fenced-frame-src",
|
|
71
|
+
"manifest-src", "prefetch-src",
|
|
71
72
|
"form-action", "frame-ancestors", "navigate-to", "base-uri", "sandbox",
|
|
72
|
-
"report-to", "report-uri",
|
|
73
|
+
"webrtc", "report-to", "report-uri",
|
|
73
74
|
"require-trusted-types-for", "trusted-types",
|
|
74
75
|
"upgrade-insecure-requests", "block-all-mixed-content",
|
|
75
76
|
];
|
package/lib/jtd.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.jtd
|
|
4
|
+
* @nav Data
|
|
5
|
+
* @title JSON Type Definition
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Validate a JSON value against a JSON Type Definition schema (RFC
|
|
9
|
+
* 8927) — a small, portable, cross-implementation schema language.
|
|
10
|
+
* Unlike the framework's fluent <code>b.safeSchema</code> builder, a
|
|
11
|
+
* JTD schema is plain JSON you can share with any JTD implementation
|
|
12
|
+
* (and generate code from), which makes it the right choice for
|
|
13
|
+
* interop contracts.
|
|
14
|
+
*
|
|
15
|
+
* <code>validate(schema, instance)</code> returns an array of errors,
|
|
16
|
+
* each a <code>{ instancePath, schemaPath }</code> pair pointing at the
|
|
17
|
+
* offending value and the schema rule it broke (an empty array means
|
|
18
|
+
* valid). All eight schema forms are supported — empty, <code>type</code>,
|
|
19
|
+
* <code>enum</code>, <code>elements</code>, <code>properties</code>,
|
|
20
|
+
* <code>values</code>, <code>discriminator</code>, and <code>ref</code>
|
|
21
|
+
* (with <code>definitions</code>) — and a malformed schema is rejected
|
|
22
|
+
* at compile time rather than mis-validating.
|
|
23
|
+
*
|
|
24
|
+
* @card
|
|
25
|
+
* JSON Type Definition (RFC 8927) — validate JSON against a portable,
|
|
26
|
+
* standardized schema (all eight forms), returning instancePath /
|
|
27
|
+
* schemaPath errors. The interop-friendly companion to the fluent
|
|
28
|
+
* <code>b.safeSchema</code> builder.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
var { defineClass } = require("./framework-error");
|
|
32
|
+
var rfc3339 = require("./rfc3339");
|
|
33
|
+
|
|
34
|
+
var JtdError = defineClass("JtdError", { alwaysPermanent: true });
|
|
35
|
+
|
|
36
|
+
var MAX_DEPTH = 10000; // allow:raw-time-literal — recursion cap for self-referential refs
|
|
37
|
+
|
|
38
|
+
var TYPES = {
|
|
39
|
+
boolean: 1, string: 1, timestamp: 1, float32: 1, float64: 1,
|
|
40
|
+
int8: 1, uint8: 1, int16: 1, uint16: 1, int32: 1, uint32: 1,
|
|
41
|
+
};
|
|
42
|
+
var INT_RANGES = {
|
|
43
|
+
int8: [-128, 127], uint8: [0, 255], int16: [-32768, 32767], // allow:raw-byte-literal — RFC 8927 integer type bounds
|
|
44
|
+
uint16: [0, 65535], int32: [-2147483648, 2147483647], uint32: [0, 4294967295], // allow:raw-byte-literal — RFC 8927 integer type bounds
|
|
45
|
+
};
|
|
46
|
+
var FORM_KEYWORDS = ["ref", "type", "enum", "elements", "properties", "optionalProperties", "values", "discriminator"];
|
|
47
|
+
var SHARED_KEYWORDS = { definitions: 1, nullable: 1, metadata: 1 };
|
|
48
|
+
|
|
49
|
+
function _isPlainObject(v) { return v !== null && typeof v === "object" && !Array.isArray(v); }
|
|
50
|
+
function _isInteger(v) { return typeof v === "number" && isFinite(v) && Math.floor(v) === v; }
|
|
51
|
+
|
|
52
|
+
// RFC 3339 date-time (the JTD "timestamp" type) — strict form shared with
|
|
53
|
+
// the other spec-driven consumers via lib/rfc3339.js.
|
|
54
|
+
var _validTimestamp = rfc3339.isValidDateTime;
|
|
55
|
+
|
|
56
|
+
// --- compile-time well-formedness (RFC 8927 section 2.2) ---
|
|
57
|
+
function _checkSchema(schema, root, isRoot) {
|
|
58
|
+
if (!_isPlainObject(schema)) throw new JtdError("jtd/bad-schema", "jtd: schema must be an object");
|
|
59
|
+
if (!isRoot && Object.prototype.hasOwnProperty.call(schema, "definitions")) throw new JtdError("jtd/bad-schema", "jtd: 'definitions' is allowed only at the root");
|
|
60
|
+
Object.keys(schema).forEach(function (k) {
|
|
61
|
+
if (FORM_KEYWORDS.indexOf(k) === -1 && !SHARED_KEYWORDS[k] && k !== "additionalProperties" && k !== "mapping") throw new JtdError("jtd/bad-schema", "jtd: unknown keyword '" + k + "'");
|
|
62
|
+
});
|
|
63
|
+
if (Object.prototype.hasOwnProperty.call(schema, "nullable") && typeof schema.nullable !== "boolean") throw new JtdError("jtd/bad-schema", "jtd: 'nullable' must be a boolean");
|
|
64
|
+
if (Object.prototype.hasOwnProperty.call(schema, "metadata") && !_isPlainObject(schema.metadata)) throw new JtdError("jtd/bad-schema", "jtd: 'metadata' must be an object");
|
|
65
|
+
if (Object.prototype.hasOwnProperty.call(schema, "definitions")) {
|
|
66
|
+
if (!_isPlainObject(schema.definitions)) throw new JtdError("jtd/bad-schema", "jtd: 'definitions' must be an object");
|
|
67
|
+
Object.keys(schema.definitions).forEach(function (k) { _checkSchema(schema.definitions[k], root, false); });
|
|
68
|
+
}
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(schema, "mapping") && !Object.prototype.hasOwnProperty.call(schema, "discriminator")) throw new JtdError("jtd/bad-schema", "jtd: 'mapping' is only valid with 'discriminator'");
|
|
70
|
+
var formSet = {};
|
|
71
|
+
FORM_KEYWORDS.forEach(function (k) { if (Object.prototype.hasOwnProperty.call(schema, k)) formSet[(k === "optionalProperties") ? "properties" : k] = 1; });
|
|
72
|
+
var formNames = Object.keys(formSet);
|
|
73
|
+
if (formNames.length > 1) throw new JtdError("jtd/bad-schema", "jtd: a schema may use only one form (got " + formNames.join(", ") + ")");
|
|
74
|
+
|
|
75
|
+
if ("ref" in schema) {
|
|
76
|
+
if (typeof schema.ref !== "string" || !root.definitions || !Object.prototype.hasOwnProperty.call(root.definitions, schema.ref)) throw new JtdError("jtd/bad-schema", "jtd: 'ref' must name a key in the root definitions");
|
|
77
|
+
}
|
|
78
|
+
if ("type" in schema && !TYPES[schema.type]) throw new JtdError("jtd/bad-schema", "jtd: unknown type '" + schema.type + "'");
|
|
79
|
+
if ("enum" in schema) {
|
|
80
|
+
if (!Array.isArray(schema.enum) || schema.enum.length === 0) throw new JtdError("jtd/bad-schema", "jtd: 'enum' must be a non-empty array");
|
|
81
|
+
var seen = Object.create(null);
|
|
82
|
+
schema.enum.forEach(function (e) { if (typeof e !== "string") throw new JtdError("jtd/bad-schema", "jtd: 'enum' values must be strings"); if (seen[e]) throw new JtdError("jtd/bad-schema", "jtd: duplicate enum value"); seen[e] = 1; });
|
|
83
|
+
}
|
|
84
|
+
if ("elements" in schema) _checkSchema(schema.elements, root, false);
|
|
85
|
+
if ("values" in schema) _checkSchema(schema.values, root, false);
|
|
86
|
+
if ("properties" in schema || "optionalProperties" in schema) {
|
|
87
|
+
var props = schema.properties || {}, opt = schema.optionalProperties || {};
|
|
88
|
+
if (!_isPlainObject(props) || !_isPlainObject(opt)) throw new JtdError("jtd/bad-schema", "jtd: properties / optionalProperties must be objects");
|
|
89
|
+
Object.keys(props).forEach(function (k) { if (Object.prototype.hasOwnProperty.call(opt, k)) throw new JtdError("jtd/bad-schema", "jtd: '" + k + "' in both properties and optionalProperties"); _checkSchema(props[k], root, false); });
|
|
90
|
+
Object.keys(opt).forEach(function (k) { _checkSchema(opt[k], root, false); });
|
|
91
|
+
if ("additionalProperties" in schema && typeof schema.additionalProperties !== "boolean") throw new JtdError("jtd/bad-schema", "jtd: 'additionalProperties' must be a boolean");
|
|
92
|
+
}
|
|
93
|
+
if ("discriminator" in schema) {
|
|
94
|
+
if (typeof schema.discriminator !== "string") throw new JtdError("jtd/bad-schema", "jtd: 'discriminator' must be a string");
|
|
95
|
+
if (!_isPlainObject(schema.mapping)) throw new JtdError("jtd/bad-schema", "jtd: 'discriminator' requires a 'mapping' object");
|
|
96
|
+
Object.keys(schema.mapping).forEach(function (k) {
|
|
97
|
+
var sub = schema.mapping[k];
|
|
98
|
+
_checkSchema(sub, root, false);
|
|
99
|
+
if (!_isPlainObject(sub) || (!("properties" in sub) && !("optionalProperties" in sub))) throw new JtdError("jtd/bad-schema", "jtd: discriminator mapping schemas must use the properties form");
|
|
100
|
+
if (sub.nullable === true) throw new JtdError("jtd/bad-schema", "jtd: discriminator mapping schemas must not be nullable");
|
|
101
|
+
var p = sub.properties || {}, o = sub.optionalProperties || {};
|
|
102
|
+
if (Object.prototype.hasOwnProperty.call(p, schema.discriminator) || Object.prototype.hasOwnProperty.call(o, schema.discriminator)) throw new JtdError("jtd/bad-schema", "jtd: discriminator tag must not appear in a mapping schema's properties");
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if ("additionalProperties" in schema && !("properties" in schema) && !("optionalProperties" in schema)) throw new JtdError("jtd/bad-schema", "jtd: 'additionalProperties' requires a properties form");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- validation (RFC 8927 section 3.3) ---
|
|
109
|
+
function _typeOk(type, v) {
|
|
110
|
+
if (type === "boolean") return typeof v === "boolean";
|
|
111
|
+
if (type === "string") return typeof v === "string";
|
|
112
|
+
if (type === "timestamp") return typeof v === "string" && _validTimestamp(v);
|
|
113
|
+
if (type === "float32" || type === "float64") return typeof v === "number" && isFinite(v);
|
|
114
|
+
var range = INT_RANGES[type];
|
|
115
|
+
return _isInteger(v) && v >= range[0] && v <= range[1];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _val(schema, inst, ip, sp, root, depth, errors, discrimTag) {
|
|
119
|
+
if (depth > MAX_DEPTH) throw new JtdError("jtd/too-deep", "jtd: schema recursion exceeded the depth cap");
|
|
120
|
+
if (schema.nullable === true && inst === null) return;
|
|
121
|
+
|
|
122
|
+
if ("ref" in schema) { _val(root.definitions[schema.ref], inst, ip, ["definitions", schema.ref], root, depth + 1, errors, undefined); return; }
|
|
123
|
+
|
|
124
|
+
if ("type" in schema) {
|
|
125
|
+
if (!_typeOk(schema.type, inst)) errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("type") });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if ("enum" in schema) {
|
|
129
|
+
if (typeof inst !== "string" || schema.enum.indexOf(inst) === -1) errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("enum") });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if ("elements" in schema) {
|
|
133
|
+
if (!Array.isArray(inst)) { errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("elements") }); return; }
|
|
134
|
+
for (var i = 0; i < inst.length; i++) _val(schema.elements, inst[i], ip.concat(String(i)), sp.concat("elements"), root, depth + 1, errors, undefined);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if ("properties" in schema || "optionalProperties" in schema) {
|
|
138
|
+
if (!_isPlainObject(inst)) {
|
|
139
|
+
errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("properties" in schema ? "properties" : "optionalProperties") });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
var props = schema.properties || {}, opt = schema.optionalProperties || {};
|
|
143
|
+
Object.keys(props).forEach(function (k) {
|
|
144
|
+
if (Object.prototype.hasOwnProperty.call(inst, k)) _val(props[k], inst[k], ip.concat(k), sp.concat("properties", k), root, depth + 1, errors, undefined);
|
|
145
|
+
else errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("properties", k) });
|
|
146
|
+
});
|
|
147
|
+
Object.keys(opt).forEach(function (k) {
|
|
148
|
+
if (Object.prototype.hasOwnProperty.call(inst, k)) _val(opt[k], inst[k], ip.concat(k), sp.concat("optionalProperties", k), root, depth + 1, errors, undefined);
|
|
149
|
+
});
|
|
150
|
+
if (schema.additionalProperties !== true) {
|
|
151
|
+
Object.keys(inst).forEach(function (k) {
|
|
152
|
+
if (!Object.prototype.hasOwnProperty.call(props, k) && !Object.prototype.hasOwnProperty.call(opt, k) && k !== discrimTag) {
|
|
153
|
+
errors.push({ instancePath: ip.concat(k), schemaPath: sp.slice() });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if ("values" in schema) {
|
|
160
|
+
if (!_isPlainObject(inst)) { errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("values") }); return; }
|
|
161
|
+
Object.keys(inst).forEach(function (k) { _val(schema.values, inst[k], ip.concat(k), sp.concat("values"), root, depth + 1, errors, undefined); });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if ("discriminator" in schema) {
|
|
165
|
+
if (!_isPlainObject(inst)) { errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("discriminator") }); return; }
|
|
166
|
+
var tag = schema.discriminator;
|
|
167
|
+
if (!Object.prototype.hasOwnProperty.call(inst, tag)) { errors.push({ instancePath: ip.slice(), schemaPath: sp.concat("discriminator") }); return; }
|
|
168
|
+
if (typeof inst[tag] !== "string") { errors.push({ instancePath: ip.concat(tag), schemaPath: sp.concat("discriminator") }); return; }
|
|
169
|
+
if (!Object.prototype.hasOwnProperty.call(schema.mapping, inst[tag])) { errors.push({ instancePath: ip.concat(tag), schemaPath: sp.concat("mapping") }); return; }
|
|
170
|
+
_val(schema.mapping[inst[tag]], inst, ip, sp.concat("mapping", inst[tag]), root, depth + 1, errors, tag);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// empty form: accepts anything
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @primitive b.jtd.validate
|
|
178
|
+
* @signature b.jtd.validate(schema, instance)
|
|
179
|
+
* @since 0.12.62
|
|
180
|
+
* @status stable
|
|
181
|
+
* @compliance soc2
|
|
182
|
+
* @related b.safeSchema, b.jsonPointer.get
|
|
183
|
+
*
|
|
184
|
+
* Validate a JSON value against a JSON Type Definition schema (RFC 8927)
|
|
185
|
+
* and return the array of validation errors — each a
|
|
186
|
+
* <code>{ instancePath, schemaPath }</code> pair of token arrays naming
|
|
187
|
+
* the offending value and the schema rule it broke. An empty array means
|
|
188
|
+
* the instance is valid. All eight schema forms are supported. The schema
|
|
189
|
+
* itself is checked for well-formedness first; a malformed schema throws
|
|
190
|
+
* <code>jtd/bad-schema</code> rather than silently mis-validating.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* b.jtd.validate({ properties: { id: { type: "uint32" } } }, { id: -1 });
|
|
194
|
+
* // -> [ { instancePath: ["id"], schemaPath: ["properties", "id", "type"] } ]
|
|
195
|
+
*/
|
|
196
|
+
function validate(schema, instance) {
|
|
197
|
+
_checkSchema(schema, schema, true);
|
|
198
|
+
var errors = [];
|
|
199
|
+
_val(schema, instance, [], [], schema, 0, errors, undefined);
|
|
200
|
+
return errors;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @primitive b.jtd.isValid
|
|
205
|
+
* @signature b.jtd.isValid(schema, instance)
|
|
206
|
+
* @since 0.12.62
|
|
207
|
+
* @status stable
|
|
208
|
+
* @related b.jtd.validate
|
|
209
|
+
*
|
|
210
|
+
* Convenience boolean form of <code>validate</code> — <code>true</code>
|
|
211
|
+
* when the instance conforms to the JTD schema (no errors). Throws
|
|
212
|
+
* <code>jtd/bad-schema</code> on a malformed schema.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* b.jtd.isValid({ type: "string" }, "hello"); // -> true
|
|
216
|
+
*/
|
|
217
|
+
function isValid(schema, instance) { return validate(schema, instance).length === 0; }
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
validate: validate,
|
|
221
|
+
isValid: isValid,
|
|
222
|
+
JtdError: JtdError,
|
|
223
|
+
};
|
package/lib/rfc3339.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* rfc3339 — strict RFC 3339 date-time validation, shared by the primitives
|
|
4
|
+
* whose specs require the full "internet date/time" form (a mandatory
|
|
5
|
+
* "T"/"t" separator and a mandatory "Z" or numeric UTC offset). b.jtd's
|
|
6
|
+
* `timestamp` type and b.cloudevents' `time` attribute both point at
|
|
7
|
+
* RFC 3339, so the field-range + leap-year + offset-range checks live here
|
|
8
|
+
* once instead of drifting between them.
|
|
9
|
+
*
|
|
10
|
+
* This is intentionally NOT the lenient validator b.guardTime ships: that
|
|
11
|
+
* one accepts a space separator and an absent offset by design (a content-
|
|
12
|
+
* safety guard tuned per profile), whereas these consumers must reject
|
|
13
|
+
* anything the spec disallows.
|
|
14
|
+
*
|
|
15
|
+
* var rfc3339 = require("./rfc3339");
|
|
16
|
+
* rfc3339.isValidDateTime("2018-04-05T17:31:00Z"); // → true
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// "T" separator required; offset ("Z"/"z" or ±HH:MM) required.
|
|
20
|
+
var RFC3339_RE = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
21
|
+
|
|
22
|
+
function isValidDateTime(s) {
|
|
23
|
+
if (typeof s !== "string") return false;
|
|
24
|
+
var m = RFC3339_RE.exec(s);
|
|
25
|
+
if (!m) return false;
|
|
26
|
+
var mo = +m[2], d = +m[3], h = +m[4], mi = +m[5], se = +m[6];
|
|
27
|
+
if (mo < 1 || mo > 12 || d < 1 || d > 31 || h > 23 || mi > 59 || se > 60) return false; // allow:raw-time-literal — RFC 3339 field ranges (60 = leap second)
|
|
28
|
+
var days = [31, ((+m[1] % 4 === 0 && +m[1] % 100 !== 0) || +m[1] % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; // allow:raw-time-literal — days per month (Gregorian)
|
|
29
|
+
if (d > days[mo - 1]) return false;
|
|
30
|
+
var tz = m[8];
|
|
31
|
+
if (tz !== "Z" && tz !== "z") {
|
|
32
|
+
if (+tz.slice(1, 3) > 23 || +tz.slice(4, 6) > 59) return false; // allow:raw-time-literal — RFC 3339 offset hour/minute ranges
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { isValidDateTime: isValidDateTime, RFC3339_RE: RFC3339_RE };
|
package/lib/safe-buffer.js
CHANGED
|
@@ -372,6 +372,13 @@ var HEX_RE = /^[0-9a-fA-F]+$/;
|
|
|
372
372
|
// is length-agnostic — callers cap length per protocol contract.
|
|
373
373
|
var BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
374
374
|
|
|
375
|
+
// BASE64_RE matches standard base64 (RFC 4648 §4) with the `+` / `/`
|
|
376
|
+
// alphabet and canonical 0-2 chars of `=` padding (empty string allowed).
|
|
377
|
+
// Shared by callers that validate padded base64 fields (backup manifest
|
|
378
|
+
// digests, CloudEvents data_base64) so the alphabet check isn't reinvented.
|
|
379
|
+
// Length-agnostic — callers cap length per their own contract / maxBytes.
|
|
380
|
+
var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
381
|
+
|
|
375
382
|
// Fixed-length hex predicates used by trace-context primitives (W3C
|
|
376
383
|
// trace-id is 16 bytes = 32 hex chars; span-id / parent-id is 8
|
|
377
384
|
// bytes = 16 hex chars). Extracted to keep callers length-bounded
|
|
@@ -552,6 +559,7 @@ module.exports = {
|
|
|
552
559
|
stripTrailingHspace: stripTrailingHspace,
|
|
553
560
|
HEX_RE: HEX_RE,
|
|
554
561
|
BASE64URL_RE: BASE64URL_RE,
|
|
562
|
+
BASE64_RE: BASE64_RE,
|
|
555
563
|
IPV6_HEXTET_RE: IPV6_HEXTET_RE,
|
|
556
564
|
TRACE_ID_HEX_RE: TRACE_ID_HEX_RE,
|
|
557
565
|
SPAN_ID_HEX_RE: SPAN_ID_HEX_RE,
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:d1709f88-d452-4b6c-9796-971cb4dc9a1a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-26T06:02:00.412Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.12.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.63",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.63",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.12.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.12.63",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.12.
|
|
57
|
+
"ref": "@blamejs/core@0.12.63",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|