@blamejs/core 0.7.4 → 0.7.18
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 +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/api-snapshot.js
CHANGED
|
@@ -1,338 +1,338 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* api-snapshot — public API surface walker + breaking-change detector.
|
|
4
|
-
*
|
|
5
|
-
* The framework's LTS-contract enforcement at the type level. Walks
|
|
6
|
-
* the framework's module exports recursively, records every member's
|
|
7
|
-
* type, and compares two snapshots to find:
|
|
8
|
-
*
|
|
9
|
-
* - removed a member present in the old snapshot but not the new
|
|
10
|
-
* (BREAKING — fails CI)
|
|
11
|
-
* - typeChanged a member's category flipped (function → object, etc.)
|
|
12
|
-
* (BREAKING — fails CI)
|
|
13
|
-
* - added a new member that wasn't in the old snapshot
|
|
14
|
-
* (ADDITIVE — does not fail; signals the snapshot is
|
|
15
|
-
* out-of-date and the operator should rerun capture)
|
|
16
|
-
*
|
|
17
|
-
* var snap = b.apiSnapshot.capture(require("@blamejs/core"));
|
|
18
|
-
* // { version, frameworkVersion, createdAt,
|
|
19
|
-
* // exports: { ... nested tree ... } }
|
|
20
|
-
*
|
|
21
|
-
* b.apiSnapshot.write(snap, "./api-snapshot.json");
|
|
22
|
-
* var loaded = b.apiSnapshot.read("./api-snapshot.json");
|
|
23
|
-
*
|
|
24
|
-
* var diff = b.apiSnapshot.compare(loaded, snap);
|
|
25
|
-
* // { breaking: [{ path, kind, was?, is? }],
|
|
26
|
-
* // typeChanged: [{ path, was, is }],
|
|
27
|
-
* // additive: [{ path, type }] }
|
|
28
|
-
*
|
|
29
|
-
* if (diff.breaking.length > 0 || diff.typeChanged.length > 0) {
|
|
30
|
-
* console.error(b.apiSnapshot.formatDiff(diff));
|
|
31
|
-
* process.exit(1);
|
|
32
|
-
* }
|
|
33
|
-
*
|
|
34
|
-
* Walker rules:
|
|
35
|
-
* - Functions record as { type: 'function', arity: fn.length }.
|
|
36
|
-
* Class constructors are still 'function' — recursive scope walks
|
|
37
|
-
* prototype only when the operator explicitly opts in via
|
|
38
|
-
* opts.includeClassPrototypes.
|
|
39
|
-
* - Plain objects recurse into their own enumerable string keys.
|
|
40
|
-
* - Primitives (string, number, boolean, null, undefined) record as
|
|
41
|
-
* { type: 'primitive', valueType: typeof v }. Specific values are
|
|
42
|
-
* NOT captured — only the type — so a version-string change in
|
|
43
|
-
* constants doesn't fail CI.
|
|
44
|
-
* - Members whose key starts with '_' are skipped (test seams,
|
|
45
|
-
* internal helpers).
|
|
46
|
-
* - Cycles are detected and short-circuit as { type: 'cycle' }.
|
|
47
|
-
* - Non-plain objects (Map, Set, Buffer, Date, RegExp, Error, etc.)
|
|
48
|
-
* are recorded as { type: 'instance', constructor: name } without
|
|
49
|
-
* recursion — they're terminal nodes.
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
var fs = require("fs");
|
|
53
|
-
var nb = require("./numeric-bounds");
|
|
54
|
-
var safeJson = require("./safe-json");
|
|
55
|
-
var { FrameworkError } = require("./framework-error");
|
|
56
|
-
|
|
57
|
-
var DEFAULT_MAX_DEPTH = 0x08;
|
|
58
|
-
|
|
59
|
-
class ApiSnapshotError extends FrameworkError {
|
|
60
|
-
constructor(code, message) {
|
|
61
|
-
super(message, code);
|
|
62
|
-
this.name = "ApiSnapshotError";
|
|
63
|
-
this.permanent = true;
|
|
64
|
-
this.isApiSnapshotError = true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
var SNAPSHOT_FORMAT_VERSION = 1;
|
|
69
|
-
|
|
70
|
-
function _isPlainObject(v) {
|
|
71
|
-
if (v === null || typeof v !== "object") return false;
|
|
72
|
-
var proto = Object.getPrototypeOf(v);
|
|
73
|
-
return proto === Object.prototype || proto === null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function _walkNode(value, depth, maxDepth, seen, skipUnderscore) {
|
|
77
|
-
if (value === null) return { type: "primitive", valueType: "null" };
|
|
78
|
-
var t = typeof value;
|
|
79
|
-
if (t === "undefined") return { type: "primitive", valueType: "undefined" };
|
|
80
|
-
if (t === "string" || t === "number" || t === "boolean" || t === "bigint" || t === "symbol") {
|
|
81
|
-
return { type: "primitive", valueType: t };
|
|
82
|
-
}
|
|
83
|
-
if (t === "function") {
|
|
84
|
-
return { type: "function", arity: value.length };
|
|
85
|
-
}
|
|
86
|
-
// Object — guard cycles + depth
|
|
87
|
-
if (seen.has(value)) return { type: "cycle" };
|
|
88
|
-
if (depth >= maxDepth) return { type: "deep", note: "max depth" };
|
|
89
|
-
|
|
90
|
-
if (!_isPlainObject(value)) {
|
|
91
|
-
var name = value && value.constructor && value.constructor.name
|
|
92
|
-
? value.constructor.name
|
|
93
|
-
: "Object";
|
|
94
|
-
// Use 'ctorName' instead of 'constructor' — json-safe.parse strips
|
|
95
|
-
// 'constructor' as a prototype-pollution defense, which would
|
|
96
|
-
// round-trip-mangle every instance node otherwise.
|
|
97
|
-
return { type: "instance", ctorName: name };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Plain object — recurse
|
|
101
|
-
seen.add(value);
|
|
102
|
-
var members = {};
|
|
103
|
-
var keys = Object.keys(value);
|
|
104
|
-
// Stable sort for canonical snapshot bytes
|
|
105
|
-
keys.sort();
|
|
106
|
-
for (var i = 0; i < keys.length; i++) {
|
|
107
|
-
var k = keys[i];
|
|
108
|
-
if (skipUnderscore && k.charAt(0) === "_") continue;
|
|
109
|
-
members[k] = _walkNode(value[k], depth + 1, maxDepth, seen, skipUnderscore);
|
|
110
|
-
}
|
|
111
|
-
seen.delete(value);
|
|
112
|
-
return { type: "object", members: members };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function capture(target, opts) {
|
|
116
|
-
opts = opts || {};
|
|
117
|
-
if (!target || typeof target !== "object") {
|
|
118
|
-
throw new ApiSnapshotError("api-snapshot/bad-target",
|
|
119
|
-
"capture: target must be a module's exports object");
|
|
120
|
-
}
|
|
121
|
-
var maxDepth = nb.isPositiveFiniteInt(opts.maxDepth) ? opts.maxDepth : DEFAULT_MAX_DEPTH;
|
|
122
|
-
var skipUnderscore = opts.skipUnderscore !== false;
|
|
123
|
-
var snapshot = _walkNode(target, 0, maxDepth, new Set(), skipUnderscore);
|
|
124
|
-
if (snapshot.type !== "object") {
|
|
125
|
-
throw new ApiSnapshotError("api-snapshot/bad-target",
|
|
126
|
-
"capture: top-level target must be a plain object (got '" + snapshot.type + "')");
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
version: SNAPSHOT_FORMAT_VERSION,
|
|
130
|
-
frameworkVersion: typeof opts.frameworkVersion === "string" && opts.frameworkVersion.length > 0
|
|
131
|
-
? opts.frameworkVersion
|
|
132
|
-
: (target.version || "0.0.0"),
|
|
133
|
-
createdAt: opts.createdAt || new Date().toISOString(),
|
|
134
|
-
exports: snapshot.members,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function write(snapshot, filePath) {
|
|
139
|
-
if (!snapshot || typeof snapshot !== "object") {
|
|
140
|
-
throw new ApiSnapshotError("api-snapshot/bad-snapshot",
|
|
141
|
-
"write: snapshot must be a snapshot object (returned by capture)");
|
|
142
|
-
}
|
|
143
|
-
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
144
|
-
throw new ApiSnapshotError("api-snapshot/bad-path",
|
|
145
|
-
"write: filePath is required");
|
|
146
|
-
}
|
|
147
|
-
// Stringify with stable key order via the explicit canonical form
|
|
148
|
-
var canonical = {
|
|
149
|
-
version: snapshot.version,
|
|
150
|
-
frameworkVersion: snapshot.frameworkVersion,
|
|
151
|
-
createdAt: snapshot.createdAt,
|
|
152
|
-
exports: snapshot.exports,
|
|
153
|
-
};
|
|
154
|
-
fs.writeFileSync(filePath, JSON.stringify(canonical, null, 2) + "\n", { mode: 0o644 });
|
|
155
|
-
return filePath;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function read(filePath) {
|
|
159
|
-
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
160
|
-
throw new ApiSnapshotError("api-snapshot/bad-path",
|
|
161
|
-
"read: filePath is required");
|
|
162
|
-
}
|
|
163
|
-
if (!fs.existsSync(filePath)) {
|
|
164
|
-
throw new ApiSnapshotError("api-snapshot/missing",
|
|
165
|
-
"read: snapshot file not found at " + filePath);
|
|
166
|
-
}
|
|
167
|
-
var raw;
|
|
168
|
-
try { raw = fs.readFileSync(filePath, "utf8"); }
|
|
169
|
-
catch (e) {
|
|
170
|
-
throw new ApiSnapshotError("api-snapshot/read-failed",
|
|
171
|
-
"read: cannot read " + filePath + ": " + ((e && e.message) || String(e)));
|
|
172
|
-
}
|
|
173
|
-
var parsed;
|
|
174
|
-
try { parsed = safeJson.parse(raw); }
|
|
175
|
-
catch (e) {
|
|
176
|
-
throw new ApiSnapshotError("api-snapshot/bad-json",
|
|
177
|
-
"read: not valid JSON: " + ((e && e.message) || String(e)));
|
|
178
|
-
}
|
|
179
|
-
if (!parsed || parsed.version !== SNAPSHOT_FORMAT_VERSION) {
|
|
180
|
-
throw new ApiSnapshotError("api-snapshot/bad-version",
|
|
181
|
-
"read: snapshot version is " + (parsed && parsed.version) +
|
|
182
|
-
", expected " + SNAPSHOT_FORMAT_VERSION);
|
|
183
|
-
}
|
|
184
|
-
if (!parsed.exports || typeof parsed.exports !== "object") {
|
|
185
|
-
throw new ApiSnapshotError("api-snapshot/bad-shape",
|
|
186
|
-
"read: snapshot is missing 'exports' object");
|
|
187
|
-
}
|
|
188
|
-
return parsed;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Walk both trees in parallel under a path. Append to breaking,
|
|
192
|
-
// additive, typeChanged.
|
|
193
|
-
function _walkCompare(oldNode, newNode, prefix, breaking, additive, typeChanged) {
|
|
194
|
-
// Both should describe the same node. If types differ at the node
|
|
195
|
-
// level, that's a breaking type change.
|
|
196
|
-
if (!oldNode || !newNode) return;
|
|
197
|
-
|
|
198
|
-
if (oldNode.type !== newNode.type) {
|
|
199
|
-
typeChanged.push({
|
|
200
|
-
path: prefix,
|
|
201
|
-
was: oldNode.type,
|
|
202
|
-
is: newNode.type,
|
|
203
|
-
});
|
|
204
|
-
breaking.push({ path: prefix, kind: "type-changed", was: oldNode.type, is: newNode.type });
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (oldNode.type === "object") {
|
|
209
|
-
var oldMembers = oldNode.members || {};
|
|
210
|
-
var newMembers = newNode.members || {};
|
|
211
|
-
var oldKeys = Object.keys(oldMembers);
|
|
212
|
-
var newKeys = Object.keys(newMembers);
|
|
213
|
-
|
|
214
|
-
// Removed: in old, not in new
|
|
215
|
-
for (var i = 0; i < oldKeys.length; i++) {
|
|
216
|
-
var ok = oldKeys[i];
|
|
217
|
-
var childPath = prefix ? (prefix + "." + ok) : ok;
|
|
218
|
-
if (!Object.prototype.hasOwnProperty.call(newMembers, ok)) {
|
|
219
|
-
breaking.push({ path: childPath, kind: "removed", was: oldMembers[ok].type });
|
|
220
|
-
} else {
|
|
221
|
-
_walkCompare(oldMembers[ok], newMembers[ok], childPath, breaking, additive, typeChanged);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
// Added: in new, not in old
|
|
225
|
-
for (var j = 0; j < newKeys.length; j++) {
|
|
226
|
-
var nk = newKeys[j];
|
|
227
|
-
if (!Object.prototype.hasOwnProperty.call(oldMembers, nk)) {
|
|
228
|
-
var addPath = prefix ? (prefix + "." + nk) : nk;
|
|
229
|
-
additive.push({ path: addPath, type: newMembers[nk].type });
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// For function nodes, arity DROPS are flagged (operator removed a
|
|
236
|
-
// required parameter). Arity INCREASES are not flagged (added
|
|
237
|
-
// optional param at the end is additive).
|
|
238
|
-
if (oldNode.type === "function") {
|
|
239
|
-
if (typeof oldNode.arity === "number" && typeof newNode.arity === "number" &&
|
|
240
|
-
newNode.arity < oldNode.arity) {
|
|
241
|
-
breaking.push({
|
|
242
|
-
path: prefix,
|
|
243
|
-
kind: "arity-decreased",
|
|
244
|
-
was: "function/" + oldNode.arity,
|
|
245
|
-
is: "function/" + newNode.arity,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// For instance nodes, a constructor-name change is breaking
|
|
252
|
-
if (oldNode.type === "instance") {
|
|
253
|
-
if (oldNode.ctorName !== newNode.ctorName) {
|
|
254
|
-
breaking.push({
|
|
255
|
-
path: prefix,
|
|
256
|
-
kind: "constructor-changed",
|
|
257
|
-
was: oldNode.ctorName,
|
|
258
|
-
is: newNode.ctorName,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// For primitive nodes, a valueType change is breaking
|
|
265
|
-
if (oldNode.type === "primitive") {
|
|
266
|
-
if (oldNode.valueType !== newNode.valueType) {
|
|
267
|
-
breaking.push({
|
|
268
|
-
path: prefix,
|
|
269
|
-
kind: "primitive-type-changed",
|
|
270
|
-
was: oldNode.valueType,
|
|
271
|
-
is: newNode.valueType,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// cycle / deep — terminal, nothing more to compare
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function compare(oldSnapshot, newSnapshot) {
|
|
281
|
-
if (!oldSnapshot || !oldSnapshot.exports) {
|
|
282
|
-
throw new ApiSnapshotError("api-snapshot/bad-snapshot",
|
|
283
|
-
"compare: oldSnapshot is required (a snapshot from read()/capture())");
|
|
284
|
-
}
|
|
285
|
-
if (!newSnapshot || !newSnapshot.exports) {
|
|
286
|
-
throw new ApiSnapshotError("api-snapshot/bad-snapshot",
|
|
287
|
-
"compare: newSnapshot is required (a snapshot from capture())");
|
|
288
|
-
}
|
|
289
|
-
var breaking = [];
|
|
290
|
-
var additive = [];
|
|
291
|
-
var typeChanged = [];
|
|
292
|
-
// Wrap exports in an object node so the recursion treats them uniformly
|
|
293
|
-
_walkCompare(
|
|
294
|
-
{ type: "object", members: oldSnapshot.exports },
|
|
295
|
-
{ type: "object", members: newSnapshot.exports },
|
|
296
|
-
"", breaking, additive, typeChanged
|
|
297
|
-
);
|
|
298
|
-
return { breaking: breaking, additive: additive, typeChanged: typeChanged };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function formatDiff(diff) {
|
|
302
|
-
if (!diff || typeof diff !== "object") {
|
|
303
|
-
throw new ApiSnapshotError("api-snapshot/bad-diff",
|
|
304
|
-
"formatDiff: argument must be a diff result from compare()");
|
|
305
|
-
}
|
|
306
|
-
var lines = [];
|
|
307
|
-
if (diff.breaking.length === 0 && diff.additive.length === 0) {
|
|
308
|
-
return "[api-snapshot] no changes";
|
|
309
|
-
}
|
|
310
|
-
if (diff.breaking.length > 0) {
|
|
311
|
-
lines.push("[api-snapshot] BREAKING (" + diff.breaking.length + "):");
|
|
312
|
-
for (var i = 0; i < diff.breaking.length; i++) {
|
|
313
|
-
var b = diff.breaking[i];
|
|
314
|
-
var line = " - " + b.path + " (" + b.kind + ")";
|
|
315
|
-
if (b.was !== undefined) line += " was=" + JSON.stringify(b.was);
|
|
316
|
-
if (b.is !== undefined) line += " is=" + JSON.stringify(b.is);
|
|
317
|
-
lines.push(line);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
if (diff.additive.length > 0) {
|
|
321
|
-
lines.push("[api-snapshot] additive (" + diff.additive.length + ", informational):");
|
|
322
|
-
for (var j = 0; j < diff.additive.length; j++) {
|
|
323
|
-
var a = diff.additive[j];
|
|
324
|
-
lines.push(" + " + a.path + " (" + a.type + ")");
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return lines.join("\n");
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
module.exports = {
|
|
331
|
-
capture: capture,
|
|
332
|
-
write: write,
|
|
333
|
-
read: read,
|
|
334
|
-
compare: compare,
|
|
335
|
-
formatDiff: formatDiff,
|
|
336
|
-
SNAPSHOT_FORMAT_VERSION: SNAPSHOT_FORMAT_VERSION,
|
|
337
|
-
ApiSnapshotError: ApiSnapshotError,
|
|
338
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* api-snapshot — public API surface walker + breaking-change detector.
|
|
4
|
+
*
|
|
5
|
+
* The framework's LTS-contract enforcement at the type level. Walks
|
|
6
|
+
* the framework's module exports recursively, records every member's
|
|
7
|
+
* type, and compares two snapshots to find:
|
|
8
|
+
*
|
|
9
|
+
* - removed a member present in the old snapshot but not the new
|
|
10
|
+
* (BREAKING — fails CI)
|
|
11
|
+
* - typeChanged a member's category flipped (function → object, etc.)
|
|
12
|
+
* (BREAKING — fails CI)
|
|
13
|
+
* - added a new member that wasn't in the old snapshot
|
|
14
|
+
* (ADDITIVE — does not fail; signals the snapshot is
|
|
15
|
+
* out-of-date and the operator should rerun capture)
|
|
16
|
+
*
|
|
17
|
+
* var snap = b.apiSnapshot.capture(require("@blamejs/core"));
|
|
18
|
+
* // { version, frameworkVersion, createdAt,
|
|
19
|
+
* // exports: { ... nested tree ... } }
|
|
20
|
+
*
|
|
21
|
+
* b.apiSnapshot.write(snap, "./api-snapshot.json");
|
|
22
|
+
* var loaded = b.apiSnapshot.read("./api-snapshot.json");
|
|
23
|
+
*
|
|
24
|
+
* var diff = b.apiSnapshot.compare(loaded, snap);
|
|
25
|
+
* // { breaking: [{ path, kind, was?, is? }],
|
|
26
|
+
* // typeChanged: [{ path, was, is }],
|
|
27
|
+
* // additive: [{ path, type }] }
|
|
28
|
+
*
|
|
29
|
+
* if (diff.breaking.length > 0 || diff.typeChanged.length > 0) {
|
|
30
|
+
* console.error(b.apiSnapshot.formatDiff(diff));
|
|
31
|
+
* process.exit(1);
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* Walker rules:
|
|
35
|
+
* - Functions record as { type: 'function', arity: fn.length }.
|
|
36
|
+
* Class constructors are still 'function' — recursive scope walks
|
|
37
|
+
* prototype only when the operator explicitly opts in via
|
|
38
|
+
* opts.includeClassPrototypes.
|
|
39
|
+
* - Plain objects recurse into their own enumerable string keys.
|
|
40
|
+
* - Primitives (string, number, boolean, null, undefined) record as
|
|
41
|
+
* { type: 'primitive', valueType: typeof v }. Specific values are
|
|
42
|
+
* NOT captured — only the type — so a version-string change in
|
|
43
|
+
* constants doesn't fail CI.
|
|
44
|
+
* - Members whose key starts with '_' are skipped (test seams,
|
|
45
|
+
* internal helpers).
|
|
46
|
+
* - Cycles are detected and short-circuit as { type: 'cycle' }.
|
|
47
|
+
* - Non-plain objects (Map, Set, Buffer, Date, RegExp, Error, etc.)
|
|
48
|
+
* are recorded as { type: 'instance', constructor: name } without
|
|
49
|
+
* recursion — they're terminal nodes.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
var fs = require("fs");
|
|
53
|
+
var nb = require("./numeric-bounds");
|
|
54
|
+
var safeJson = require("./safe-json");
|
|
55
|
+
var { FrameworkError } = require("./framework-error");
|
|
56
|
+
|
|
57
|
+
var DEFAULT_MAX_DEPTH = 0x08;
|
|
58
|
+
|
|
59
|
+
class ApiSnapshotError extends FrameworkError {
|
|
60
|
+
constructor(code, message) {
|
|
61
|
+
super(message, code);
|
|
62
|
+
this.name = "ApiSnapshotError";
|
|
63
|
+
this.permanent = true;
|
|
64
|
+
this.isApiSnapshotError = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var SNAPSHOT_FORMAT_VERSION = 1;
|
|
69
|
+
|
|
70
|
+
function _isPlainObject(v) {
|
|
71
|
+
if (v === null || typeof v !== "object") return false;
|
|
72
|
+
var proto = Object.getPrototypeOf(v);
|
|
73
|
+
return proto === Object.prototype || proto === null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _walkNode(value, depth, maxDepth, seen, skipUnderscore) {
|
|
77
|
+
if (value === null) return { type: "primitive", valueType: "null" };
|
|
78
|
+
var t = typeof value;
|
|
79
|
+
if (t === "undefined") return { type: "primitive", valueType: "undefined" };
|
|
80
|
+
if (t === "string" || t === "number" || t === "boolean" || t === "bigint" || t === "symbol") {
|
|
81
|
+
return { type: "primitive", valueType: t };
|
|
82
|
+
}
|
|
83
|
+
if (t === "function") {
|
|
84
|
+
return { type: "function", arity: value.length };
|
|
85
|
+
}
|
|
86
|
+
// Object — guard cycles + depth
|
|
87
|
+
if (seen.has(value)) return { type: "cycle" };
|
|
88
|
+
if (depth >= maxDepth) return { type: "deep", note: "max depth" };
|
|
89
|
+
|
|
90
|
+
if (!_isPlainObject(value)) {
|
|
91
|
+
var name = value && value.constructor && value.constructor.name
|
|
92
|
+
? value.constructor.name
|
|
93
|
+
: "Object";
|
|
94
|
+
// Use 'ctorName' instead of 'constructor' — json-safe.parse strips
|
|
95
|
+
// 'constructor' as a prototype-pollution defense, which would
|
|
96
|
+
// round-trip-mangle every instance node otherwise.
|
|
97
|
+
return { type: "instance", ctorName: name };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Plain object — recurse
|
|
101
|
+
seen.add(value);
|
|
102
|
+
var members = {};
|
|
103
|
+
var keys = Object.keys(value);
|
|
104
|
+
// Stable sort for canonical snapshot bytes
|
|
105
|
+
keys.sort();
|
|
106
|
+
for (var i = 0; i < keys.length; i++) {
|
|
107
|
+
var k = keys[i];
|
|
108
|
+
if (skipUnderscore && k.charAt(0) === "_") continue;
|
|
109
|
+
members[k] = _walkNode(value[k], depth + 1, maxDepth, seen, skipUnderscore);
|
|
110
|
+
}
|
|
111
|
+
seen.delete(value);
|
|
112
|
+
return { type: "object", members: members };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function capture(target, opts) {
|
|
116
|
+
opts = opts || {};
|
|
117
|
+
if (!target || typeof target !== "object") {
|
|
118
|
+
throw new ApiSnapshotError("api-snapshot/bad-target",
|
|
119
|
+
"capture: target must be a module's exports object");
|
|
120
|
+
}
|
|
121
|
+
var maxDepth = nb.isPositiveFiniteInt(opts.maxDepth) ? opts.maxDepth : DEFAULT_MAX_DEPTH;
|
|
122
|
+
var skipUnderscore = opts.skipUnderscore !== false;
|
|
123
|
+
var snapshot = _walkNode(target, 0, maxDepth, new Set(), skipUnderscore);
|
|
124
|
+
if (snapshot.type !== "object") {
|
|
125
|
+
throw new ApiSnapshotError("api-snapshot/bad-target",
|
|
126
|
+
"capture: top-level target must be a plain object (got '" + snapshot.type + "')");
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
version: SNAPSHOT_FORMAT_VERSION,
|
|
130
|
+
frameworkVersion: typeof opts.frameworkVersion === "string" && opts.frameworkVersion.length > 0
|
|
131
|
+
? opts.frameworkVersion
|
|
132
|
+
: (target.version || "0.0.0"),
|
|
133
|
+
createdAt: opts.createdAt || new Date().toISOString(),
|
|
134
|
+
exports: snapshot.members,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function write(snapshot, filePath) {
|
|
139
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
140
|
+
throw new ApiSnapshotError("api-snapshot/bad-snapshot",
|
|
141
|
+
"write: snapshot must be a snapshot object (returned by capture)");
|
|
142
|
+
}
|
|
143
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
144
|
+
throw new ApiSnapshotError("api-snapshot/bad-path",
|
|
145
|
+
"write: filePath is required");
|
|
146
|
+
}
|
|
147
|
+
// Stringify with stable key order via the explicit canonical form
|
|
148
|
+
var canonical = {
|
|
149
|
+
version: snapshot.version,
|
|
150
|
+
frameworkVersion: snapshot.frameworkVersion,
|
|
151
|
+
createdAt: snapshot.createdAt,
|
|
152
|
+
exports: snapshot.exports,
|
|
153
|
+
};
|
|
154
|
+
fs.writeFileSync(filePath, JSON.stringify(canonical, null, 2) + "\n", { mode: 0o644 });
|
|
155
|
+
return filePath;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function read(filePath) {
|
|
159
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
160
|
+
throw new ApiSnapshotError("api-snapshot/bad-path",
|
|
161
|
+
"read: filePath is required");
|
|
162
|
+
}
|
|
163
|
+
if (!fs.existsSync(filePath)) {
|
|
164
|
+
throw new ApiSnapshotError("api-snapshot/missing",
|
|
165
|
+
"read: snapshot file not found at " + filePath);
|
|
166
|
+
}
|
|
167
|
+
var raw;
|
|
168
|
+
try { raw = fs.readFileSync(filePath, "utf8"); }
|
|
169
|
+
catch (e) {
|
|
170
|
+
throw new ApiSnapshotError("api-snapshot/read-failed",
|
|
171
|
+
"read: cannot read " + filePath + ": " + ((e && e.message) || String(e)));
|
|
172
|
+
}
|
|
173
|
+
var parsed;
|
|
174
|
+
try { parsed = safeJson.parse(raw); }
|
|
175
|
+
catch (e) {
|
|
176
|
+
throw new ApiSnapshotError("api-snapshot/bad-json",
|
|
177
|
+
"read: not valid JSON: " + ((e && e.message) || String(e)));
|
|
178
|
+
}
|
|
179
|
+
if (!parsed || parsed.version !== SNAPSHOT_FORMAT_VERSION) {
|
|
180
|
+
throw new ApiSnapshotError("api-snapshot/bad-version",
|
|
181
|
+
"read: snapshot version is " + (parsed && parsed.version) +
|
|
182
|
+
", expected " + SNAPSHOT_FORMAT_VERSION);
|
|
183
|
+
}
|
|
184
|
+
if (!parsed.exports || typeof parsed.exports !== "object") {
|
|
185
|
+
throw new ApiSnapshotError("api-snapshot/bad-shape",
|
|
186
|
+
"read: snapshot is missing 'exports' object");
|
|
187
|
+
}
|
|
188
|
+
return parsed;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Walk both trees in parallel under a path. Append to breaking,
|
|
192
|
+
// additive, typeChanged.
|
|
193
|
+
function _walkCompare(oldNode, newNode, prefix, breaking, additive, typeChanged) {
|
|
194
|
+
// Both should describe the same node. If types differ at the node
|
|
195
|
+
// level, that's a breaking type change.
|
|
196
|
+
if (!oldNode || !newNode) return;
|
|
197
|
+
|
|
198
|
+
if (oldNode.type !== newNode.type) {
|
|
199
|
+
typeChanged.push({
|
|
200
|
+
path: prefix,
|
|
201
|
+
was: oldNode.type,
|
|
202
|
+
is: newNode.type,
|
|
203
|
+
});
|
|
204
|
+
breaking.push({ path: prefix, kind: "type-changed", was: oldNode.type, is: newNode.type });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (oldNode.type === "object") {
|
|
209
|
+
var oldMembers = oldNode.members || {};
|
|
210
|
+
var newMembers = newNode.members || {};
|
|
211
|
+
var oldKeys = Object.keys(oldMembers);
|
|
212
|
+
var newKeys = Object.keys(newMembers);
|
|
213
|
+
|
|
214
|
+
// Removed: in old, not in new
|
|
215
|
+
for (var i = 0; i < oldKeys.length; i++) {
|
|
216
|
+
var ok = oldKeys[i];
|
|
217
|
+
var childPath = prefix ? (prefix + "." + ok) : ok;
|
|
218
|
+
if (!Object.prototype.hasOwnProperty.call(newMembers, ok)) {
|
|
219
|
+
breaking.push({ path: childPath, kind: "removed", was: oldMembers[ok].type });
|
|
220
|
+
} else {
|
|
221
|
+
_walkCompare(oldMembers[ok], newMembers[ok], childPath, breaking, additive, typeChanged);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Added: in new, not in old
|
|
225
|
+
for (var j = 0; j < newKeys.length; j++) {
|
|
226
|
+
var nk = newKeys[j];
|
|
227
|
+
if (!Object.prototype.hasOwnProperty.call(oldMembers, nk)) {
|
|
228
|
+
var addPath = prefix ? (prefix + "." + nk) : nk;
|
|
229
|
+
additive.push({ path: addPath, type: newMembers[nk].type });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// For function nodes, arity DROPS are flagged (operator removed a
|
|
236
|
+
// required parameter). Arity INCREASES are not flagged (added
|
|
237
|
+
// optional param at the end is additive).
|
|
238
|
+
if (oldNode.type === "function") {
|
|
239
|
+
if (typeof oldNode.arity === "number" && typeof newNode.arity === "number" &&
|
|
240
|
+
newNode.arity < oldNode.arity) {
|
|
241
|
+
breaking.push({
|
|
242
|
+
path: prefix,
|
|
243
|
+
kind: "arity-decreased",
|
|
244
|
+
was: "function/" + oldNode.arity,
|
|
245
|
+
is: "function/" + newNode.arity,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// For instance nodes, a constructor-name change is breaking
|
|
252
|
+
if (oldNode.type === "instance") {
|
|
253
|
+
if (oldNode.ctorName !== newNode.ctorName) {
|
|
254
|
+
breaking.push({
|
|
255
|
+
path: prefix,
|
|
256
|
+
kind: "constructor-changed",
|
|
257
|
+
was: oldNode.ctorName,
|
|
258
|
+
is: newNode.ctorName,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// For primitive nodes, a valueType change is breaking
|
|
265
|
+
if (oldNode.type === "primitive") {
|
|
266
|
+
if (oldNode.valueType !== newNode.valueType) {
|
|
267
|
+
breaking.push({
|
|
268
|
+
path: prefix,
|
|
269
|
+
kind: "primitive-type-changed",
|
|
270
|
+
was: oldNode.valueType,
|
|
271
|
+
is: newNode.valueType,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// cycle / deep — terminal, nothing more to compare
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function compare(oldSnapshot, newSnapshot) {
|
|
281
|
+
if (!oldSnapshot || !oldSnapshot.exports) {
|
|
282
|
+
throw new ApiSnapshotError("api-snapshot/bad-snapshot",
|
|
283
|
+
"compare: oldSnapshot is required (a snapshot from read()/capture())");
|
|
284
|
+
}
|
|
285
|
+
if (!newSnapshot || !newSnapshot.exports) {
|
|
286
|
+
throw new ApiSnapshotError("api-snapshot/bad-snapshot",
|
|
287
|
+
"compare: newSnapshot is required (a snapshot from capture())");
|
|
288
|
+
}
|
|
289
|
+
var breaking = [];
|
|
290
|
+
var additive = [];
|
|
291
|
+
var typeChanged = [];
|
|
292
|
+
// Wrap exports in an object node so the recursion treats them uniformly
|
|
293
|
+
_walkCompare(
|
|
294
|
+
{ type: "object", members: oldSnapshot.exports },
|
|
295
|
+
{ type: "object", members: newSnapshot.exports },
|
|
296
|
+
"", breaking, additive, typeChanged
|
|
297
|
+
);
|
|
298
|
+
return { breaking: breaking, additive: additive, typeChanged: typeChanged };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function formatDiff(diff) {
|
|
302
|
+
if (!diff || typeof diff !== "object") {
|
|
303
|
+
throw new ApiSnapshotError("api-snapshot/bad-diff",
|
|
304
|
+
"formatDiff: argument must be a diff result from compare()");
|
|
305
|
+
}
|
|
306
|
+
var lines = [];
|
|
307
|
+
if (diff.breaking.length === 0 && diff.additive.length === 0) {
|
|
308
|
+
return "[api-snapshot] no changes";
|
|
309
|
+
}
|
|
310
|
+
if (diff.breaking.length > 0) {
|
|
311
|
+
lines.push("[api-snapshot] BREAKING (" + diff.breaking.length + "):");
|
|
312
|
+
for (var i = 0; i < diff.breaking.length; i++) {
|
|
313
|
+
var b = diff.breaking[i];
|
|
314
|
+
var line = " - " + b.path + " (" + b.kind + ")";
|
|
315
|
+
if (b.was !== undefined) line += " was=" + JSON.stringify(b.was);
|
|
316
|
+
if (b.is !== undefined) line += " is=" + JSON.stringify(b.is);
|
|
317
|
+
lines.push(line);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (diff.additive.length > 0) {
|
|
321
|
+
lines.push("[api-snapshot] additive (" + diff.additive.length + ", informational):");
|
|
322
|
+
for (var j = 0; j < diff.additive.length; j++) {
|
|
323
|
+
var a = diff.additive[j];
|
|
324
|
+
lines.push(" + " + a.path + " (" + a.type + ")");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return lines.join("\n");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = {
|
|
331
|
+
capture: capture,
|
|
332
|
+
write: write,
|
|
333
|
+
read: read,
|
|
334
|
+
compare: compare,
|
|
335
|
+
formatDiff: formatDiff,
|
|
336
|
+
SNAPSHOT_FORMAT_VERSION: SNAPSHOT_FORMAT_VERSION,
|
|
337
|
+
ApiSnapshotError: ApiSnapshotError,
|
|
338
|
+
};
|