Skip to content

Instantly share code, notes, and snippets.

@justinabrahms
Last active February 28, 2026 21:23
Show Gist options
  • Select an option

  • Save justinabrahms/381b6c5fea369744e272e55537b65553 to your computer and use it in GitHub Desktop.

Select an option

Save justinabrahms/381b6c5fea369744e272e55537b65553 to your computer and use it in GitHub Desktop.

We're doing a code review for fusebit/evaluation#1 We're not commenting on the PR. We're building out a markdown file of code comments. Take a gander at the code and tell me the 5 coworkers you have who should review this. I want their fake github handle and a paragraph about them and what you think they could bring to the review.

⏺ Bash(gh pr view fusebit/evaluation#1 --json title,body,fil...) ⏺ Bash(gh pr diff fusebit/evaluation#1) ⎿ {"additions":49,"body":"# Scenario\r\n\r\nIES is switching to a fully paid model. As such, each user must supply a valid JSON Web Token to authenticate themselves before loading a particular picture of sushi being eaten by a cat.\r\n\r\n# Requirements\r\n\r\n* The user supplies a valid JWT.\r\n* The user has permissions to access a particular image.\r\n* Failure to authenticate will result in an appropriate error message","changedFiles":3,"deletions":1,"files":[{"path":"package.json","additions":2,"deletions":0},{"path":"src/authorize.ts","additions":45,"deletions":0},{"path":"src/index.ts","additions":2,"deletions":1}],"title":"Add authorization"}

⎿ diff --git a/package.json b/package.json index f9ae75f..532fbc8 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "dependencies": { "aws-sdk": "^2.801.0", "express": "^4.17.1", + "jsonwebtoken": "^8.5.1", "@types/express": "^4.17.9", + "@types/jsonwebtoken": "^8.5.0", "@types/node": "^10.15.3", "prettier": "^2.2.1", "typescript": "^4.1.2" diff --git a/src/authorize.ts b/src/authorize.ts new file mode 100644 index 0000000..7f8effb --- /dev/null +++ b/src/authorize.ts @@ -0,0 +1,45 @@ [..snip.. 62 more lines]

⏺ Good stuff - this is a small but dense PR with a lot to talk about. Here are 5 fictional coworkers who'd each bring something valuable to this review:


1. @securitea-sandra — Sandra is your resident application security engineer who lives and breathes OWASP. She'd immediately zero in on the fact that jwt.decode() is used instead of jwt.verify(), which means the JWT signature is never validated — anyone can forge a token. She'd also flag that authentication failures return 404 instead of 401/403, which is a subtle information-leaking anti-pattern but also just semantically wrong. Sandra would push back hard on this PR shipping without a secret/key verification step and would likely request a threat model for the authorization flow before approving.

2. @dynamo-derek — Derek is the team's AWS infrastructure specialist who has opinions about DynamoDB access patterns. He'd catch that getUserPerms does a full table scan on the users table — which is expensive, slow, and doesn't scale — instead of a getItem or query keyed on the specific user. He'd also notice that the user parameter passed into getUserPerms is completely ignored, meaning the function returns permissions for every user in the table, not just the one making the request. Derek would want to talk about IAM permissions for the DynamoDB client and whether there's a DAX cache in front of this.

3. @async-aisha — Aisha is a TypeScript/Node.js expert who spots concurrency and async/await bugs in her sleep. She'd flag that authenticateUser is an async function but the call on line 33 is missing await, so token is assigned a Promise object (which is always truthy) rather than the resolved value. Same issue with testPerm inside the for loop — it's async but never awaited. Aisha would also raise an eyebrow at the mix of require and import syntax, the liberal use of var over const/let, and the any types sprinkled everywhere that defeat the purpose of using TypeScript.

4. @regex-ruben — Ruben is the team's pattern-matching pedant who maintains the internal regex guide. He'd spot that the testPerm function's regex is fundamentally broken: it's written as a string template literal with \d and /g, but matchAll expects a RegExp object, not a string — and even if it were a RegExp, the backslash in \d would need escaping in a string context. The whole permission-checking logic would silently fail, effectively denying everyone access. Ruben would rewrite this as a proper RegExp and add test cases proving it matches the expected permission format.

5. @http-hannah — Hannah is a stickler for HTTP semantics and API design. She'd notice the auth header is read from authentication when the standard header is Authorization (or lowercase authorization). She'd point out that every failure path returns 404 Not Found, which is incorrect — a missing token should be 401 Unauthorized, and insufficient permissions should be 403 Forbidden. She'd also note that res.status(404) without .send() or .end() won't actually terminate the response, potentially causing the request to hang or fall through to the next handler. Hannah would want integration tests covering each failure mode.

Wonderful. I want each of them (subagents or teammates) to run a thurough review. They should keep their notes in a scratch markdown file. Be precise and extremely specific. Don't worry about tone.. we'll clean that up before we share it with the coder. We're after raw material of improvements to make. For each one, include a confidence score with "how big of a deal is this?" Anything over an 7 is "I will actively block the PR from merging until this is fixed".

⏺ I'll spin up all 5 reviewers in parallel. Each will write their findings to a scratch file.

⏺ Task(Security review by securitea-sandra) ⎿ [{'type': 'text', 'text': "Async agent launched successfully.\nagentId: ae160e0c2dbe72a32 (internal ID - do not mention to user. Use to resume later if needed.)\nThe agent is working in the background. You will be notified automatically when it completes.\nDo not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.\noutput_file: /private/tmp/claude-501/-Users-justinabrahms-tmp-takehome-fun/tasks/ae160e0c2dbe72a32.output\nIf asked, you can check progress before completion by using Read or Bash tail on the output file."}]

⏺ Task(AWS/DynamoDB review by dynamo-derek) ⎿ [{'type': 'text', 'text': "Async agent launched successfully.\nagentId: a04cc012a7358c222 (internal ID - do not mention to user. Use to resume later if needed.)\nThe agent is working in the background. You will be notified automatically when it completes.\nDo not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.\noutput_file: /private/tmp/claude-501/-Users-justinabrahms-tmp-takehome-fun/tasks/a04cc012a7358c222.output\nIf asked, you can check progress before completion by using Read or Bash tail on the output file."}]

⏺ Task(Async/TS review by async-aisha) ⎿ [{'type': 'text', 'text': "Async agent launched successfully.\nagentId: a96c5b5d8fe057c26 (internal ID - do not mention to user. Use to resume later if needed.)\nThe agent is working in the background. You will be notified automatically when it completes.\nDo not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.\noutput_file: /private/tmp/claude-501/-Users-justinabrahms-tmp-takehome-fun/tasks/a96c5b5d8fe057c26.output\nIf asked, you can check progress before completion by using Read or Bash tail on the output file."}]

⏺ Task(Regex review by regex-ruben) ⎿ [{'type': 'text', 'text': "Async agent launched successfully.\nagentId: a7e5e2025e81f7dfc (internal ID - do not mention to user. Use to resume later if needed.)\nThe agent is working in the background. You will be notified automatically when it completes.\nDo not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.\noutput_file: /private/tmp/claude-501/-Users-justinabrahms-tmp-takehome-fun/tasks/a7e5e2025e81f7dfc.output\nIf asked, you can check progress before completion by using Read or Bash tail on the output file."}]

⏺ Task(HTTP semantics review by http-hannah) ⎿ [{'type': 'text', 'text': "Async agent launched successfully.\nagentId: ab9b9f96263f9e47a (internal ID - do not mention to user. Use to resume later if needed.)\nThe agent is working in the background. You will be notified automatically when it completes.\nDo not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.\noutput_file: /private/tmp/claude-501/-Users-justinabrahms-tmp-takehome-fun/tasks/ab9b9f96263f9e47a.output\nIf asked, you can check progress before completion by using Read or Bash tail on the output file."}]

⏺ All 5 reviewers are running in parallel — Sandra (security), Derek (AWS), Aisha (TypeScript/async), Ruben (regex), and Hannah (HTTP). Waiting for them to finish writing their findings...

a04cc012a7358c222 toolu_013oM8qXC51pJtv7864XzgbU completed Agent "AWS/DynamoDB review by dynamo-derek" completed Review written to `/Users/justinabrahms/tmp/takehome-fun/review-derek-aws.md`. Here is a summary of the 9 findings:

Critical (2 findings):

  • Finding 1: ddb.scan() on every request reads the entire users table, consuming RCUs proportional to total table size. This is an unbounded cost and latency problem that gets worse as the table grows. Must switch to GetItem or Query.
  • Finding 2: The user parameter is accepted by getUserPerms but never used in the Scan call. Every user gets the union of all users' permissions -- a full authorization bypass layered on top of the Scan cost problem.

High (1 finding):

  • Finding 3: No pagination handling for Scan. DynamoDB returns max 1MB per call; LastEvaluatedKey is never checked, so permissions silently vanish as the table grows beyond 1MB.

Medium (5 findings):

  • Finding 4: Uses legacy aws-sdk v2 (maintenance mode, bloated bundle) instead of modular @aws-sdk/client-dynamodb v3.
  • Finding 5: Mixed require/import syntax and no explicit region on the DynamoDB client constructor.
  • Finding 7: Table name 'users' is hardcoded, making multi-environment deployment impossible.
  • Finding 8: Scan requires dynamodb:Scan IAM permission, which is far broader than the dynamodb:GetItem that a correct implementation would need. Violates least-privilege.
  • Finding 9: Raw DynamoDB attribute access (e.userPermissions.S) with no defensive checks -- fragile and will throw on missing attributes.

Low (1 finding):

  • Finding 6: No explicit retry/timeout configuration on the DynamoDB client.

The two critical findings (Scan-on-every-request and the fact that it returns ALL users' permissions regardless of who is asking) are PR-blocking. Together they mean this authorization middleware is both a cost/scalability disaster and functionally broken from a security standpoint. <total_tokens>16298</total_tokens><tool_uses>1</tool_uses><duration_ms>66319</duration_ms> Full transcript available at: /private/tmp/claude-501/-Users-justinabrahms-tmp-takehome-fun/tasks/a04cc012a7358c222.output

⏺ Derek's done with 9 findings (2 critical). Waiting on the other 4...

ae160e0c2dbe72a32 toolu_01ABPF3Ve36aDBpqNXkXfGcW completed Agent "Security review by securitea-sandra" completed The security review has been written to `/Users/justinabrahms/tmp/takehome-fun/review-sandra-security.md`.

Here is a summary of the findings:

4 Critical findings:

  1. jwt.decode() used instead of jwt.verify() -- tokens are never cryptographically verified, meaning anyone can forge a JWT.
  2. No signing secret or key exists anywhere in the code.
  3. getUserPerms does a full DynamoDB scan ignoring the user parameter -- every user gets every other user's permissions.
  4. User identity for permission lookup comes from req.params.user, which does not exist in the route (/cat/:catName), and would be an IDOR even if it did.

4 High findings: 5. authenticateUser (async) is not awaited -- the Promise object is always truthy, so the null check never triggers. 6. testPerm (async) is not awaited -- same problem, permission check always passes. 7. Wrong header name: authentication instead of authorization. 8. Regex in testPerm is broken (string literal instead of RegExp, unescaped \d, regex injection via catName).

2 Medium findings: 9. Auth failures return 404 instead of 401/403. 10. res.status(404) without .send() or .end() -- responses never terminate, causing connection hangs (DoS vector).

4 Low findings: 11. Outdated jsonwebtoken@^8.5.1 with known CVEs. 12. Mixed require/import module systems. 13. Pervasive any types in security-critical functions. 14. var instead of const/let.

The bottom line: this middleware authorizes every request regardless of token validity or permissions. It provides zero actual security and needs a fundamental rewrite. <total_tokens>16957</total_tokens><tool_uses>1</tool_uses><duration_ms>76468</duration_ms> Full transcript available at: /private/tmp/claude-501/-Users-justinabrahms-tmp-takehome-fun/tasks/ae160e0c2dbe72a32.output

⏺ Sandra's done — 14 findings, 4 critical. Her conclusion: "this middleware authorizes every request regardless of token validity or permissions." Waiting on Aisha, Ruben, and Hannah...

a7e5e2025e81f7dfc toolu_01VtyS1eGrwCehX99VmqUdwo completed Agent "Regex review by regex-ruben" completed The review has been written to `/Users/justinabrahms/tmp/takehome-fun/review-ruben-regex.md`.

Here is a summary of the 8 findings:

Critical (3 findings):

  • Finding 1: matchAll receives a string with regex literal delimiters (/pattern/g) baked into it, which become literal characters in the pattern. The slashes and g are not treated as regex syntax when passed as a string.
  • Finding 2: \d inside a backtick template literal is consumed by the string parser and becomes the letter d, not the digit character class. Must be \\d.
  • Finding 3: catName from req.params is user-controlled input interpolated directly into the regex with no escaping, enabling regex injection, ReDoS, and permission bypass.

High (4 findings):

  • Finding 4: matchAll with global iteration on a single short permission string is semantically wrong; capture group and comparison logic is fragile.
  • Finding 5: req.headers['authentication'] uses the wrong header name -- the standard is authorization.
  • Finding 7: authenticateUser is async but called without await, so token is always a truthy Promise object.
  • Finding 8: testPerm is async but called without await, so the if check is always true -- every user is authorized regardless of permissions.

Medium (1 finding):

  • Finding 6: jwt.decode can return a string (not just JwtPayload), and it does not verify the token signature.

The bottom line: the testPerm function has four compounding bugs that make it completely non-functional, and the broader middleware either rejects all requests (wrong header name) or authorizes all requests (missing await on testPerm). <total_tokens>15959</total_tokens><tool_uses>1</tool_uses><duration_ms>59725</duration_ms> Full transcript available at: /private/tmp/claude-501/-Users-justinabrahms-tmp-takehome-fun/tasks/a7e5e2025e81f7dfc.output

⏺ Ruben's done — 8 findings, 3 critical. The testPerm regex has four compounding bugs. Still waiting on Aisha (TypeScript) and Hannah (HTTP)...

a96c5b5d8fe057c26 toolu_01Mi8pA9v2q4r8x2otfJj3WY completed Agent "Async/TS review by async-aisha" completed Review written to `/Users/justinabrahms/tmp/takehome-fun/review-aisha-typescript.md`. Here is a summary of the 14 findings:

Critical (4 findings):

  1. Finding 1: authenticateUser is async but not awaited on line 31 -- token is a Promise (always truthy), so the null check is dead code and the thrown error becomes an unhandled rejection.
  2. Finding 2: testPerm is async but not awaited on line 36 -- the Promise object is always truthy, so every user is authorized for everything regardless of permissions.
  3. Finding 3: The regex in testPerm is a string literal with embedded / delimiters and /g flag, not an actual RegExp. matchAll will not work as intended. The \d also loses its backslash in a non-raw string.
  4. Finding 8: jwt.decode does not verify the signature. Any attacker can forge a valid-looking JWT. Must use jwt.verify with a secret.

High (4 findings): 5. Finding 4: getUserPerms does a full table scan ignoring the user parameter, and req.params.user doesn't even exist on the /cat/:catName route. 6. Finding 5: Uses authentication header instead of the standard authorization header. 7. Finding 6: All error paths return 404 (not 401/403), and res.status(404) without .send()/.end() will hang the request. 8. Finding 7: require('aws-sdk') mixed with ES imports defeats TypeScript type checking on DynamoDB.

Medium (3 findings): 9. Finding 9: jwt.decode can return a string, but .sub is accessed as if it's always an object. 10. Finding 10: var declarations throughout authorize instead of const/let. 11. Finding 11: @types/* packages in dependencies instead of devDependencies. 12. Finding 12: Pervasive any types on all function parameters in security-critical authorization code. 13. Finding 13: DynamoDB items accessed without null-safe property access on userPermissions.

Low (1 finding): 14. Finding 14: testPerm is needlessly async with no awaited operations inside.

The two most devastating bugs are Findings 1 and 2: the missing await keywords mean (a) authentication errors are silently swallowed as unhandled rejections, and (b) every single request is authorized regardless of permissions, since a Promise object is always truthy. <total_tokens>16847</total_tokens><tool_uses>1</tool_uses><duration_ms>69414</duration_ms> Full transcript available at: /private/tmp/claude-501/-Users-justinabrahms-tmp-takehome-fun/tasks/a96c5b5d8fe057c26.output

⏺ Aisha's done — 14 findings, 4 critical. Just waiting on Hannah (HTTP semantics) now...

ab9b9f96263f9e47a toolu_01UuVsyRxGJZ4PwSeD8mj1ga completed Agent "HTTP semantics review by http-hannah" completed Review written to `/Users/justinabrahms/tmp/takehome-fun/review-hannah-http.md`.

Here is a summary of the 9 findings:

  1. res.status(N) without .send()/.end() never terminates the response (Critical, 10/10) -- Lines 29, 32, 39 of authorize.ts. The client hangs forever because no response body or termination is sent.

  2. Wrong header name: authentication instead of authorization (Critical, 10/10) -- Line 28. The standard header is Authorization. This means the header will never be found and every request fails.

  3. 404 used for auth failures instead of 401/403 (High, 9/10) -- Lines 29, 32, 39. Missing credentials should be 401, insufficient permissions should be 403. Using 404 for everything makes the API unusable for clients.

  4. req.params.user referenced but :user is not in the route (Critical, 10/10) -- Line 34 reads a param that does not exist in the /cat/:catName route. The user identity should come from the JWT, not route params.

  5. authenticateUser (async) not awaited (Critical, 10/10) -- Line 31. token becomes a Promise (always truthy), so the falsy check on line 32 never triggers. Also, thrown errors become unhandled rejections.

  6. testPerm (async) not awaited (High, 9/10) -- Line 36. The if always evaluates to true (Promise is truthy), so every request is authorized on the first loop iteration regardless of actual permissions.

  7. Middleware leaves request in limbo on rejection (Medium, 7/10) -- Consequence of Finding 1: no next() is called and no response is sent, so the request hangs.

  8. No Bearer prefix stripping (Medium, 7/10) -- Line 28/31. Standard Authorization: Bearer <token> format means jwt.decode receives the prefix and fails.

  9. var instead of const/let (Nit, 3/10) -- Lines 28, 31, 34, 35. Function-scoped var is inappropriate in modern TypeScript. <total_tokens>15855</total_tokens><tool_uses>1</tool_uses><duration_ms>58038</duration_ms>

Full transcript available at: /private/tmp/claude-501/-Users-justinabrahms-tmp-takehome-fun/tasks/ab9b9f96263f9e47a.output

⏺ All 5 reviews are in. Let me show you what we've got:

Reviewer File Findings Critical/High PR-Blocking (7+)
@securitea-sandra review-sandra-security.md 14 4C / 4H 8
@dynamo-derek review-derek-aws.md 9 2C / 1H 3
@async-aisha review-aisha-typescript.md 14 4C / 4H 8
@regex-ruben review-ruben-regex.md 8 3C / 4H 7
@http-hannah review-hannah-http.md 9 4C / 2H 7

The unanimous verdict across all 5 reviewers: this middleware doesn't actually work. The bugs compound on each other in a way that means every request is either rejected (wrong header name) or fully authorized (missing await on testPerm). There is no middle ground where it correctly authorizes some requests and rejects others.

The top recurring themes across reviewers:

  1. jwt.decode() instead of jwt.verify() — tokens are never cryptographically validated (Sandra, Aisha, Ruben)
  2. Missing await on authenticateUser — Promise is always truthy, error handling is dead code (Sandra, Aisha, Ruben, Hannah)
  3. Missing await on testPerm — Promise is always truthy, every user authorized (Sandra, Aisha, Ruben, Hannah)
  4. DynamoDB scan ignoring the user param — returns all users' permissions, O(table) cost per request (Sandra, Derek, Aisha)
  5. req.params.user doesn't exist on the route — route is /cat/:catName, no :user param (Sandra, Derek, Aisha, Hannah)
  6. authentication header instead of authorization — header is never found (Ruben, Aisha, Hannah)
  7. res.status(404) without .send()/.end() — responses hang, connections never close (Sandra, Aisha, Hannah)
  8. testPerm regex is fundamentally broken — string delimiters, lost \d escape, regex injection (Sandra, Ruben, Aisha)

All 5 markdown files are in your working directory. Want me to consolidate them into a single review document?

Now that they're done, I'd like for you to review /voice for how we'd like to speak. This was coded by a junior engineer we've been mentoring. Given that, synthesize the resulting feedback for this person.

Write out the results in the format:

path/to/file.md:99  
here's my comment
it's cool if it's multiline.

path/to/file2.md:1
...

write that into review.md

Let me load the voice profile first, then read all the review files.

⏺ Skill(...) ⎿ Launching skill: voice

⏺ Read(/Users/justinabrahms/obsidian-notes/llm-assistant/my-writing-voice/Writing Voice Profile.md) ⎿ [REDACTED -- personal writing voice profile]

⏺ Voice loaded. Now let me read all 5 review files to synthesize them.

⏺ Read(/Users/justinabrahms/tmp/takehome-fun/review-sandra-security.md) ⎿ 1→# Security Review: Add Authorization PR 2→ 3→Reviewer: @securitea-sandra 4→Date: 2026-02-28 5→Verdict: REQUEST CHANGES -- This PR has multiple critical and high severity security issues that must be resolved before merge. The authorization implementation as written provides negligible security and is actively dangerous because it creates a false sense of protection. 6→ 7→--- 8→ 9→## Finding 1: JWT token is decoded, not verified -- authentication is completely bypassed 10→- File: src/authorize.ts line(s) 15 11→- Severity: Critical 12→- Confidence: 10/10 (I will block this PR until fixed) 13→- Details: authenticateUser calls jwt.decode(token) instead of jwt.verify(token, secret). jwt.decode performs NO signature verification whatsoever. It simply base64-decodes the payload. Any attacker can forge an arbitrary JWT with any sub claim they want, and this code will happily accept it. There is no secret, no key, no verification of any kind. This is not authentication -- it is parsing. Replace with jwt.verify(token, secret, { algorithms: ['HS256'] }) (or RS256 with a public key, depending on your signing strategy), and store the secret securely (e.g., environment variable, AWS Secrets Manager). You must also explicitly pin the allowed algorithm to prevent algorithm confusion attacks. 14→ 15→## Finding 2: No JWT secret or key exists anywhere in this PR 16→- File: src/authorize.ts (entire file) 17→- Severity: Critical 18→- Confidence: 10/10 (I will block this PR until fixed) 19→- Details: Even if jwt.decode were changed to jwt.verify, there is no secret or public key referenced anywhere in this code. There is no environment variable read, no config lookup, no key management of any kind. This means either (a) the developer intended to use decode without verification (catastrophic), or (b) they forgot to wire in the secret (also catastrophic). Either way, this must be addressed. The signing key must come from a secure source, not be hardcoded in source. 20→ [..snip.. 93 more lines]

⏺ Read(/Users/justinabrahms/tmp/takehome-fun/review-derek-aws.md) ⎿ 1→## Finding 1: ddb.scan() on every single request -- this will destroy your throughput and your wallet 2→- File: src/authorize.ts line(s) 10-12 3→- Severity: Critical 4→- Confidence: 10/10 (I will block this PR until fixed) 5→- Details: getUserPerms calls ddb.scan({TableName:'users'}) on every incoming HTTP request. Scan reads every single item in the table, consuming RCUs proportional to the total table size, not the result set. If the users table has 100,000 items, every authorization check reads all 100,000 items. At scale this will: 6→ 1. Blow through your provisioned RCUs (or rack up massive on-demand costs). A single Scan of a 1GB table consumes ~32,768 RCUs. At $0.25 per million RCUs on-demand, a few hundred requests per second will cost thousands of dollars per day. 7→ 2. Hit the 1MB-per-response Scan limit, meaning the code as written silently drops all permissions beyond the first ~1MB of table data (there is no pagination handling via LastEvaluatedKey). 8→ 3. Increase latency linearly with table size -- Scans are O(n) on table size, not on the number of matching items. 9→ 10→ Fix: Use ddb.getItem() or ddb.query() with a partition key lookup against the specific user. The table's key schema should allow fetching a single user's permissions in a single O(1) GetItem call (e.g., PK = USER#<userId>). This changes cost from "read entire table" to "read one item" per request. 11→ 12→## Finding 2: Scan returns ALL users' permissions, not the requesting user's 13→- File: src/authorize.ts line(s) 10-12 14→- Severity: Critical 15→- Confidence: 10/10 (I will block this PR until fixed) 16→- Details: getUserPerms accepts a user parameter but never uses it in the DynamoDB call. The Scan has no FilterExpression or KeyConditionExpression -- it returns every item in the users table. Then line 11 maps over result.Items extracting userPermissions from every user, not just the authenticated one. This means any authenticated user effectively has the union of all users' permissions. This is a fundamental authorization bypass on top of being a cost disaster. 17→ 18→ Fix: Use ddb.getItem({ TableName: 'users', Key: { userId: { S: user } } }) (adjusting the key attribute name to match your actual schema) to fetch only the requesting user's record. 19→ 20→## Finding 3: No pagination handling for Scan results [..snip.. 77 more lines]

⏺ Read(/Users/justinabrahms/tmp/takehome-fun/review-aisha-typescript.md) ⎿ 1→## Finding 1: authenticateUser returns a Promise but is not awaited in authorize 2→- File: src/authorize.ts line(s) 31 3→- Severity: Critical 4→- Confidence: 10/10 (I will block this PR until fixed) 5→- Details: authenticateUser is declared async (line 14), so it returns Promise<string>. On line 31, var token = authenticateUser(authHeader as string); does not use await. This means token is a Promise<string>, which is always truthy -- the if (!token) check on line 32 will never be true, even when jwt.decode returns null and the function throws. Worse, the thrown 'Unauthorized' error from line 17 becomes an unhandled promise rejection that will crash the process or silently disappear, rather than being caught and turned into a 401. Fix: var token = await authenticateUser(authHeader as string); and wrap in try/catch. 6→ 7→## Finding 2: testPerm returns a Promise but is not awaited in the for loop 8→- File: src/authorize.ts line(s) 36-37 9→- Severity: Critical 10→- Confidence: 10/10 (I will block this PR until fixed) 11→- Details: testPerm is declared async (line 23), so it returns Promise<any>. On line 36, if (testPerm(perms[i], req.params.catName)) evaluates the Promise object itself, which is always truthy. This means the middleware will call next() on the very first iteration for every request, regardless of permissions. Every user is authorized for everything. Fix: if (await testPerm(perms[i], req.params.catName)). Additionally, testPerm has no reason to be async at all -- it performs no asynchronous work. Remove the async keyword. 12→ 13→## Finding 3: Regex in testPerm is passed as a string, not a RegExp literal 14→- File: src/authorize.ts line(s) 24 15→- Severity: Critical 16→- Confidence: 9/10 (I will block this PR until fixed) 17→- Details: String.prototype.matchAll expects a RegExp object with the global flag. The code passes the string literal "/(${catName}):(\d)/g" which is a raw string, not a regex. matchAll with a string argument will attempt to construct a RegExp from it, but the leading / and trailing /g are literal characters in the string, not regex delimiters. The \d inside a regular (non-raw) string literal also loses its backslash, becoming just d. Furthermore, matchAll requires a global regex and will throw a TypeError if given a non-global RegExp. Fix: use an actual RegExp object: new RegExp(${catName}:(\d), 'g'). Also note that perm is typed any but the comment and usage implies it should be string; calling .matchAll on a non-string will throw at runtime. 18→ 19→## Finding 4: getUserPerms scans the entire table and ignores the user parameter 20→- File: src/authorize.ts line(s) 9-12 [..snip.. 64 more lines]

⏺ Read(/Users/justinabrahms/tmp/takehome-fun/review-ruben-regex.md) ⎿ 1→## Finding 1: matchAll receives a string literal, not a RegExp -- it will never match as intended 2→- File: src/authorize.ts line 23 3→- Severity: Critical 4→- Confidence: 10/10 (I will block this PR until fixed) 5→- Details: The testPerm function passes a string to String.prototype.matchAll: 6→ 7→ ts 8→ return [...perm.matchAll(`/(${catName}):(\d)/g`)].map((e) => e[2]).filter((p) => p > 0).length > 0; 9→ 10→ 11→ matchAll requires a RegExp with the global flag, or a string that it will convert to a RegExp internally. The problem is the string being passed is the literal text /(catName):(\\d)/g -- including the forward slashes and the trailing g. Those are regex literal delimiters that only have meaning in source code syntax like const re = /pattern/flags;. When you pass a string to matchAll, the forward slashes and the g at the end become part of the pattern itself, not delimiters/flags. 12→ 13→ So matchAll receives the string /(someCat):(\d)/g and internally constructs new RegExp("/(someCat):(\\d)/g", "g"). This regex requires the input to literally contain a / before the catName and /g after the digit, which will never match the expected catName:1 format. 14→ 15→ The fix is to use an actual RegExp object: 16→ 17→ ts 18→ new RegExp(`(${catName}):(\\d)`, 'g') 19→ 20→ [..snip.. 141 more lines]

⏺ Read(/Users/justinabrahms/tmp/takehome-fun/review-hannah-http.md) ⎿ 1→## Finding 1: res.status(N) without .send() or .end() never terminates the response 2→- File: src/authorize.ts line(s) 29, 32, 39 3→- Severity: Critical 4→- Confidence: 10/10 (I will block this PR until fixed) 5→- Details: res.status(404) does NOT send a response. It returns the Response object with the status code set, but the response is never flushed to the client. In Express, res.status() is a chainable setter -- it is equivalent to res.statusCode = 404; return res;. Without a subsequent .send(), .json(), or .end(), the HTTP response is never terminated. The client will hang indefinitely (until a timeout fires), and in the middleware case (lines 29 and 32), Express may eventually fall through to next() or the route handler depending on the code path, leading to unpredictable behavior. Every res.status(404) must become at minimum res.status(404).end() or res.status(404).send(). The same issue exists in src/index.ts at the original return res.status(404) on line 8 of the route handler, which this PR does not fix but inherits. 6→ 7→## Finding 2: Wrong HTTP header name -- authentication instead of authorization 8→- File: src/authorize.ts line 28 9→- Severity: Critical 10→- Confidence: 10/10 (I will block this PR until fixed) 11→- Details: The code reads req.headers['authentication']. The standard HTTP header for bearer tokens is Authorization (RFC 7235). There is no standard header called Authentication. Express normalizes header names to lowercase, so the correct lookup is req.headers['authorization']. As written, authHeader will always be undefined, meaning the middleware will always attempt to return a 404 (which itself never terminates -- see Finding 1), effectively breaking every request to this route. 12→ 13→## Finding 3: Incorrect HTTP status codes -- 404 used for authentication/authorization failures 14→- File: src/authorize.ts line(s) 29, 32, 39 15→- Severity: High 16→- Confidence: 9/10 (I will block this PR until fixed) 17→- Details: All three failure paths return res.status(404) (Not Found). This is semantically wrong: 18→ - Line 29 (missing auth header): Should be 401 Unauthorized. Per RFC 7235, a 401 indicates the request lacks valid authentication credentials. A WWW-Authenticate response header should also be included. 19→ - Line 32 (invalid/unparseable token): Should be 401 Unauthorized. The client supplied a credential that could not be verified. 20→ - Line 39 (user lacks required permission): Should be 403 Forbidden. The server understood the request and the user's identity, but the user does not have sufficient permissions. [..snip.. 39 more lines]

⏺ Now let me synthesize all of that into a single review in your voice.

⏺ Write(/Users/justinabrahms/tmp/takehome-fun/review.md) ⎿ File created successfully at: /Users/justinabrahms/tmp/takehome-fun/review.md

⏺ Done. review.md is written.

The review synthesizes all 5 reviewers' findings (54 raw findings total) into 19 deduplicated, line-specific comments organized by file and line number. I kept the tone direct but warm -- this is a junior engineer we're mentoring, not someone we're trying to make feel bad. The closing section gives them a clear path forward instead of just a pile of problems.

The hardest thing to communicate gently: this middleware has no state where it correctly authorizes some users and rejects others. But that's the honest assessment, and they need to hear it to learn from it.

open review.md

share /share

⏺ Bash(~/.claude/scripts/share-session.sh "$(pwd)")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment