@blamejs/blamejs-shop 0.4.3 → 0.4.4
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/README.md +8 -7
- package/lib/admin.js +376 -0
- package/lib/asset-manifest.json +3 -3
- package/lib/storefront.js +252 -6
- package/lib/vendor/MANIFEST.json +23 -23
- package/lib/vendor/blamejs/.pinact.yaml +1 -1
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/SECURITY.md +1 -1
- package/lib/vendor/blamejs/api-snapshot.json +15 -2
- package/lib/vendor/blamejs/index.js +5 -1
- package/lib/vendor/blamejs/lib/auth/jar.js +190 -28
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +213 -0
- package/lib/vendor/blamejs/lib/auth/oauth.js +115 -101
- package/lib/vendor/blamejs/lib/http-client.js +3 -4
- package/lib/vendor/blamejs/lib/lro.js +3 -4
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +2 -10
- package/lib/vendor/blamejs/lib/middleware/health.js +1 -4
- package/lib/vendor/blamejs/lib/middleware/trace-log-correlation.js +3 -6
- package/lib/vendor/blamejs/lib/validate-opts.js +34 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.22.json +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jar.test.js +226 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +122 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/jwt-external.test.js +104 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +127 -0
- package/package.json +1 -1
- package/lib/vendor/blamejs/memory/specs/node-26-map-getorinsert-migration.md +0 -165
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* - authorizationUrl / exchangeCode authorization_details (RFC 9396 RAR)
|
|
9
9
|
* - buildClientAttestation / buildClientAttestationPop /
|
|
10
10
|
* verifyClientAttestation (draft-ietf-oauth-attestation-based-client-auth)
|
|
11
|
+
* - pushAuthorizationRequest signed-request-object opt (RFC 9101 +
|
|
12
|
+
* RFC 9126 §3 — request= in the PAR body, params as JAR claims)
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
var http = require("node:http");
|
|
@@ -39,6 +41,46 @@ function _spawnDiscoveryServer(methods) {
|
|
|
39
41
|
return server;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
// Mock authorization server with discovery + a PAR endpoint that records
|
|
45
|
+
// the posted form body. Discovery advertises S256 so the PKCE-downgrade
|
|
46
|
+
// gate passes; the PAR endpoint returns a fixed request_uri.
|
|
47
|
+
function _spawnParServer() {
|
|
48
|
+
var captured = { body: null };
|
|
49
|
+
var holder = { value: null };
|
|
50
|
+
var server = http.createServer(function (req, res) {
|
|
51
|
+
var u = new URL(req.url, "http://localhost");
|
|
52
|
+
if (u.pathname === "/.well-known/openid-configuration") {
|
|
53
|
+
var doc = {
|
|
54
|
+
issuer: holder.value,
|
|
55
|
+
authorization_endpoint: holder.value + "/auth",
|
|
56
|
+
token_endpoint: holder.value + "/token",
|
|
57
|
+
jwks_uri: holder.value + "/jwks",
|
|
58
|
+
pushed_authorization_request_endpoint: holder.value + "/par",
|
|
59
|
+
code_challenge_methods_supported: ["S256"],
|
|
60
|
+
};
|
|
61
|
+
var body = JSON.stringify(doc);
|
|
62
|
+
res.writeHead(200, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
|
|
63
|
+
res.end(body);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (u.pathname === "/par" && req.method === "POST") {
|
|
67
|
+
var chunks = [];
|
|
68
|
+
req.on("data", function (c) { chunks.push(c); });
|
|
69
|
+
req.on("end", function () {
|
|
70
|
+
captured.body = Buffer.concat(chunks).toString("utf8");
|
|
71
|
+
var out = JSON.stringify({ request_uri: "urn:ietf:params:oauth:request_uri:abc123", expires_in: 90 });
|
|
72
|
+
res.writeHead(201, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(out) });
|
|
73
|
+
res.end(out);
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
res.writeHead(404); res.end();
|
|
78
|
+
});
|
|
79
|
+
server._holder = holder;
|
|
80
|
+
server._captured = captured;
|
|
81
|
+
return server;
|
|
82
|
+
}
|
|
83
|
+
|
|
42
84
|
async function _pkceDowngradeCase(methods, expectRefusal, label) {
|
|
43
85
|
var server = _spawnDiscoveryServer(methods);
|
|
44
86
|
await new Promise(function (r) { server.listen(0, "127.0.0.1", r); });
|
|
@@ -409,6 +451,91 @@ async function run() {
|
|
|
409
451
|
check("oauth.clientAttestationHeaders: emits both header fields",
|
|
410
452
|
typeof hdrs.headers["OAuth-Client-Attestation"] === "string" &&
|
|
411
453
|
typeof hdrs.headers["OAuth-Client-Attestation-PoP"] === "string");
|
|
454
|
+
|
|
455
|
+
// ---- RFC 9101 + RFC 9126 §3 — PAR with a signed request object ----
|
|
456
|
+
await _testParRequestObject();
|
|
457
|
+
await _testParPlainUnchanged();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// PAR carrying a signed request object: the form body MUST hold `request=`
|
|
461
|
+
// + client auth ONLY, and the authorization parameters travel as request-
|
|
462
|
+
// object claims, NOT as bare form params. The pushed JWT is verified
|
|
463
|
+
// in-test against the client's public JWK via b.auth.jar.parse.
|
|
464
|
+
async function _testParRequestObject() {
|
|
465
|
+
var server = _spawnParServer();
|
|
466
|
+
await new Promise(function (r) { server.listen(0, "127.0.0.1", r); });
|
|
467
|
+
var issuer = "http://127.0.0.1:" + server.address().port;
|
|
468
|
+
server._holder.value = issuer;
|
|
469
|
+
try {
|
|
470
|
+
var kp = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
471
|
+
var pubJwk = Object.assign(kp.publicKey.export({ format: "jwk" }), { kid: "c1", use: "sig", alg: "ES256" });
|
|
472
|
+
var oa = b.auth.oauth.create({
|
|
473
|
+
issuer: issuer,
|
|
474
|
+
clientId: "par-client",
|
|
475
|
+
clientSecret: "par-secret",
|
|
476
|
+
redirectUri: "https://rp.example/cb",
|
|
477
|
+
scope: ["openid", "profile"],
|
|
478
|
+
isOidc: true,
|
|
479
|
+
allowHttp: true,
|
|
480
|
+
allowInternal: true,
|
|
481
|
+
});
|
|
482
|
+
var rv = await oa.pushAuthorizationRequest({
|
|
483
|
+
signedRequestObject: { key: kp.privateKey, kid: "c1" },
|
|
484
|
+
});
|
|
485
|
+
check("oauth.PAR+RO: returns a request_uri", rv.requestUri === "urn:ietf:params:oauth:request_uri:abc123");
|
|
486
|
+
check("oauth.PAR+RO: flags requestObjectSent", rv.requestObjectSent === true);
|
|
487
|
+
|
|
488
|
+
var posted = new URLSearchParams(server._captured.body);
|
|
489
|
+
check("oauth.PAR+RO: form body carries request=", typeof posted.get("request") === "string" && posted.get("request").length > 0);
|
|
490
|
+
check("oauth.PAR+RO: form body carries client_id (client auth)", posted.get("client_id") === "par-client");
|
|
491
|
+
check("oauth.PAR+RO: form body carries client_secret (client auth)", posted.get("client_secret") === "par-secret");
|
|
492
|
+
// RFC 9126 §3 — the authorization parameters MUST NOT appear as bare
|
|
493
|
+
// form params alongside the request object.
|
|
494
|
+
check("oauth.PAR+RO: response_type NOT a bare form param", posted.get("response_type") === null);
|
|
495
|
+
check("oauth.PAR+RO: redirect_uri NOT a bare form param", posted.get("redirect_uri") === null);
|
|
496
|
+
check("oauth.PAR+RO: scope NOT a bare form param", posted.get("scope") === null);
|
|
497
|
+
check("oauth.PAR+RO: code_challenge NOT a bare form param", posted.get("code_challenge") === null);
|
|
498
|
+
|
|
499
|
+
// The pushed request object verifies against the client's public key and
|
|
500
|
+
// carries the authorization parameters as claims (round-trip via jar.parse).
|
|
501
|
+
var parsed = await b.auth.jar.parse(posted.get("request"), {
|
|
502
|
+
clientId: "par-client", audience: issuer, algorithms: ["ES256"], jwks: [pubJwk],
|
|
503
|
+
});
|
|
504
|
+
check("oauth.PAR+RO: request object verifies + carries response_type claim", parsed.params.response_type === "code");
|
|
505
|
+
check("oauth.PAR+RO: request object carries redirect_uri + scope + S256 claims",
|
|
506
|
+
parsed.params.redirect_uri === "https://rp.example/cb" && parsed.params.scope === "openid profile" &&
|
|
507
|
+
parsed.params.code_challenge_method === "S256" && typeof parsed.params.code_challenge === "string");
|
|
508
|
+
check("oauth.PAR+RO: request object aud is the AS issuer", parsed.claims.aud === issuer);
|
|
509
|
+
} finally { server.close(); }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// PAR WITHOUT a signed request object: byte-for-byte the prior plain-form
|
|
513
|
+
// behavior — authorization parameters are bare form params, no `request=`.
|
|
514
|
+
async function _testParPlainUnchanged() {
|
|
515
|
+
var server = _spawnParServer();
|
|
516
|
+
await new Promise(function (r) { server.listen(0, "127.0.0.1", r); });
|
|
517
|
+
var issuer = "http://127.0.0.1:" + server.address().port;
|
|
518
|
+
server._holder.value = issuer;
|
|
519
|
+
try {
|
|
520
|
+
var oa = b.auth.oauth.create({
|
|
521
|
+
issuer: issuer,
|
|
522
|
+
clientId: "par-client",
|
|
523
|
+
clientSecret: "par-secret",
|
|
524
|
+
redirectUri: "https://rp.example/cb",
|
|
525
|
+
scope: ["openid"],
|
|
526
|
+
isOidc: true,
|
|
527
|
+
allowHttp: true,
|
|
528
|
+
allowInternal: true,
|
|
529
|
+
});
|
|
530
|
+
var rv = await oa.pushAuthorizationRequest();
|
|
531
|
+
check("oauth.PAR plain: requestObjectSent is false", rv.requestObjectSent === false);
|
|
532
|
+
var posted = new URLSearchParams(server._captured.body);
|
|
533
|
+
check("oauth.PAR plain: NO request= param", posted.get("request") === null);
|
|
534
|
+
check("oauth.PAR plain: response_type bare form param present", posted.get("response_type") === "code");
|
|
535
|
+
check("oauth.PAR plain: redirect_uri bare form param present", posted.get("redirect_uri") === "https://rp.example/cb");
|
|
536
|
+
check("oauth.PAR plain: code_challenge_method S256 present", posted.get("code_challenge_method") === "S256");
|
|
537
|
+
check("oauth.PAR plain: client_secret present", posted.get("client_secret") === "par-secret");
|
|
538
|
+
} finally { server.close(); }
|
|
412
539
|
}
|
|
413
540
|
|
|
414
541
|
module.exports = { run: run };
|
package/package.json
CHANGED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
# Node 26 — `Map.prototype.getOrInsertComputed` migration plan
|
|
2
|
-
|
|
3
|
-
**Status:** detector landed; sweep deferred to the Node 26 floor-bump
|
|
4
|
-
(eligible Oct 2026 per the LTS calendar).
|
|
5
|
-
|
|
6
|
-
**Floor today:** `engines.node: ">=24"`. Do NOT do the sweep yet.
|
|
7
|
-
|
|
8
|
-
## What changes when the floor moves
|
|
9
|
-
|
|
10
|
-
Node 26 ships
|
|
11
|
-
[`Map.prototype.getOrInsertComputed(key, factory)`](https://github.com/tc39/proposal-upsert)
|
|
12
|
-
(TC39 stage-4, V8 13.x). It replaces two distinct framework-internal
|
|
13
|
-
shapes:
|
|
14
|
-
|
|
15
|
-
**Variant A** — `var X = M.get(k); if (!X) { ... ; M.set(k, ...); }`:
|
|
16
|
-
|
|
17
|
-
```js
|
|
18
|
-
// before
|
|
19
|
-
var s = tagIndex.get(tags[i]);
|
|
20
|
-
if (!s) { s = new Set(); tagIndex.set(tags[i], s); }
|
|
21
|
-
s.add(key);
|
|
22
|
-
|
|
23
|
-
// after
|
|
24
|
-
var s = tagIndex.getOrInsertComputed(tags[i], function () { return new Set(); });
|
|
25
|
-
s.add(key);
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
**Variant B** — `if (!M.has(k)) { ... M.set(k, ...); }`:
|
|
29
|
-
|
|
30
|
-
```js
|
|
31
|
-
// before
|
|
32
|
-
if (!channelToConns.has(channel)) {
|
|
33
|
-
channelToConns.set(channel, new Set());
|
|
34
|
-
var token = ps.subscribe(channel, _onPubsubMessage);
|
|
35
|
-
channelToToken.set(channel, token);
|
|
36
|
-
}
|
|
37
|
-
channelToConns.get(channel).add(conn);
|
|
38
|
-
|
|
39
|
-
// after — single lookup, no half-built-state observer window
|
|
40
|
-
var conns = channelToConns.getOrInsertComputed(channel, function () {
|
|
41
|
-
channelToToken.set(channel, ps.subscribe(channel, _onPubsubMessage));
|
|
42
|
-
return new Set();
|
|
43
|
-
});
|
|
44
|
-
conns.add(conn);
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Two wins:
|
|
48
|
-
|
|
49
|
-
1. **One lookup instead of two.** `has` + `set` (or `get` + `set`) is a
|
|
50
|
-
double hash probe. `getOrInsertComputed` is one probe with an
|
|
51
|
-
on-miss factory call.
|
|
52
|
-
2. **Race-window closure in cluster-shared registries.** Between
|
|
53
|
-
`M.has(k) === false` and `M.set(k, v)`, an interleaved observer
|
|
54
|
-
(debug tooling, registry-snapshot, audit-chain walker) could see
|
|
55
|
-
the key as absent OR halfway-built. `getOrInsertComputed`
|
|
56
|
-
collapses the gap to a single engine-internal step; no
|
|
57
|
-
intermediate state is observable.
|
|
58
|
-
|
|
59
|
-
## Call-site survey (lib/ ground truth, vendor/ excluded)
|
|
60
|
-
|
|
61
|
-
Detector survey at v0.11.2. Counts are *call sites*, not files —
|
|
62
|
-
several files house multiple sites in different methods of the same
|
|
63
|
-
closure-built object (e.g. metrics counters/gauges/histograms).
|
|
64
|
-
|
|
65
|
-
### Agent substrate / cluster-shared state
|
|
66
|
-
|
|
67
|
-
None. (`lib/agent-event-bus.js`, `lib/agent-orchestrator.js`,
|
|
68
|
-
`lib/agent-snapshot.js`, `lib/agent-idempotency.js`,
|
|
69
|
-
`lib/audit-chain.js`, `lib/audit.js`, `lib/break-glass.js`,
|
|
70
|
-
`lib/cms-codec.js` audited — only guard-throw / presence-assertion
|
|
71
|
-
shapes, no get-or-insert.)
|
|
72
|
-
|
|
73
|
-
### Cache / memoization
|
|
74
|
-
|
|
75
|
-
| File | Lines | Map | Factory |
|
|
76
|
-
| --------------------------------- | ----- | ------------------------- | --------------------- |
|
|
77
|
-
| `lib/cache.js` | 318 | `tagIndex` | `new Set()` |
|
|
78
|
-
| `lib/i18n-messageformat.js` | 317 | `_pluralRulesCache` | `new Intl.PluralRules`|
|
|
79
|
-
| `lib/i18n.js` | 360 | formatter cache (closure) | closure-`make()` |
|
|
80
|
-
| `lib/deprecate.js` | 134 | `_seen` | object-literal |
|
|
81
|
-
|
|
82
|
-
### Observability / metrics
|
|
83
|
-
|
|
84
|
-
| File | Lines | Map | Factory |
|
|
85
|
-
| ------------------------------------- | ------------------ | ----------------- | ---------------- |
|
|
86
|
-
| `lib/metrics.js` | 390, 430, 526 | `values` (×3) | object-literal (counter + gauge `_ensure` + histogram observe; each with cardinality-cap early-return) |
|
|
87
|
-
| `lib/observability-otlp-exporter.js` | 164 | `byResource` | object-literal |
|
|
88
|
-
| `lib/otel-export.js` | 138, 151 | `counters` / `observations` | object-literal |
|
|
89
|
-
|
|
90
|
-
### Rate-limiting / quotas
|
|
91
|
-
|
|
92
|
-
| File | Lines | Map | Factory |
|
|
93
|
-
| --------------------------------- | ------------------ | ---------------------------------------------------------- | ---------------- |
|
|
94
|
-
| `lib/mail-server-rate-limit.js` | 209, 261, 291 | `connectionTimes` / `authFailureTimes` / `rcptFailureTimes` | `[]` (array) |
|
|
95
|
-
| `lib/middleware/rate-limit.js` | 130 | `buckets` | object-literal |
|
|
96
|
-
| `lib/network-byte-quota.js` | 82 | `store` | `_newEntry()` |
|
|
97
|
-
| `lib/crypto-field.js` | 592 | `_rateFailWindows` (in `_rateNoteFailure`) | `[]` (array) |
|
|
98
|
-
|
|
99
|
-
### Pubsub / websocket-channels
|
|
100
|
-
|
|
101
|
-
| File | Lines | Map | Factory | Race-window callout |
|
|
102
|
-
| --------------------------------- | ----- | ----------------- | ------------- | ------------------- |
|
|
103
|
-
| `lib/pubsub.js` | 350 | `exactSubs` | `new Set()` | local-only; sub-record under same-process closure — race-window benign |
|
|
104
|
-
| `lib/websocket-channels.js` | 193 | `channelToConns` | `new Set()` | **cluster-shared via `ps.subscribe` — race-window non-trivial.** Between `has(channel) === false` and `set(channel, new Set())`, the pubsub backend opens a remote subscription; a concurrent `subscribe(otherConn, sameChannel)` can see `channel`-as-known and skip the pubsub `subscribe()` even though the new Set is empty. `getOrInsertComputed` collapses the gap; the factory closure runs once, the pubsub subscribe call lifts inside it. |
|
|
105
|
-
|
|
106
|
-
### Edge cases — flagged structurally, do NOT migrate cleanly
|
|
107
|
-
|
|
108
|
-
| File | Lines | Why it doesn't migrate |
|
|
109
|
-
| --------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
-
| `lib/mail-greylist.js` | 405 | `memoryStore.put` is *put-or-replace-with-insertion-order-side-effect*. `data.set(key, value)` runs unconditionally (always overwrites the value); the `if (!data.has(key))` block manages an evict-oldest `insertionOrder` sidecar. `getOrInsertComputed` only runs the factory on miss — wrong semantics for an always-write. **Skip during sweep; keep inline.** |
|
|
111
|
-
| `lib/dsr.js` | 875 | `memoryTicketStore.update` is a *presence assertion*: `if (!byId.has(id)) throw new DsrError(...)` then `byId.set(id, ...)` runs OUTSIDE the if-block as an UPDATE. False positive — detector window crosses the closing `}`. **Skip during sweep; no rewrite.** |
|
|
112
|
-
|
|
113
|
-
### Totals
|
|
114
|
-
|
|
115
|
-
- **Migratable call sites:** ~18 across 12 files (variants A + B; metrics + rate-limit + otel each have multiple sites per file).
|
|
116
|
-
- **Files allowlisted (migratable):** 13 (`cache.js`, `crypto-field.js`, `deprecate.js`, `i18n-messageformat.js`, `i18n.js`, `mail-server-rate-limit.js`, `metrics.js`, `middleware/rate-limit.js`, `network-byte-quota.js`, `observability-otlp-exporter.js`, `otel-export.js`, `pubsub.js`, `websocket-channels.js`).
|
|
117
|
-
- **Files allowlisted (do-not-migrate edge cases):** 2 (`mail-greylist.js`, `dsr.js`).
|
|
118
|
-
|
|
119
|
-
The user-supplied "~137 sites across 80+ files" figure was an estimate
|
|
120
|
-
that included WeakMap / plain-object / `has`-as-allowlist-membership
|
|
121
|
-
shapes; the actual `getOrInsertComputed`-migratable surface is the
|
|
122
|
-
~17 sites above.
|
|
123
|
-
|
|
124
|
-
## Floor-bump sweep plan
|
|
125
|
-
|
|
126
|
-
When `engines.node` advances to `>=26` (eligible Oct 2026 per LTS
|
|
127
|
-
calendar; do NOT bump earlier):
|
|
128
|
-
|
|
129
|
-
1. **One commit per domain group** (cache, observability, rate-limit,
|
|
130
|
-
pubsub/websocket). Each commit:
|
|
131
|
-
- Walks every call site in the matching allowlist entry.
|
|
132
|
-
- Converts `var X = M.get(k); if (!X) { X = factory(); M.set(k, X); }`
|
|
133
|
-
to `var X = M.getOrInsertComputed(k, function () { return factory(); });`.
|
|
134
|
-
- Verifies the factory closure is pure (or its side-effects are
|
|
135
|
-
intentional in the on-miss-only path). For `websocket-channels`
|
|
136
|
-
specifically, the pubsub-subscribe side-effect MUST move into
|
|
137
|
-
the factory — that's the race-window fix.
|
|
138
|
-
- Removes the file from the detector's allowlist.
|
|
139
|
-
- Runs the full release-gate sequence (codebase-patterns, smoke,
|
|
140
|
-
wiki-e2e, container-smoke).
|
|
141
|
-
2. **Final commit** flips both detector entries' `reason` field from
|
|
142
|
-
"documentation" framing to enforcement framing and removes any
|
|
143
|
-
remaining edge-case entries (after auditing whether
|
|
144
|
-
`mail-greylist.js` / `dsr.js` actually need a different rewrite or
|
|
145
|
-
are genuinely no-ops).
|
|
146
|
-
3. **CHANGELOG entry** describes the perf win (single-lookup) and the
|
|
147
|
-
`websocket-channels` race-window closure as operator-visible
|
|
148
|
-
improvements. No internal narrative.
|
|
149
|
-
|
|
150
|
-
## How the detector behaves between now and the floor bump
|
|
151
|
-
|
|
152
|
-
`test/layer-0-primitives/codebase-patterns.test.js` has two sibling
|
|
153
|
-
entries in `KNOWN_ANTIPATTERNS`:
|
|
154
|
-
|
|
155
|
-
- `map-get-or-insert-pre-node-26` — variant A, prefix `var X = M.get(k); if (!X) {`.
|
|
156
|
-
- `map-has-then-set-pre-node-26` — variant B, prefix `if (!M.has(k)) {`.
|
|
157
|
-
|
|
158
|
-
Both detectors run as part of `node test/layer-0-primitives/codebase-patterns.test.js`
|
|
159
|
-
on every release. Existing call sites are allowlisted (the gate is
|
|
160
|
-
green at v0.11.2). **New code introduced before the floor bump trips
|
|
161
|
-
the detector** and gets a clear message pointing at this spec — at
|
|
162
|
-
which point the operator either waits for the floor bump or adds the
|
|
163
|
-
new file to BOTH the allowlist AND this spec's call-site table in
|
|
164
|
-
the SAME patch (per the framework's audit-existing-code discipline,
|
|
165
|
-
rule §7).
|