@blamejs/core 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/index.js +2 -0
- package/lib/auth/jwt.js +13 -0
- package/lib/break-glass.js +16 -5
- package/lib/cookies.js +11 -1
- package/lib/error-page.js +15 -6
- package/lib/middleware/cors.js +8 -3
- package/lib/middleware/rate-limit.js +8 -0
- package/lib/pagination.js +16 -0
- package/lib/queue.js +7 -2
- package/lib/uuid.js +105 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.5.x
|
|
10
10
|
|
|
11
|
+
- **0.5.7** (2026-04-30) — defensive validation + queue closure capture + audit context
|
|
12
|
+
- **0.5.6** (2026-04-30) — break-glass: trustProxy honored, cache require hoisted
|
|
11
13
|
- **0.5.5** (2026-04-30) — strict default CSP + IPv6 special-range expansion
|
|
12
14
|
- **0.5.4** (2026-04-30) — close SSRF DNS-rebinding window with pinned outbound DNS
|
|
13
15
|
- **0.5.3** (2026-04-30) — security cleanup: trustProxy primitive + Vary merge + HSTS gate
|
package/index.js
CHANGED
|
@@ -106,6 +106,7 @@ var forms = require("./lib/forms");
|
|
|
106
106
|
var app = require("./lib/app");
|
|
107
107
|
var jobs = require("./lib/jobs");
|
|
108
108
|
var breakGlass = require("./lib/break-glass");
|
|
109
|
+
var uuid = require("./lib/uuid");
|
|
109
110
|
var mail = require("./lib/mail");
|
|
110
111
|
var mailBounce = require("./lib/mail-bounce");
|
|
111
112
|
var websocketChannels = require("./lib/websocket-channels");
|
|
@@ -208,6 +209,7 @@ module.exports = {
|
|
|
208
209
|
createApp: app.createApp,
|
|
209
210
|
jobs: jobs,
|
|
210
211
|
breakGlass: breakGlass,
|
|
212
|
+
uuid: uuid,
|
|
211
213
|
mail: mail,
|
|
212
214
|
mailBounce: mailBounce,
|
|
213
215
|
websocketChannels: websocketChannels,
|
package/lib/auth/jwt.js
CHANGED
|
@@ -243,6 +243,19 @@ async function verify(token, opts) {
|
|
|
243
243
|
|
|
244
244
|
// Time-based claim validation
|
|
245
245
|
var nowSec = Math.floor((opts.now || Date.now()) / 1000);
|
|
246
|
+
if (opts.clockToleranceSec !== undefined && opts.clockToleranceSec !== null) {
|
|
247
|
+
if (typeof opts.clockToleranceSec !== "number" ||
|
|
248
|
+
!isFinite(opts.clockToleranceSec) ||
|
|
249
|
+
opts.clockToleranceSec < 0) {
|
|
250
|
+
// Tier-A: a negative tolerance over-tightens the window (legit
|
|
251
|
+
// tokens get rejected as expired or not-yet-valid). A non-finite
|
|
252
|
+
// value would NaN-poison the comparison. Catch both at config
|
|
253
|
+
// time rather than producing surprise rejections in production.
|
|
254
|
+
throw new AuthError("auth-jwt/bad-clock-tolerance",
|
|
255
|
+
"verify: clockToleranceSec must be a non-negative finite number, got " +
|
|
256
|
+
JSON.stringify(opts.clockToleranceSec));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
246
259
|
var tol = typeof opts.clockToleranceSec === "number" ? opts.clockToleranceSec : 0;
|
|
247
260
|
var p = decoded.payload;
|
|
248
261
|
if (typeof p.exp === "number" && p.exp + tol < nowSec) {
|
package/lib/break-glass.js
CHANGED
|
@@ -776,12 +776,23 @@ function _reasonForAudit(reason, mode) {
|
|
|
776
776
|
|
|
777
777
|
// ---- Use a grant ----
|
|
778
778
|
|
|
779
|
-
async function unsealRow(grantHandle, table, rowId) {
|
|
779
|
+
async function unsealRow(grantHandle, table, rowId, opts) {
|
|
780
780
|
_requireInit();
|
|
781
781
|
if (!grantHandle || typeof grantHandle !== "object" || typeof grantHandle.id !== "string") {
|
|
782
782
|
throw new BreakGlassError("breakglass/bad-grant-opts",
|
|
783
783
|
"unsealRow: grant handle is required (returned from b.breakGlass.grant())");
|
|
784
784
|
}
|
|
785
|
+
// Optional opts.req lets the caller thread the originating request
|
|
786
|
+
// into per-row audit emits so the 5 W's (ip / userAgent / sessionId /
|
|
787
|
+
// requestId / method / route) populate alongside the grant's actor
|
|
788
|
+
// userId. Backward-compatible — calls without opts continue to work
|
|
789
|
+
// and simply audit with userId-only actor.
|
|
790
|
+
opts = opts || {};
|
|
791
|
+
function _actorFor(grantRow) {
|
|
792
|
+
return requestHelpers.extractActorContext(opts.req, {
|
|
793
|
+
userId: grantRow.issuedToActorId,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
785
796
|
if (typeof table !== "string" || table.length === 0) {
|
|
786
797
|
throw new BreakGlassError("breakglass/bad-grant-opts",
|
|
787
798
|
"unsealRow: table must be a non-empty string");
|
|
@@ -806,7 +817,7 @@ async function unsealRow(grantHandle, table, rowId) {
|
|
|
806
817
|
audit.safeEmit({
|
|
807
818
|
action: "breakglass.unsealrow",
|
|
808
819
|
outcome: "denied",
|
|
809
|
-
actor:
|
|
820
|
+
actor: _actorFor(grantRow),
|
|
810
821
|
reason: "grant-table-mismatch",
|
|
811
822
|
metadata: { grantId: grantRow._id, expectedTable: grantRow.scopeTable, gotTable: table, rowId: String(rowId) },
|
|
812
823
|
});
|
|
@@ -827,7 +838,7 @@ async function unsealRow(grantHandle, table, rowId) {
|
|
|
827
838
|
audit.safeEmit({
|
|
828
839
|
action: "breakglass.grant.expired",
|
|
829
840
|
outcome: "success",
|
|
830
|
-
actor:
|
|
841
|
+
actor: _actorFor(grantRow),
|
|
831
842
|
metadata: { grantId: grantRow._id, table: table, rowsConsumed: Number(grantRow.rowsConsumed) },
|
|
832
843
|
});
|
|
833
844
|
throw new BreakGlassError("breakglass/grant-expired",
|
|
@@ -840,7 +851,7 @@ async function unsealRow(grantHandle, table, rowId) {
|
|
|
840
851
|
audit.safeEmit({
|
|
841
852
|
action: "breakglass.grant.exhausted",
|
|
842
853
|
outcome: "success",
|
|
843
|
-
actor:
|
|
854
|
+
actor: _actorFor(grantRow),
|
|
844
855
|
metadata: { grantId: grantRow._id, table: table, rowsConsumed: Number(grantRow.rowsConsumed) },
|
|
845
856
|
});
|
|
846
857
|
throw new BreakGlassError("breakglass/grant-exhausted",
|
|
@@ -932,7 +943,7 @@ async function unsealRow(grantHandle, table, rowId) {
|
|
|
932
943
|
audit.safeEmit({
|
|
933
944
|
action: "breakglass.unsealrow",
|
|
934
945
|
outcome: "success",
|
|
935
|
-
actor:
|
|
946
|
+
actor: _actorFor(grantRow),
|
|
936
947
|
reason: reasonForAudit.cleartext,
|
|
937
948
|
metadata: {
|
|
938
949
|
grantId: grantRow._id,
|
package/lib/cookies.js
CHANGED
|
@@ -151,7 +151,17 @@ function serialize(name, value, attrs) {
|
|
|
151
151
|
parts.push("Expires=" + d.toUTCString());
|
|
152
152
|
}
|
|
153
153
|
if (attrs.domain) {
|
|
154
|
-
|
|
154
|
+
var dom = _scrubAttr(String(attrs.domain));
|
|
155
|
+
// RFC 6265 §4.1.2.3 + §5.1.3: Domain attribute is a host name.
|
|
156
|
+
// A leading "." is tolerated by browsers (legacy form). Anything
|
|
157
|
+
// else (URLs, paths, spaces, scheme prefixes) makes browsers
|
|
158
|
+
// either ignore the cookie or apply it inconsistently.
|
|
159
|
+
if (!/^\.?[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i.test(dom)) {
|
|
160
|
+
throw new CookieError("cookies/invalid-attr",
|
|
161
|
+
"cookie attr domain must be a valid host name " +
|
|
162
|
+
"(letters/digits/dots/hyphens, optional leading dot), got " + JSON.stringify(dom));
|
|
163
|
+
}
|
|
164
|
+
parts.push("Domain=" + dom);
|
|
155
165
|
}
|
|
156
166
|
if (attrs.path !== undefined && attrs.path !== null) {
|
|
157
167
|
parts.push("Path=" + _scrubAttr(String(attrs.path)));
|
package/lib/error-page.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
var lazyRequire = require("./lazy-require");
|
|
42
|
+
var requestHelpers = require("./request-helpers");
|
|
42
43
|
var template = require("./template");
|
|
43
44
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
44
45
|
|
|
@@ -280,6 +281,10 @@ function create(opts) {
|
|
|
280
281
|
var auditAction = typeof opts.auditAction === "string" && opts.auditAction.length > 0
|
|
281
282
|
? opts.auditAction
|
|
282
283
|
: "request.error";
|
|
284
|
+
// Same proxy-trust boundary as the rest of the framework — without
|
|
285
|
+
// trustProxy, X-Forwarded-For is ignored as attacker-forgeable.
|
|
286
|
+
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
287
|
+
? opts.trustProxy : false;
|
|
283
288
|
// defaultFormat:
|
|
284
289
|
// "auto" (default) — negotiate via Accept header
|
|
285
290
|
// "json" — always JSON (API-style middleware default)
|
|
@@ -355,12 +360,16 @@ function create(opts) {
|
|
|
355
360
|
audit().emit({
|
|
356
361
|
action: auditAction,
|
|
357
362
|
outcome: info.status >= 500 ? "failure" : "denied",
|
|
358
|
-
actor: {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
363
|
+
actor: requestHelpers.extractActorContext(req, {
|
|
364
|
+
// Honor the framework's trustProxy boundary — extractActorContext
|
|
365
|
+
// reads from req.socket.remoteAddress, but we want X-Forwarded-For
|
|
366
|
+
// resolution gated by the operator's opt.
|
|
367
|
+
ip: requestHelpers.clientIp(req, { trustProxy: trustProxy }),
|
|
368
|
+
// sessionId from session.sid for back-compat with the
|
|
369
|
+
// pre-extractActorContext shape (extractActorContext also
|
|
370
|
+
// checks session.id; passing both via override is safe).
|
|
371
|
+
sessionId: req && req.session && (req.session.sid || req.session.id),
|
|
372
|
+
}),
|
|
364
373
|
metadata: {
|
|
365
374
|
status: info.status,
|
|
366
375
|
code: info.code,
|
package/lib/middleware/cors.js
CHANGED
|
@@ -174,7 +174,13 @@ function create(opts) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
var methods = (opts.methods || ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).join(", ");
|
|
177
|
-
var
|
|
177
|
+
var headersList = (opts.headers || ["Content-Type", "Authorization", "X-Request-Id"]).slice();
|
|
178
|
+
var headers = headersList.join(", ");
|
|
179
|
+
// Pre-compute the lowercase allowlist once at create() time so the
|
|
180
|
+
// preflight path doesn't re-derive it per-request from the joined
|
|
181
|
+
// string. Avoids coupling allow-list validation to the wire-format
|
|
182
|
+
// serialization.
|
|
183
|
+
var allowedHeadersSet = headersList.map(function (h) { return String(h).trim().toLowerCase(); });
|
|
178
184
|
var exposeHeaders = (opts.exposeHeaders || ["X-Request-Id"]).join(", ");
|
|
179
185
|
var credentials = !!opts.credentials;
|
|
180
186
|
var maxAge = String(opts.maxAgeSeconds || 600);
|
|
@@ -230,10 +236,9 @@ function create(opts) {
|
|
|
230
236
|
if (refuseUnknown) {
|
|
231
237
|
var requestedHdrs = req.headers["access-control-request-headers"];
|
|
232
238
|
if (requestedHdrs) {
|
|
233
|
-
var allowedSet = headers.toLowerCase().split(",").map(function (s) { return s.trim(); });
|
|
234
239
|
var asked = String(requestedHdrs).toLowerCase().split(",").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
235
240
|
for (var ah = 0; ah < asked.length; ah++) {
|
|
236
|
-
if (
|
|
241
|
+
if (allowedHeadersSet.indexOf(asked[ah]) === -1) {
|
|
237
242
|
if (typeof res.writeHead === "function") {
|
|
238
243
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
239
244
|
res.end("CORS: requested header '" + asked[ah] + "' not in allow-list");
|
|
@@ -240,6 +240,14 @@ function create(opts) {
|
|
|
240
240
|
var bodyOnLimit = opts.bodyOnLimit !== undefined ? opts.bodyOnLimit : "Too Many Requests";
|
|
241
241
|
var emitHeaders = opts.header !== false;
|
|
242
242
|
var skipPaths = opts.skipPaths || [];
|
|
243
|
+
// Tier-A: each entry must be a string prefix or a RegExp. Anything
|
|
244
|
+
// else would crash _shouldSkip with TypeError on the first request.
|
|
245
|
+
for (var sp = 0; sp < skipPaths.length; sp++) {
|
|
246
|
+
if (typeof skipPaths[sp] !== "string" && !(skipPaths[sp] instanceof RegExp)) {
|
|
247
|
+
throw new Error("middleware.rateLimit: skipPaths[" + sp +
|
|
248
|
+
"] must be a string prefix or RegExp, got " + typeof skipPaths[sp]);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
243
251
|
var scope = opts.scope || "global";
|
|
244
252
|
|
|
245
253
|
var backend = _resolveBackend(opts);
|
package/lib/pagination.js
CHANGED
|
@@ -214,6 +214,15 @@ async function cursor(query, opts) {
|
|
|
214
214
|
}
|
|
215
215
|
var limit = _resolveLimit(opts);
|
|
216
216
|
var orderBy = typeof opts.orderBy === "string" && opts.orderBy.length > 0 ? opts.orderBy : "_id";
|
|
217
|
+
// Tier-A on orderBy — the value is interpolated into a raw SQL fragment
|
|
218
|
+
// for the keyset where-clause. Restrict to identifier-safe characters
|
|
219
|
+
// so a careless caller piping `req.query.orderBy` through doesn't
|
|
220
|
+
// create an SQL-injection vector.
|
|
221
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(orderBy)) {
|
|
222
|
+
throw new PaginationError("pagination/bad-orderby",
|
|
223
|
+
"cursor: orderBy must match /^[A-Za-z_][A-Za-z0-9_]*$/ (identifier-safe), got " +
|
|
224
|
+
JSON.stringify(orderBy));
|
|
225
|
+
}
|
|
217
226
|
var direction = (opts.direction === "desc") ? "desc" : "asc";
|
|
218
227
|
|
|
219
228
|
// Decode incoming cursor (if any) and override direction from cursor.
|
|
@@ -332,6 +341,13 @@ async function offset(query, opts) {
|
|
|
332
341
|
var page = parseInt(opts.page, 10);
|
|
333
342
|
if (isNaN(page) || page < 1) page = 1;
|
|
334
343
|
var orderBy = typeof opts.orderBy === "string" && opts.orderBy.length > 0 ? opts.orderBy : "_id";
|
|
344
|
+
// Same Tier-A on offset() as cursor() — orderBy passes through to the
|
|
345
|
+
// db Query; identifier-safe-only to prevent SQL injection.
|
|
346
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(orderBy)) {
|
|
347
|
+
throw new PaginationError("pagination/bad-orderby",
|
|
348
|
+
"offset: orderBy must match /^[A-Za-z_][A-Za-z0-9_]*$/ (identifier-safe), got " +
|
|
349
|
+
JSON.stringify(orderBy));
|
|
350
|
+
}
|
|
335
351
|
var direction = (opts.direction === "desc") ? "desc" : "asc";
|
|
336
352
|
|
|
337
353
|
// Count gives total — required for totalPages calculation. Cheap on
|
package/lib/queue.js
CHANGED
|
@@ -70,7 +70,12 @@ function init(opts) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
backends = {};
|
|
73
|
-
|
|
73
|
+
// IIFE per-iteration so each backend's wrappers close over its own
|
|
74
|
+
// raw / breaker / cfg. With `var` (function-scoped) those bindings
|
|
75
|
+
// would otherwise be shared across iterations and every wrapper
|
|
76
|
+
// would end up using the LAST backend's breaker + retry config —
|
|
77
|
+
// multi-backend isolation broken without a single error surfacing.
|
|
78
|
+
Object.keys(opts.backends).forEach(function (name) {
|
|
74
79
|
var cfg = opts.backends[name];
|
|
75
80
|
var proto = dispatcher.resolve(cfg.protocol);
|
|
76
81
|
var raw = proto.create(cfg);
|
|
@@ -114,7 +119,7 @@ function init(opts) {
|
|
|
114
119
|
dlqRetry: raw.dlqRetry ? wrapWithRetry(raw.dlqRetry) : null,
|
|
115
120
|
dlqSize: raw.dlqSize ? wrapWithRetry(raw.dlqSize) : null,
|
|
116
121
|
};
|
|
117
|
-
}
|
|
122
|
+
});
|
|
118
123
|
|
|
119
124
|
defaultBackend = opts.defaultBackend || Object.keys(backends)[0];
|
|
120
125
|
|
package/lib/uuid.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* uuid — RFC 4122 v4 (random) + RFC 9562 v7 (time-ordered).
|
|
4
|
+
*
|
|
5
|
+
* Two flavors:
|
|
6
|
+
*
|
|
7
|
+
* b.uuid.v4() — fully random 128-bit UUID. Standard, portable,
|
|
8
|
+
* the default choice when ordering doesn't matter.
|
|
9
|
+
*
|
|
10
|
+
* b.uuid.v7() — Unix-millisecond timestamp prefix + 74 random
|
|
11
|
+
* bits. Time-ordered (sorts by creation time even
|
|
12
|
+
* lexicographically), ideal as a database PK
|
|
13
|
+
* because B-tree inserts stay near the right edge
|
|
14
|
+
* — no random scattering across the index.
|
|
15
|
+
*
|
|
16
|
+
* b.uuid.parse(str) — { ok, version, bytes }. Validates the canonical
|
|
17
|
+
* 8-4-4-4-12 hex form and the version + variant
|
|
18
|
+
* bits. Returns ok:false (no throw) on bad input.
|
|
19
|
+
*
|
|
20
|
+
* b.uuid.isValid(str) — boolean shorthand. No version/variant check
|
|
21
|
+
* beyond shape — operators who care use parse().
|
|
22
|
+
*
|
|
23
|
+
* All entropy comes from `b.crypto.generateBytes`, which routes through
|
|
24
|
+
* `node:crypto.randomBytes` — same source as `crypto.randomUUID()`.
|
|
25
|
+
*
|
|
26
|
+
* Why ship v7 ourselves? Native `crypto.randomUUID()` only emits v4.
|
|
27
|
+
* v7 is the modern recommendation for any UUID landing in a sortable
|
|
28
|
+
* column (jobs queue, audit chain extensions, anything where insertion
|
|
29
|
+
* order matters for index locality).
|
|
30
|
+
*/
|
|
31
|
+
var { generateBytes } = require("./crypto");
|
|
32
|
+
|
|
33
|
+
// Canonical UUID layout: 8-4-4-4-12 hex digits, version nibble at byte
|
|
34
|
+
// 6 high-nibble, variant bits at byte 8 high two bits (must be 10).
|
|
35
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-7][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
36
|
+
// Loose form for `isValid` shape-only check — accepts any version 1-8 and
|
|
37
|
+
// any variant top-2-bit value. Operators wanting strict version+variant
|
|
38
|
+
// gating use `parse()`.
|
|
39
|
+
var UUID_LOOSE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
40
|
+
|
|
41
|
+
function _bytesToString(bytes) {
|
|
42
|
+
var hex = bytes.toString("hex");
|
|
43
|
+
return hex.slice(0, 8) + "-" +
|
|
44
|
+
hex.slice(8, 12) + "-" +
|
|
45
|
+
hex.slice(12, 16) + "-" +
|
|
46
|
+
hex.slice(16, 20) + "-" +
|
|
47
|
+
hex.slice(20, 32);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function v4() {
|
|
51
|
+
var b = generateBytes(16);
|
|
52
|
+
// version = 4 (0100): high nibble of byte 6
|
|
53
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
54
|
+
// variant = RFC 4122 (10xx): top two bits of byte 8
|
|
55
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
56
|
+
return _bytesToString(b);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function v7(opts) {
|
|
60
|
+
// RFC 9562 §5.7 layout:
|
|
61
|
+
// bytes 0-5 : 48-bit big-endian Unix timestamp in milliseconds
|
|
62
|
+
// bytes 6-7 : version nibble (7) + 12 bits random_a
|
|
63
|
+
// bytes 8-15 : variant bits + 62 bits random_b
|
|
64
|
+
var ms = (opts && typeof opts.now === "number") ? opts.now : Date.now();
|
|
65
|
+
var b = generateBytes(16);
|
|
66
|
+
// 48-bit ms timestamp (big-endian) into bytes 0-5
|
|
67
|
+
// ms can exceed 2^32 (we're in 2026, ms is ~1.78e12), so use Math + bit ops carefully
|
|
68
|
+
var msHi = Math.floor(ms / 0x100000000); // top 16 bits live in low 16 of msHi
|
|
69
|
+
var msLo = ms >>> 0; // bottom 32 bits unsigned
|
|
70
|
+
b[0] = (msHi >> 8) & 0xff;
|
|
71
|
+
b[1] = msHi & 0xff;
|
|
72
|
+
b[2] = (msLo >>> 24) & 0xff;
|
|
73
|
+
b[3] = (msLo >>> 16) & 0xff;
|
|
74
|
+
b[4] = (msLo >>> 8) & 0xff;
|
|
75
|
+
b[5] = msLo & 0xff;
|
|
76
|
+
// version = 7 (0111) in high nibble of byte 6, random_a in low nibble + byte 7
|
|
77
|
+
b[6] = (b[6] & 0x0f) | 0x70;
|
|
78
|
+
// variant = RFC 4122 (10xx) in top two bits of byte 8
|
|
79
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
80
|
+
return _bytesToString(b);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parse(str) {
|
|
84
|
+
if (typeof str !== "string") return { ok: false, reason: "not-a-string" };
|
|
85
|
+
if (!UUID_RE.test(str)) return { ok: false, reason: "malformed" };
|
|
86
|
+
var hex = str.replace(/-/g, "");
|
|
87
|
+
var bytes = Buffer.from(hex, "hex");
|
|
88
|
+
// Version is the high nibble of byte 6.
|
|
89
|
+
var version = (bytes[6] >> 4) & 0x0f;
|
|
90
|
+
// Variant: top two bits of byte 8 must be 10 for RFC 4122 / 9562.
|
|
91
|
+
var variant = (bytes[8] >> 6) & 0x03;
|
|
92
|
+
if (variant !== 0b10) return { ok: false, reason: "bad-variant" };
|
|
93
|
+
return { ok: true, version: version, bytes: bytes };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isValid(str) {
|
|
97
|
+
return typeof str === "string" && UUID_LOOSE_RE.test(str);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
v4: v4,
|
|
102
|
+
v7: v7,
|
|
103
|
+
parse: parse,
|
|
104
|
+
isValid: isValid,
|
|
105
|
+
};
|