ReviewOS
@if (!pullRequest)

No such pull request

stacks/reportshq has no pull request numbered 36.

@else

also looking at this

stacks/reportshq

fix(auth): point the action registry at this app's Auth actions

#36
Merged glennmichael123 wants to merge fix/reportshq-auth-actions-registry into main
4 files +22 -7
@if (tab === 'conversation')
9 days ago

main has been red since 2026-09-22. Because ci.yml's deploy job has needs: [lint, typecheck, test, test-postgres, build], nothing has deployed since - production is still serving 50be0ca, the last green commit, so the auth rework in 437970e has never been live.

Seven tests were failing in both test and test-postgres. Four of them were one bug.

The bug: the app's Auth actions were shadowed by the framework defaults

storage/framework/auto-imports/actions.ts is tracked, and was last regenerated at 2dc1c68 - before 437970e added app/Actions/Auth/. So the four names that commit introduced were still mapped to the framework's own actions:

'Actions/Auth/LoginAction': '../defaults/app/Actions/Auth/LoginAction.ts',
'Actions/Auth/RegisterAction': '../defaults/app/Actions/Auth/RegisterAction.ts',

The router's string-handler resolver consults that map first and does not check whether an app file of the same name exists, so every route.post('/login', 'Actions/Auth/LoginAction') in routes/auth.ts resolved to the framework default. The entire auth rework was dead code at runtime:

Behaviour the app's action implementsWhat actually happened
10-character password policya 9-character password registered fine, 200
email lowercased before lookupUSER@x.test refused 401
email-taken pre-check, 409422 from a unique-constraint path
the app's own sign-in rate limitingnot applied at all
201 on register200

The generator already prefers app/Actions over the defaults - generatePathIndex scans path.appPath("Actions") before frameworkDefaultsDir("app/Actions") and keeps the first key it sees - which is why statushq's registry is correct. This file was simply never regenerated after the actions were added. Regenerated here on the locked 0.74.5; it changes only those four lines.

Worth knowing for next time: autoImportsAreStale() decides by mtime. A fresh CI checkout stamps every file at checkout time, so a stale committed registry is used as though it were current. The resulting failure is silent, because the name still resolves to a real action - just not yours.

The three stale assertions

All three were written against the client pre-paint guard that 437970e deleted along with resources/partials/AuthGuard.stx.

  • marketing-links scanned resources/partials/AuthGuard.stx, which no longer exists, so the test died in readFileSync rather than failing an assertion.
  • reports-surface required /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3620:2492: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx (line 3620, col 2492) Message: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' Context:  3618: 3619:

    main has been red since 2026-09-22. Because ci.yml's deploy job has needs: [lint, typecheck, test, test-postgres, build], nothing has deployed since - production is still serving 50be0ca, the last green commit, so the auth rework in 437970e has never been live.

    Seven tests were failing in both test and test-postgres. Four of them were one bug.

    The bug: the app's Auth actions were shadowed by the framework defaults

    storage/framework/auto-imports/actions.ts is tracked, and was last regenerated at 2dc1c68 - before 437970e added app/Actions/Auth/. So the four names that commit introduced were still mapped to the framework's own actions:

    'Actions/Auth/LoginAction': '../defaults/app/Actions/Auth/LoginAction.ts',
    > 3620: 'Actions/Auth/RegisterAction': '../defaults/app/Actions/Auth/RegisterAction.ts',

    The router's string-handler resolver consults that map first and does not check whether an app file of the same name exists, so every route.post('/login', 'Actions/Auth/LoginAction') in routes/auth.ts resolved to the framework default. The entire auth rework was dead code at runtime:

    Behaviour the app's action implementsWhat actually happened
    10-character password policya 9-character password registered fine, 200
    email lowercased before lookupUSER@x.test refused 401
    email-taken pre-check, 409422 from a unique-constraint path
    the app's own sign-in rate limitingnot applied at all
    201 on register200

    The generator already prefers app/Actions over the defaults - generatePathIndex scans path.appPath("Actions") before frameworkDefaultsDir("app/Actions") and keeps the first key it sees - which is why statushq's registry is correct. This file was simply never regenerated after the actions were added. Regenerated here on the locked 0.74.5; it changes only those four lines.

    Worth knowing for next time: autoImportsAreStale() decides by mtime. A fresh CI checkout stamps every file at checkout time, so a stale committed registry is used as though it were current. The resulting failure is silent, because the name still resolves to a real action - just not yours.

    The three stale assertions

    All three were written against the client pre-paint guard that 437970e deleted along with resources/partials/AuthGuard.stx.

    • marketing-links scanned resources/partials/AuthGuard.stx, which no longer exists, so the test died in readFileSync rather than failing an assertion.
    • reports-surface required /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3724:31: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx (line 3724, col 31) Message: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' Context:  3722: 3723:

      > 3724: Sign in to comment on this pull request. 3725:

      3726: @else
      and an #auth-required element on both report pages - that is, it asserted the browser guard was still there. Inverted to assert what the test's own name claims: the server reads the session, data is withheld from an unidentified visitor, and nothing gates in the browser. Matched as the directive and the element rather than the bare word, since both pages mention AuthGuard in a comment explaining its removal. Mutation-checked: re-adding /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3752:1: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx (line 3752, col 1) Message: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' Context:  3750: @if (tab === 'commits') 3751: > 3752:

      This branch carries no commits of its own.

      3753:
      storage/framework/auto-imports/actions.tsmodified+4-4
      Changes to storage/framework/auto-imports/actions.ts
      @@ -5,6 +5,10 @@
      55// resolves. Values are paths rather than import thunks on purpose: thunks
      66// would make every compilation that touches a name resolve every module.
      77export const actions = {
      8 'Actions/Auth/LoginAction': '../../../app/Actions/Auth/LoginAction.ts',
      9 'Actions/Auth/LogoutAction': '../../../app/Actions/Auth/LogoutAction.ts',
      10 'Actions/Auth/RegisterAction': '../../../app/Actions/Auth/RegisterAction.ts',
      11 'Actions/Auth/VerifyTwoFactorLoginAction': '../../../app/Actions/Auth/VerifyTwoFactorLoginAction.ts',
      812 'Actions/NotifyUser': '../../../app/Actions/NotifyUser.ts',
      913 'Actions/SendWelcomeEmail': '../../../app/Actions/SendWelcomeEmail.ts',
      1014 'Actions/AI/AskAction': '../defaults/app/Actions/AI/AskAction.ts',
      @@ -17,13 +21,10 @@ export const actions = {
      1721 'Actions/Auth/GenerateRegistrationAction': '../defaults/app/Actions/Auth/GenerateRegistrationAction.ts',
      1822 'Actions/Auth/GenerateTwoFactorSecretAction': '../defaults/app/Actions/Auth/GenerateTwoFactorSecretAction.ts',
      1923 'Actions/Auth/ListTokensAction': '../defaults/app/Actions/Auth/ListTokensAction.ts',
      20 'Actions/Auth/LoginAction': '../defaults/app/Actions/Auth/LoginAction.ts',
      21 'Actions/Auth/LogoutAction': '../defaults/app/Actions/Auth/LogoutAction.ts',
      2224 'Actions/Auth/LogoutAllAction': '../defaults/app/Actions/Auth/LogoutAllAction.ts',
      2325 'Actions/Auth/MagicLinkConsumeAction': '../defaults/app/Actions/Auth/MagicLinkConsumeAction.ts',
      2426 'Actions/Auth/MagicLinkSendAction': '../defaults/app/Actions/Auth/MagicLinkSendAction.ts',
      2527 'Actions/Auth/RefreshTokenAction': '../defaults/app/Actions/Auth/RefreshTokenAction.ts',
      26 'Actions/Auth/RegisterAction': '../defaults/app/Actions/Auth/RegisterAction.ts',
      2728 'Actions/Auth/ResendVerificationAction': '../defaults/app/Actions/Auth/ResendVerificationAction.ts',
      2829 'Actions/Auth/RevokeTokenAction': '../defaults/app/Actions/Auth/RevokeTokenAction.ts',
      2930 'Actions/Auth/SocialCallbackAction': '../defaults/app/Actions/Auth/SocialCallbackAction.ts',
      @@ -32,7 +33,6 @@ export const actions = {
      3233 'Actions/Auth/VerifyAuthenticationAction': '../defaults/app/Actions/Auth/VerifyAuthenticationAction.ts',
      3334 'Actions/Auth/VerifyEmailAction': '../defaults/app/Actions/Auth/VerifyEmailAction.ts',
      3435 'Actions/Auth/VerifyRegistrationAction': '../defaults/app/Actions/Auth/VerifyRegistrationAction.ts',
      35 'Actions/Auth/VerifyTwoFactorLoginAction': '../defaults/app/Actions/Auth/VerifyTwoFactorLoginAction.ts',
      3636 'Actions/Auth/token-request': '../defaults/app/Actions/Auth/token-request.ts',
      3737 'Actions/Billable/CheckoutAction': '../defaults/app/Actions/Billable/CheckoutAction.ts',
      3838 'Actions/Blog/BlogDestroyAction': '../defaults/app/Actions/Blog/BlogDestroyAction.ts',
      tests/feature/marketing-links.test.tsmodified+4-1
      Changes to tests/feature/marketing-links.test.ts
      @@ -45,7 +45,10 @@ function publicTemplates(): string[] {
      4545 join(views, 'forgot.stx'),
      4646 join(views, 'reset.stx'),
      4747 join(views, 'account.stx'),
      48 join(partials, 'AuthGuard.stx'),
      48 // AuthGuard.stx is deliberately absent. The client pre-paint guard was
      49 // deleted when auth moved to the server-rendered session (437970e): a page
      50 // that renders its numbers and hides them in the browser still serves them
      51 // to curl. reports-surface.test.ts asserts the server gate that replaced it.
      4952 ]
      5053
      5154 for (const group of ['features', 'use-cases', 'compare']) {
      tests/feature/reports-surface.test.tsmodified+13-2
      Changes to tests/feature/reports-surface.test.ts
      @@ -85,10 +85,21 @@ describe('the reports surface', () => {
      8585 const source = readFileSync(join(root, page), 'utf8')
      8686
      8787 expect(source).toContain("sessionFrom(typeof cookies !== 'undefined' ? cookies : undefined)")
      88 expect(source).toContain(` /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3923:13776: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx (line 3923, col 13776) Message: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' Context:  3921:
      tests/feature/marketing-links.test.tsmodified+4-1
      Changes to tests/feature/marketing-links.test.ts
      @@ -45,7 +45,10 @@ function publicTemplates(): string[] {
      4545 join(views, 'forgot.stx'),
      4646 join(views, 'reset.stx'),
      4747 join(views, 'account.stx'),
      48 join(partials, 'AuthGuard.stx'),
      48 // AuthGuard.stx is deliberately absent. The client pre-paint guard was
      49 // deleted when auth moved to the server-rendered session (437970e): a page
      50 // that renders its numbers and hides them in the browser still serves them
      51 // to curl. reports-surface.test.ts asserts the server gate that replaced it.
      4952 ]
      5053
      5154 for (const group of ['features', 'use-cases', 'compare']) {
      3922: > 3923:
      tests/feature/reports-surface.test.tsmodified+13-2
      Changes to tests/feature/reports-surface.test.ts
      @@ -85,10 +85,21 @@ describe('the reports surface', () => {
      8585 const source = readFileSync(join(root, page), 'utf8')
      8686
      8787 expect(source).toContain("sessionFrom(typeof cookies !== 'undefined' ? cookies : undefined)")
      88 expect(source).toContain(` /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:4128:45: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx (line 4128, col 45) Message: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' Context:  4126: 4127: > 4128:
      4129: Suggested reviewers 4130:  `)
      89 expect(source).toContain('auth-required')
      9088 // And the data is fetched only for an identified visitor.
      9189 expect(source).toMatch(/if \(!user\)|user \?/)
      90
      91 // The other half of the claim, and the half that used to be asserted by
      92 // requiring /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:4291:48: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx (line 4291, col 48) Message: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' Context:  4289: 4290:
      > 4291:

      Checks

      4292: 4293:

      No checks are required on this branch.

       and an #auth-required element. Both were
      93 // deleted in 437970e when the pre-paint guard was replaced by the server
      94 // session, so asserting them asserted the browser guard was still there.
      95 // What matters now is the opposite: that nothing gates in the browser,
      96 // because a client-side bounce leaves the markup already rendered.
      97 // Matched as the directive and the element, not as the word: both pages
      98 // name AuthGuard in a comment explaining that the bounce is gone, and a
      99 // bare substring check fails on the prose that documents the fix.
      100 expect(source).not.toContain(` /var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:4423:35: include error: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx (line 4423, col 35) Message: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' Context:  4421: 4422: .pr-previews { padding: 13px 17px; margin-bottom: 14px; } > 4423: .pr-preview { margin: 6px 0 0; display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; font-size: 13px; } 4424: .pr-preview a { word-break: break-all; } 4425:  `)
      101 expect(source).not.toMatch(/id=["']auth-(?:required|pending)["']/)
      102 expect(source).not.toMatch(/<script client[\s\S]*?location\.(?:replace|href)/)
      92103 }
      93104 })
      94105
      3924: 3925:
      tests/feature/route-gates.test.tsmodified+1-0
      Changes to tests/feature/route-gates.test.ts
      @@ -42,6 +42,7 @@ const ALLOWED = new Map<string, string>([
      4242 // credential each one checks IS its gate.
      4343 ['POST /register', 'Creating an account is the act of becoming a user. Rate limited per address and per IP.'],
      4444 ['POST /login', 'Checks the password, which is the gate. Rate limited per address and per IP.'],
      45 ['POST /verify-two-factor-login', 'Step two of a sign-in, so it cannot require a session either. Its gate is the pair it demands: a single-use challenge token minted by LoginAction only after the password was already checked, plus a TOTP code verified against that account. Neither alone opens a session, and a spent challenge sends the visitor back to /login.'],
      4546 ['POST /forgot', 'Checks nothing and reveals nothing: answers the same either way, so an address cannot be probed. Rate limited.'],
      4647 ['POST /reset', 'The emailed token is the credential. Single use and expiring.'],
      4748 ['POST /logout', 'Revokes whatever token the caller presents. Presenting someone else\'s is signing them out with a credential already held.'],
       `
      )
      89 expect(source).toContain('auth-required')
      9088 // And the data is fetched only for an identified visitor.
      9189 expect(source).toMatch(/if \(!user\)|user \?/)
      90
      91 // The other half of the claim, and the half that used to be asserted by
      92 // requiring ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx Message: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' and an #auth-required element. Both were
      93 // deleted in 437970e when the pre-paint guard was replaced by the server
      94 // session, so asserting them asserted the browser guard was still there.
      95 // What matters now is the opposite: that nothing gates in the browser,
      96 // because a client-side bounce leaves the markup already rendered.
      97 // Matched as the directive and the element, not as the word: both pages
      98 // name AuthGuard in a comment explaining that the bounce is gone, and a
      99 // bare substring check fails on the prose that documents the fix.
      100 expect(source).not.toContain(` ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx Message: Error loading include file AuthGuard: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/AuthGuard.stx' `)
      101 expect(source).not.toMatch(/id=["']auth-(?:required|pending)["']/)
      102 expect(source).not.toMatch(/<script client[\s\S]*?location\.(?:replace|href)/)
      92103 }
      93104 })
      94105
      tests/feature/route-gates.test.tsmodified+1-0
      Changes to tests/feature/route-gates.test.ts
      @@ -42,6 +42,7 @@ const ALLOWED = new Map<string, string>([
      4242 // credential each one checks IS its gate.
      4343 ['POST /register', 'Creating an account is the act of becoming a user. Rate limited per address and per IP.'],
      4444 ['POST /login', 'Checks the password, which is the gate. Rate limited per address and per IP.'],
      45 ['POST /verify-two-factor-login', 'Step two of a sign-in, so it cannot require a session either. Its gate is the pair it demands: a single-use challenge token minted by LoginAction only after the password was already checked, plus a TOTP code verified against that account. Neither alone opens a session, and a spent challenge sends the visitor back to /login.'],
      4546 ['POST /forgot', 'Checks nothing and reveals nothing: answers the same either way, so an address cannot be probed. Rate limited.'],
      4647 ['POST /reset', 'The emailed token is the credential. Single use and expiring.'],
      4748 ['POST /logout', 'Revokes whatever token the caller presents. Presenting someone else\'s is signing them out with a credential already held.'],