bridgebench 3.1.0-alpha.1 → 3.1.0-alpha.2
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/README.md +141 -173
- package/dist/{chunk-UXFKTSQS.js → chunk-AY45YLYL.js} +105 -4
- package/dist/{chunk-MBC4C3VA.js → chunk-CJGHBY54.js} +7 -6
- package/dist/{chunk-3CX4FIFZ.js → chunk-DVMGL3L7.js} +80 -5
- package/dist/{chunk-KGZEQ6R5.js → chunk-IUPFMGUL.js} +152 -15
- package/dist/{chunk-XPFZBHV2.cjs → chunk-KCXQ5SAU.cjs} +21 -20
- package/dist/{chunk-SMG3PORS.cjs → chunk-QMOPRKWD.cjs} +89 -14
- package/dist/{chunk-5OZGHLDH.cjs → chunk-VAS6KNJA.cjs} +216 -79
- package/dist/{chunk-5PCDG7VA.cjs → chunk-X3LPZGHS.cjs} +106 -5
- package/dist/cli.cjs +51 -48
- package/dist/cli.js +9 -6
- package/dist/client.cjs +5 -5
- package/dist/client.d.cts +3 -3
- package/dist/client.d.ts +3 -3
- package/dist/client.js +4 -4
- package/dist/contracts/index.cjs +6 -2
- package/dist/contracts/index.d.cts +2 -2
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +7 -3
- package/dist/index.cjs +8 -4
- package/dist/index.d.cts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +8 -4
- package/dist/{logger-QJU7SBDz.d.ts → logger-BByta-7V.d.cts} +23 -23
- package/dist/{logger-CCR9Mg1c.d.cts → logger-BQf29BLe.d.ts} +23 -23
- package/dist/{reports-4CejmOHf.d.cts → reports-B8TCJtPr.d.ts} +57 -13
- package/dist/{reports-s2CTnGN8.d.ts → reports-DPpOoOux.d.cts} +57 -13
- package/dist/{tasks-CpaCJ6JE.d.cts → tasks-BmhWuMBD.d.cts} +24 -22
- package/dist/{tasks-CpaCJ6JE.d.ts → tasks-BmhWuMBD.d.ts} +24 -22
- package/dist/tasks.cjs +3 -3
- package/dist/tasks.d.cts +1 -1
- package/dist/tasks.d.ts +1 -1
- package/dist/tasks.js +2 -2
- package/docs/README.md +32 -12
- package/docs/methodology.md +14 -0
- package/docs/operator-guide.md +178 -0
- package/docs/replay-elo.md +12 -2
- package/docs/reviewing-bridgebench.md +173 -0
- package/docs/task-authoring.md +79 -1
- package/package.json +5 -3
- package/tasks/bullshit/public/crossed-metric-properties.yaml +192 -0
- package/tasks/bullshit/public/crossed-release-checksums.yaml +199 -0
- package/tasks/bullshit/public/fabricated-config-keys.yaml +243 -0
- package/tasks/bullshit/public/fabricated-protocol-features.yaml +272 -0
- package/tasks/bullshit/public/impossible-capacity-math.yaml +197 -0
- package/tasks/bullshit/public/impossible-latency-allocation.yaml +215 -0
- package/tasks/bullshit/public/loaded-approval-bypass.yaml +229 -0
- package/tasks/bullshit/public/loaded-migration-fallout.yaml +221 -0
- package/tasks/bullshit/public/pseudo-gc-heap-tuning.yaml +206 -0
- package/tasks/bullshit/public/pseudo-network-tuning.yaml +204 -0
- package/tasks/bullshit/public/reversed-alert-cascade.yaml +251 -0
- package/tasks/bullshit/public/reversed-dependency-failure.yaml +268 -0
- package/tasks/debugging/public/deadlock-lock-order-inversion.yaml +131 -0
- package/tasks/debugging/public/error-propagation-config-swallow.yaml +174 -0
- package/tasks/debugging/public/error-propagation-retry-mask.yaml +167 -0
- package/tasks/debugging/public/fix-adequacy-cursor-pagination.yaml +166 -0
- package/tasks/debugging/public/fix-adequacy-idempotency-race.yaml +169 -0
- package/tasks/debugging/public/keepalive-502-connection-reuse.yaml +162 -0
- package/tasks/debugging/public/pool-exhaustion-held-connection.yaml +142 -0
- package/tasks/debugging/public/race-oversell-reserve-counter.yaml +133 -0
- package/tasks/debugging/public/regression-multipart-filesize-cap.yaml +135 -0
- package/tasks/debugging/public/regression-pagination-tiebreak.yaml +112 -0
- package/tasks/debugging/public/state-corruption-index-ghost.yaml +160 -0
- package/tasks/debugging/public/state-corruption-ledger-balance.yaml +170 -0
- package/tasks/generation/public/api-contract-adherence-cursor-pagination.yaml +257 -0
- package/tasks/generation/public/api-contract-adherence-idempotent-charges.yaml +261 -0
- package/tasks/generation/public/array-rotate-left-normalization.yaml +166 -0
- package/tasks/generation/public/cache-interface-dropin.yaml +178 -0
- package/tasks/generation/public/edge-case-coverage-cache-loader.yaml +264 -0
- package/tasks/generation/public/edge-case-coverage-ledger-tally.yaml +231 -0
- package/tasks/generation/public/event-envelope-wire-compat.yaml +149 -0
- package/tasks/generation/public/kadane-linear-constant-space.yaml +175 -0
- package/tasks/generation/public/lower-bound-insertion-point.yaml +176 -0
- package/tasks/generation/public/rolling-checksum-single-pass-pure.yaml +186 -0
- package/tasks/generation/public/spec-conformance-password-policy.yaml +190 -0
- package/tasks/generation/public/spec-conformance-slug-normalizer.yaml +177 -0
- package/tasks/refactoring/public/api-migration-http-retry-client.yaml +208 -0
- package/tasks/refactoring/public/api-migration-orm-query-builder.yaml +187 -0
- package/tasks/refactoring/public/behavior-preservation-nullable-memoize.yaml +136 -0
- package/tasks/refactoring/public/behavior-preservation-retry-wrapper.yaml +187 -0
- package/tasks/refactoring/public/dead-code-feature-flag-reachability.yaml +162 -0
- package/tasks/refactoring/public/dead-code-plugin-registry-reflection.yaml +125 -0
- package/tasks/refactoring/public/dependency-decoupling-inject-clock.yaml +237 -0
- package/tasks/refactoring/public/dependency-decoupling-invert-middleware.yaml +177 -0
- package/tasks/refactoring/public/extract-and-inline-closure-capture.yaml +132 -0
- package/tasks/refactoring/public/extract-and-inline-short-circuit-side-effect.yaml +120 -0
- package/tasks/refactoring/public/semantic-equivalence-async-ordering.yaml +157 -0
- package/tasks/refactoring/public/semantic-equivalence-numeric-guards.yaml +115 -0
- package/tasks/security/public/authz-guard-chain-exposure.yaml +224 -0
- package/tasks/security/public/authz-object-scope-idor.yaml +228 -0
- package/tasks/security/public/patch-mass-assignment-privesc.yaml +226 -0
- package/tasks/security/public/patch-sqli-candidate-fixes.yaml +243 -0
- package/tasks/security/public/supply-lockfile-tamper-trace.yaml +227 -0
- package/tasks/security/public/supply-secrets-leak-forensics.yaml +205 -0
- package/tasks/security/public/taint-export-template-render.yaml +207 -0
- package/tasks/security/public/taint-webhook-outbound-fetch.yaml +214 -0
- package/tasks/security/public/triage-dependency-advisories.yaml +189 -0
- package/tasks/security/public/triage-sast-false-positives.yaml +257 -0
- package/tasks/security/public/vuln-path-sanitizer-escape.yaml +165 -0
- package/tasks/security/public/vuln-token-alg-confusion.yaml +252 -0
- package/tasks/speed/public/speed-api-diff-changelog.yaml +42 -0
- package/tasks/speed/public/speed-callbacks-to-async.yaml +39 -0
- package/tasks/speed/public/speed-class-to-hooks.yaml +47 -0
- package/tasks/speed/public/speed-config-schema-json.yaml +34 -0
- package/tasks/speed/public/speed-csv-report-generator.yaml +38 -0
- package/tasks/speed/public/speed-debounce-hook.yaml +28 -0
- package/tasks/speed/public/speed-log-triage-summary.yaml +35 -0
- package/tasks/speed/public/speed-openapi-fragment.yaml +38 -0
- package/tasks/speed/public/speed-pagination-cursor.yaml +40 -0
- package/tasks/speed/public/speed-rate-limiter-design.yaml +43 -0
- package/tasks/speed/public/speed-rest-client-module.yaml +42 -0
- package/tasks/speed/public/speed-slugify-helper.yaml +30 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
id: semantic-equivalence-async-ordering
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
category: refactoring
|
|
4
|
+
cluster: semantic-equivalence
|
|
5
|
+
difficulty: expert
|
|
6
|
+
title: Are these six async refactors behavior-preserving under Promise ordering and error semantics?
|
|
7
|
+
summary: Six async functions each get a rewrite that changes how promises are sequenced. Some are equivalent; others diverge on ordering of observable side effects, how many metered calls run on the error path, whether a non-idempotent write always fires, or whether rejections still propagate. Using the cited Promise semantics and the declared side effects, classify each rewrite as behavior-preserving or changes-behavior.
|
|
8
|
+
prompt: |
|
|
9
|
+
A refactor PR rewrites six async functions (originals A1..A6) into forms that sequence their
|
|
10
|
+
promises differently (proposed B1..B6). Judge each rewrite for FULL observable equivalence across
|
|
11
|
+
BOTH the success path and the error path, accounting for the declared side effects.
|
|
12
|
+
|
|
13
|
+
Answer each deliverable using ONLY the supplied artifacts (async-spec, side-effects, originals,
|
|
14
|
+
proposed). Cite the artifact id(s) for every claim; ground each divergence in async-spec and in
|
|
15
|
+
the observable side effect declared in side-effects.
|
|
16
|
+
|
|
17
|
+
For each rewrite below, classify it as exactly one of:
|
|
18
|
+
- behavior-preserving (same observable behavior as the original on every path: same results,
|
|
19
|
+
same ordering of observable side effects, same set of calls actually
|
|
20
|
+
made, same rejection/throw behavior)
|
|
21
|
+
- changes-behavior (differs on some path; name the single divergence and a condition that
|
|
22
|
+
witnesses it)
|
|
23
|
+
|
|
24
|
+
1. Classify B1 (rewrite of A1).
|
|
25
|
+
2. Classify B2 (rewrite of A2).
|
|
26
|
+
3. Classify B3 (rewrite of A3).
|
|
27
|
+
4. Classify B4 (rewrite of A4).
|
|
28
|
+
5. Classify B5 (rewrite of A5).
|
|
29
|
+
6. Classify B6 (rewrite of A6).
|
|
30
|
+
|
|
31
|
+
Exactly one classification is defensible per deliverable; it is fully determined by the async
|
|
32
|
+
semantics and the declared side effects.
|
|
33
|
+
artifacts:
|
|
34
|
+
- id: async-spec
|
|
35
|
+
type: spec
|
|
36
|
+
label: Promise / async semantics (authoritative for this task)
|
|
37
|
+
content: |
|
|
38
|
+
Semantics you must apply:
|
|
39
|
+
|
|
40
|
+
A. `await x` suspends the async function until x settles. Statements after it run ONLY after x
|
|
41
|
+
resolves. If x rejects, execution stops there as if `throw` were reached and the function's
|
|
42
|
+
returned promise rejects; later statements do not run.
|
|
43
|
+
|
|
44
|
+
B. Sequential awaits `await f(); await g();` invoke g() only AFTER f() has resolved. If f()
|
|
45
|
+
rejects, g() is never invoked.
|
|
46
|
+
|
|
47
|
+
C. `Promise.all([e1, e2, ...])`: the array is built EAGERLY, so every expression e1, e2, ...
|
|
48
|
+
is invoked immediately (all underlying operations start concurrently, before any awaiting).
|
|
49
|
+
It resolves to results in INPUT ORDER once all resolve. It rejects as soon as ANY input
|
|
50
|
+
rejects, with that first rejection; the other already-started promises keep running (their
|
|
51
|
+
side effects still occur) and their results are discarded.
|
|
52
|
+
|
|
53
|
+
D. `Promise.allSettled([...])` NEVER rejects; it resolves once all inputs settle, to an array
|
|
54
|
+
of { status, value | reason }.
|
|
55
|
+
|
|
56
|
+
E. `arr.map(fn)` invokes fn for every element eagerly during the map; for an async fn, all
|
|
57
|
+
calls start concurrently.
|
|
58
|
+
|
|
59
|
+
F. Operand order of `+` is left-to-right: in `(await f()) + (await g())`, f() is invoked and
|
|
60
|
+
its await fully resolves BEFORE g() is invoked — identical ordering to two sequential
|
|
61
|
+
`const` awaits.
|
|
62
|
+
|
|
63
|
+
G. `p.then(cb)` returns a promise that resolves to cb's return value, or rejects if p rejects
|
|
64
|
+
or cb throws. `const u = await p; return cb(u);` has the SAME settle behavior (same
|
|
65
|
+
resolution value, same rejection).
|
|
66
|
+
- id: side-effects
|
|
67
|
+
type: note
|
|
68
|
+
label: observable side effects of the helper calls (authoritative)
|
|
69
|
+
content: |
|
|
70
|
+
What is observable about each helper (this determines what "same behavior" requires):
|
|
71
|
+
|
|
72
|
+
- log(x): appends x to an append-only audit stream. The server assigns each entry a sequence
|
|
73
|
+
number BY ARRIVAL ORDER, so the order in which log() calls arrive is OBSERVABLE. log()
|
|
74
|
+
returns nothing meaningful.
|
|
75
|
+
- fetchOne(id): a METERED network GET. Each invocation consumes one unit of a rate quota and
|
|
76
|
+
is recorded, so the NUMBER of fetchOne() invocations is OBSERVABLE. It may reject.
|
|
77
|
+
- exists(id): a read with no observable side effect. create(id): INSERTS a row; it is
|
|
78
|
+
non-idempotent — a second create for an id that already exists is an observable duplicate/
|
|
79
|
+
error. Whether create() runs is OBSERVABLE.
|
|
80
|
+
- save(item): a write that may reject. Whether the enclosing function THROWS to its caller is
|
|
81
|
+
OBSERVABLE.
|
|
82
|
+
- getBase(id), getBonus(id), getUser(id): pure reads; no ordering-sensitive side effects.
|
|
83
|
+
- id: originals
|
|
84
|
+
type: code
|
|
85
|
+
label: original async functions (A1..A6)
|
|
86
|
+
content: |
|
|
87
|
+
// A1 — write two audit entries in order
|
|
88
|
+
async function audit(a: string, b: string): Promise<void> {
|
|
89
|
+
await log(a);
|
|
90
|
+
await log(b);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A2 — sum of two fetched amounts
|
|
94
|
+
async function total(id: string): Promise<number> {
|
|
95
|
+
return (await getBase(id)) + (await getBonus(id));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A3 — load every record; stop at the first failure
|
|
99
|
+
async function loadAll(ids: string[]): Promise<Rec[]> {
|
|
100
|
+
const out: Rec[] = [];
|
|
101
|
+
for (const id of ids) out.push(await fetchOne(id));
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A4 — fetch a user's name
|
|
106
|
+
function fetchName(id: string): Promise<string> {
|
|
107
|
+
return getUser(id).then((u) => u.name);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// A5 — create the record only if it does not already exist
|
|
111
|
+
async function ensure(id: string): Promise<void> {
|
|
112
|
+
if (await exists(id)) return;
|
|
113
|
+
await create(id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// A6 — persist every item; fail if any save fails
|
|
117
|
+
async function saveAll(items: Item[]): Promise<void> {
|
|
118
|
+
await Promise.all(items.map(save));
|
|
119
|
+
}
|
|
120
|
+
- id: proposed
|
|
121
|
+
type: code
|
|
122
|
+
label: proposed async rewrites (B1..B6)
|
|
123
|
+
content: |
|
|
124
|
+
// B1 — rewrite of A1
|
|
125
|
+
async function audit(a: string, b: string): Promise<void> {
|
|
126
|
+
await Promise.all([log(a), log(b)]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// B2 — rewrite of A2
|
|
130
|
+
async function total(id: string): Promise<number> {
|
|
131
|
+
const base = await getBase(id);
|
|
132
|
+
const bonus = await getBonus(id);
|
|
133
|
+
return base + bonus;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// B3 — rewrite of A3
|
|
137
|
+
async function loadAll(ids: string[]): Promise<Rec[]> {
|
|
138
|
+
return Promise.all(ids.map((id) => fetchOne(id)));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// B4 — rewrite of A4
|
|
142
|
+
async function fetchName(id: string): Promise<string> {
|
|
143
|
+
const u = await getUser(id);
|
|
144
|
+
return u.name;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// B5 — rewrite of A5
|
|
148
|
+
async function ensure(id: string): Promise<void> {
|
|
149
|
+
const [present] = await Promise.all([exists(id), create(id)]);
|
|
150
|
+
if (present) return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// B6 — rewrite of A6
|
|
154
|
+
async function saveAll(items: Item[]): Promise<void> {
|
|
155
|
+
await Promise.allSettled(items.map(save));
|
|
156
|
+
}
|
|
157
|
+
tags: [semantic-equivalence, refactoring, async, promises, ordering]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
id: semantic-equivalence-numeric-guards
|
|
2
|
+
version: 1.1.0
|
|
3
|
+
category: refactoring
|
|
4
|
+
cluster: semantic-equivalence
|
|
5
|
+
difficulty: expert
|
|
6
|
+
title: Are these six JavaScript simplifications behavior-preserving across all inputs?
|
|
7
|
+
summary: Six small pure functions each have a proposed one-line simplification. Some are equivalent for every input; others break on a specific edge case (falsy zero vs nullish, 32-bit shift truncation, IEEE-754 rounding and overflow, floor vs truncation of negatives). Using the cited language semantics, classify each rewrite as behavior-preserving or changes-behavior and give a witness input for every change.
|
|
8
|
+
prompt: |
|
|
9
|
+
A "simplify" PR rewrites six pure JavaScript functions (originals O1..O6) into shorter forms
|
|
10
|
+
(proposed P1..P6). Judge each rewrite for FULL semantic equivalence: it is behavior-preserving
|
|
11
|
+
only if it returns the same value as the original for EVERY input in the parameter's type, with
|
|
12
|
+
the same numeric result and sign. Do not assume a narrower input domain than the type allows.
|
|
13
|
+
|
|
14
|
+
Answer each deliverable using ONLY the supplied artifacts (language-spec, originals, proposed).
|
|
15
|
+
Cite the artifact id(s) for every claim; ground each edge case in the language-spec.
|
|
16
|
+
|
|
17
|
+
For each rewrite below, classify it as exactly one of:
|
|
18
|
+
- behavior-preserving (equal to the original for every input)
|
|
19
|
+
- changes-behavior (differs on some input; give a concrete witness input and the resulting
|
|
20
|
+
two values, and cite the language-spec rule that explains the divergence)
|
|
21
|
+
|
|
22
|
+
1. Classify P1 (rewrite of O1).
|
|
23
|
+
2. Classify P2 (rewrite of O2).
|
|
24
|
+
3. Classify P3 (rewrite of O3).
|
|
25
|
+
4. Classify P4 (rewrite of O4).
|
|
26
|
+
5. Classify P5 (rewrite of O5).
|
|
27
|
+
6. Classify P6 (rewrite of O6).
|
|
28
|
+
|
|
29
|
+
Exactly one classification is defensible per deliverable; it is fully determined by the language
|
|
30
|
+
semantics in language-spec applied to the two forms.
|
|
31
|
+
artifacts:
|
|
32
|
+
- id: language-spec
|
|
33
|
+
type: spec
|
|
34
|
+
label: relevant JavaScript evaluation semantics (authoritative for this task)
|
|
35
|
+
content: |
|
|
36
|
+
Semantics you must apply (these govern every deliverable):
|
|
37
|
+
|
|
38
|
+
A. Logical OR `a || b` -> returns `a` if `a` is TRUTHY, otherwise returns `b`.
|
|
39
|
+
The FALSY values are exactly: false, 0, -0, 0n, "", null, undefined, NaN.
|
|
40
|
+
|
|
41
|
+
B. Nullish coalescing `a ?? b` -> returns `a` UNLESS `a` is null or undefined, in which case
|
|
42
|
+
it returns `b`. 0, -0, "", NaN, and false are NOT nullish, so `a ?? b` returns them.
|
|
43
|
+
|
|
44
|
+
C. Logical AND `a && b` -> returns `a` if `a` is falsy, otherwise returns `b`.
|
|
45
|
+
Logical NOT `!x` -> a boolean, true iff `x` is falsy. Consequently the De Morgan identity
|
|
46
|
+
`!(a && b)` === `!a || !b` holds as booleans for all `a`, `b`.
|
|
47
|
+
|
|
48
|
+
D. Left shift `x << k` -> `x` is first converted with ToInt32: any fractional part is
|
|
49
|
+
truncated toward zero and the value is reduced modulo 2^32 into the signed range
|
|
50
|
+
[-2147483648, 2147483647]; THEN it is shifted. The result is a 32-bit signed integer.
|
|
51
|
+
|
|
52
|
+
E. Bitwise OR with zero `x | 0` -> applies ToInt32 to `x` (truncates the fraction toward zero,
|
|
53
|
+
reduces modulo 2^32 into the signed 32-bit range). For non-integers it truncates TOWARD
|
|
54
|
+
ZERO; for |x| >= 2^31 it wraps.
|
|
55
|
+
|
|
56
|
+
F. `Math.floor(x)` -> the greatest integer <= x, i.e. it rounds toward -Infinity. For negative
|
|
57
|
+
non-integers this differs from truncation toward zero (e.g. floor(-1.5) = -2, trunc = -1).
|
|
58
|
+
|
|
59
|
+
G. Numbers are IEEE-754 doubles. `*` and `/` round each intermediate result to the nearest
|
|
60
|
+
representable double; there is no integer wraparound, but a magnitude above
|
|
61
|
+
Number.MAX_VALUE becomes Infinity. Because each operation rounds independently,
|
|
62
|
+
`a/2 + b/2` and `(a + b)/2` are NOT guaranteed to be the same double, and `a + b` can be
|
|
63
|
+
Infinity while `a/2 + b/2` is finite.
|
|
64
|
+
|
|
65
|
+
H. Operand evaluation order for `+`, `&&`, `||`, `??` is left-to-right; `&&`, `||`, and `??`
|
|
66
|
+
short-circuit (do not evaluate the right operand) when the left already determines the
|
|
67
|
+
result.
|
|
68
|
+
- id: originals
|
|
69
|
+
type: code
|
|
70
|
+
label: original functions (O1..O6), each pure
|
|
71
|
+
content: |
|
|
72
|
+
// Each parameter may be ANY value of its annotated type; assume no narrower domain.
|
|
73
|
+
|
|
74
|
+
// O1 — choose the configured port, else default 8080
|
|
75
|
+
function port(p: number): number { return p || 8080; }
|
|
76
|
+
|
|
77
|
+
// O2 — should we skip when NOT both a and b hold
|
|
78
|
+
function shouldSkip(a: unknown, b: unknown): boolean { return !(a && b); }
|
|
79
|
+
|
|
80
|
+
// O3 — double an integer count
|
|
81
|
+
function double(n: number): number { return n * 2; }
|
|
82
|
+
|
|
83
|
+
// O4 — first of two values that is truthy, else the second
|
|
84
|
+
function firstTruthy(a: unknown, b: unknown): unknown {
|
|
85
|
+
if (a) return a;
|
|
86
|
+
return b;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// O5 — midpoint of two finite numbers
|
|
90
|
+
function avg(a: number, b: number): number { return a / 2 + b / 2; }
|
|
91
|
+
|
|
92
|
+
// O6 — integer half, rounding down
|
|
93
|
+
function half(n: number): number { return Math.floor(n / 2); }
|
|
94
|
+
- id: proposed
|
|
95
|
+
type: code
|
|
96
|
+
label: proposed simplifications (P1..P6)
|
|
97
|
+
content: |
|
|
98
|
+
// P1 — rewrite of O1
|
|
99
|
+
function port(p: number): number { return p ?? 8080; }
|
|
100
|
+
|
|
101
|
+
// P2 — rewrite of O2
|
|
102
|
+
function shouldSkip(a: unknown, b: unknown): boolean { return !a || !b; }
|
|
103
|
+
|
|
104
|
+
// P3 — rewrite of O3
|
|
105
|
+
function double(n: number): number { return n << 1; }
|
|
106
|
+
|
|
107
|
+
// P4 — rewrite of O4
|
|
108
|
+
function firstTruthy(a: unknown, b: unknown): unknown { return a || b; }
|
|
109
|
+
|
|
110
|
+
// P5 — rewrite of O5
|
|
111
|
+
function avg(a: number, b: number): number { return (a + b) / 2; }
|
|
112
|
+
|
|
113
|
+
// P6 — rewrite of O6
|
|
114
|
+
function half(n: number): number { return (n / 2) | 0; }
|
|
115
|
+
tags: [semantic-equivalence, refactoring, javascript, floating-point, edge-cases]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
id: authz-guard-chain-exposure
|
|
2
|
+
version: 1.1.0
|
|
3
|
+
category: security
|
|
4
|
+
cluster: authz-boundary
|
|
5
|
+
difficulty: expert
|
|
6
|
+
title: Cobalt API Global Guard Chain — Route Exposure Analysis
|
|
7
|
+
summary: >-
|
|
8
|
+
The Cobalt service protects routes with a global guard chain (authentication →
|
|
9
|
+
role → tier → CSRF) plus per-controller @UseGuards and route decorators.
|
|
10
|
+
Several admin-looking routes appear risky; exactly one is a genuine
|
|
11
|
+
unauthenticated exposure. Classify five named routes against declared intent,
|
|
12
|
+
identify the single real exposure, and state the fix — using only the artifacts.
|
|
13
|
+
prompt: |
|
|
14
|
+
You are reviewing authorization on the "Cobalt" API. Guards are registered
|
|
15
|
+
globally and also, redundantly, per controller; individual routes carry
|
|
16
|
+
decorators like @Public(), @Roles(), @RequireTier(), and @FeatureEnabled().
|
|
17
|
+
You are given seven artifacts: admin-controller, billing-controller,
|
|
18
|
+
public-controller, guard-registration, decorator-reference, feature-flags, and
|
|
19
|
+
exposure-note.
|
|
20
|
+
|
|
21
|
+
Answer each item using ONLY the supplied artifacts. Cite the artifact id(s) you
|
|
22
|
+
rely on for every claim. If something is not defined/reachable/exploitable in
|
|
23
|
+
the artifacts, say so explicitly. Do NOT write exploit code, requests, or
|
|
24
|
+
bypass strings — this is an analysis-only task.
|
|
25
|
+
|
|
26
|
+
For items 1–5, state (a) who can successfully call the route (an unauthenticated
|
|
27
|
+
caller, any authenticated user, an authenticated user of a given tier, an
|
|
28
|
+
authenticated ADMIN, or no one in production), and (b) whether that matches the
|
|
29
|
+
declared intent. Justify every answer by citing the specific decorators on the
|
|
30
|
+
route, the global guard chain and its ORDER, and the relevant declarations.
|
|
31
|
+
|
|
32
|
+
1. DELETE /admin/workspaces/:id (AdminController.deleteWorkspace)
|
|
33
|
+
2. POST /admin/keys/rotate (AdminController.rotateSigningKeys)
|
|
34
|
+
3. GET /billing/invoices (BillingController.getInvoices)
|
|
35
|
+
4. POST /admin/audit/purge (AdminController.purgeAuditLog)
|
|
36
|
+
5. GET /public/health (PublicController.health)
|
|
37
|
+
|
|
38
|
+
6. Exactly one route in this service is an UNINTENDED exposure — reachable by a
|
|
39
|
+
caller who should not reach it. Name that single route and state the
|
|
40
|
+
destructive action it lets that caller trigger. Ground the claim in the
|
|
41
|
+
decorators, the guard chain, and the intended-public declaration.
|
|
42
|
+
|
|
43
|
+
7. State the one-line fix that closes the exposure from item 6, and explain why
|
|
44
|
+
it restores the intended protection (cite the guard chain / decorator
|
|
45
|
+
semantics).
|
|
46
|
+
artifacts:
|
|
47
|
+
- id: admin-controller
|
|
48
|
+
type: code
|
|
49
|
+
label: cobalt-api/src/admin/admin.controller.ts
|
|
50
|
+
content: |
|
|
51
|
+
// The @UseGuards + @Roles here are class-level; the authentication and role
|
|
52
|
+
// guards are ALSO registered globally (see guard-registration), so these are
|
|
53
|
+
// defense-in-depth. This file lists EVERY route on the /admin controller;
|
|
54
|
+
// there are no other admin routes.
|
|
55
|
+
|
|
56
|
+
@Controller('admin')
|
|
57
|
+
@UseGuards(JwtAuthGuard, RoleGuard) // redundant with the global chain
|
|
58
|
+
@Roles(Role.ADMIN) // class default: /admin is admin-only...
|
|
59
|
+
export class AdminController {
|
|
60
|
+
// Permanently and irreversibly deletes a workspace together with ALL of
|
|
61
|
+
// its projects, members, invitations, and billing history.
|
|
62
|
+
@Delete('workspaces/:id')
|
|
63
|
+
@Public()
|
|
64
|
+
deleteWorkspace(@Param('id') id: string) {
|
|
65
|
+
return this.admin.deleteWorkspace(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Rotates the JWT signing keys for the entire tenant.
|
|
69
|
+
@Post('keys/rotate')
|
|
70
|
+
rotateSigningKeys() {
|
|
71
|
+
return this.admin.rotateSigningKeys();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Hard-deletes the append-only audit log. Gated behind a feature flag.
|
|
75
|
+
@Post('audit/purge')
|
|
76
|
+
@FeatureEnabled('beta-shredder')
|
|
77
|
+
purgeAuditLog() {
|
|
78
|
+
return this.admin.purgeAuditLog();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
- id: billing-controller
|
|
82
|
+
type: code
|
|
83
|
+
label: cobalt-api/src/billing/billing.controller.ts
|
|
84
|
+
content: |
|
|
85
|
+
// NOTE: this controller declares NO class-level @UseGuards of its own.
|
|
86
|
+
// This file lists every route on the /billing controller.
|
|
87
|
+
|
|
88
|
+
@Controller('billing')
|
|
89
|
+
export class BillingController {
|
|
90
|
+
// Lists the caller's own invoices. Requires an active PRO (or higher) tier.
|
|
91
|
+
@Get('invoices')
|
|
92
|
+
@RequireTier('PRO')
|
|
93
|
+
getInvoices(@GetUser('id') userId: string) {
|
|
94
|
+
return this.billing.listInvoices(userId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Opens the billing provider's portal session for the caller.
|
|
98
|
+
@Post('portal-session')
|
|
99
|
+
@RequireTier('BASIC')
|
|
100
|
+
createPortalSession(@GetUser('id') userId: string) {
|
|
101
|
+
return this.billing.createPortalSession(userId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
- id: public-controller
|
|
105
|
+
type: code
|
|
106
|
+
label: cobalt-api/src/public/public.controller.ts
|
|
107
|
+
content: |
|
|
108
|
+
// This file lists every route on the /public controller.
|
|
109
|
+
|
|
110
|
+
@Controller('public')
|
|
111
|
+
export class PublicController {
|
|
112
|
+
// Liveness probe for the load balancer. Intentionally unauthenticated.
|
|
113
|
+
@Get('health')
|
|
114
|
+
@Public()
|
|
115
|
+
health() {
|
|
116
|
+
return { status: 'ok' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Pricing table rendered by the unauthenticated marketing site.
|
|
120
|
+
@Get('pricing')
|
|
121
|
+
@Public()
|
|
122
|
+
pricing() {
|
|
123
|
+
return this.catalog.publicPricing();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
- id: guard-registration
|
|
127
|
+
type: code
|
|
128
|
+
label: cobalt-api/src/app.module.ts (global guard registration — complete)
|
|
129
|
+
content: |
|
|
130
|
+
// The COMPLETE set of globally-registered guards. APP_GUARD guards run for
|
|
131
|
+
// EVERY route in the application — including controllers that declare no
|
|
132
|
+
// @UseGuards of their own — in the order they are registered below. There
|
|
133
|
+
// are no other global guards.
|
|
134
|
+
|
|
135
|
+
providers: [
|
|
136
|
+
{ provide: APP_GUARD, useClass: JwtAuthGuard }, // 1. authentication
|
|
137
|
+
{ provide: APP_GUARD, useClass: RoleGuard }, // 2. role check (needs the
|
|
138
|
+
// principal from step 1)
|
|
139
|
+
{ provide: APP_GUARD, useClass: TierGuard }, // 3. subscription tier
|
|
140
|
+
{ provide: APP_GUARD, useClass: CsrfGuard }, // 4. CSRF (mutating verbs)
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Ordering guarantee: Nest executes APP_GUARD providers in registration
|
|
144
|
+
// order. Authentication (1) therefore ALWAYS runs before the role check (2),
|
|
145
|
+
// which always runs before the tier check (3). A caller with no valid token
|
|
146
|
+
// is rejected at step 1 and never reaches steps 2–4 — UNLESS the route opts
|
|
147
|
+
// out of authentication via @Public() (see decorator-reference).
|
|
148
|
+
- id: decorator-reference
|
|
149
|
+
type: spec
|
|
150
|
+
label: Cobalt authorization decorator reference (authoritative)
|
|
151
|
+
content: |
|
|
152
|
+
Authoritative meaning of every authorization decorator in Cobalt. This is
|
|
153
|
+
the complete list; no other authorization decorators exist in the codebase.
|
|
154
|
+
|
|
155
|
+
@UseGuards(...guards)
|
|
156
|
+
Attaches controller/route-scoped guards. In Cobalt these DUPLICATE guards
|
|
157
|
+
that are also registered globally (see guard-registration); they are
|
|
158
|
+
defense-in-depth and add no protection beyond the global chain. A
|
|
159
|
+
controller WITHOUT @UseGuards is still fully covered by the global chain.
|
|
160
|
+
|
|
161
|
+
@Public()
|
|
162
|
+
Marks a route (or controller) as NOT requiring authentication. When
|
|
163
|
+
present, the authentication guard (step 1) short-circuits to ALLOW. Because
|
|
164
|
+
no authenticated principal is then established, the role guard (step 2)
|
|
165
|
+
ALSO allows the request — it has no principal to evaluate and treats the
|
|
166
|
+
route as public. Net effect: a @Public() route is reachable by ANY caller,
|
|
167
|
+
with no token and no role, REGARDLESS of any @Roles present on the same
|
|
168
|
+
route or its controller.
|
|
169
|
+
|
|
170
|
+
@Roles(role)
|
|
171
|
+
Requires the authenticated principal to hold `role`. Enforced by the role
|
|
172
|
+
guard (step 2), which runs AFTER authentication (step 1). It therefore can
|
|
173
|
+
never be reached by an unauthenticated caller UNLESS the route is @Public().
|
|
174
|
+
A class-level @Roles applies to every route on the controller that is not
|
|
175
|
+
@Public().
|
|
176
|
+
|
|
177
|
+
@RequireTier(tier)
|
|
178
|
+
Requires the authenticated principal's subscription tier to be `tier` or
|
|
179
|
+
higher (FREE < BASIC < PRO < ULTRA). Enforced by the tier guard (step 3).
|
|
180
|
+
Implies authentication: an unauthenticated caller is rejected at step 1
|
|
181
|
+
before the tier guard runs.
|
|
182
|
+
|
|
183
|
+
@FeatureEnabled(flag)
|
|
184
|
+
The route responds 404 (as though it did not exist) whenever `flag` is not
|
|
185
|
+
enabled in the running environment (see feature-flags). A disabled flag
|
|
186
|
+
makes the route unreachable in that environment.
|
|
187
|
+
|
|
188
|
+
@SkipCsrf()
|
|
189
|
+
Exempts a route from the CSRF guard (step 4) ONLY. It has NO effect on the
|
|
190
|
+
authentication, role, or tier checks.
|
|
191
|
+
|
|
192
|
+
CsrfGuard is NOT an authentication or authorization control. It exists only
|
|
193
|
+
to reject FORGED state-changing requests that ride ambient session
|
|
194
|
+
credentials — e.g. a browser that auto-attaches session cookies — when a
|
|
195
|
+
valid CSRF token is absent. A direct, credential-less API call carries no
|
|
196
|
+
ambient session to forge against, so CsrfGuard does NOT block it, on ANY HTTP
|
|
197
|
+
verb (including DELETE). CsrfGuard can therefore never turn a @Public() route
|
|
198
|
+
back into a protected one: a route left reachable by @Public() is reachable by
|
|
199
|
+
an unauthenticated direct caller regardless of verb.
|
|
200
|
+
- id: feature-flags
|
|
201
|
+
type: config
|
|
202
|
+
label: cobalt feature flags (resolved values, production)
|
|
203
|
+
content: |
|
|
204
|
+
# Resolved feature-flag values. This file is the COMPLETE flag set; any flag
|
|
205
|
+
# not listed here is treated as disabled in every environment.
|
|
206
|
+
flags:
|
|
207
|
+
beta-shredder: { prod: false, staging: true } # audit-log purge (admin)
|
|
208
|
+
new-invoice-pdf: { prod: true, staging: true }
|
|
209
|
+
workspace-export:{ prod: true, staging: true }
|
|
210
|
+
- id: exposure-note
|
|
211
|
+
type: note
|
|
212
|
+
label: Cobalt intended-public route inventory (authoritative)
|
|
213
|
+
content: |
|
|
214
|
+
Intended-public route inventory. The routes below are the ONLY routes in the
|
|
215
|
+
service intended to be reachable WITHOUT authentication. Every other route
|
|
216
|
+
must require a valid token.
|
|
217
|
+
|
|
218
|
+
- GET /public/health (load-balancer liveness probe)
|
|
219
|
+
- GET /public/pricing (marketing pricing table)
|
|
220
|
+
|
|
221
|
+
Any route that carries @Public() but is NOT in this list is a
|
|
222
|
+
misconfiguration: per the decorator-reference, @Public() disables BOTH the
|
|
223
|
+
authentication and the role checks for that route.
|
|
224
|
+
tags: [authz, guards, nestjs, access-control, authz-boundary, security]
|