AtAuth
7

Configure Feed

Select the types of activity you want to include in your feed.

fix: Express 5 req.params type safety, restore CI pipeline

Add String() wrappers on all req.params usages to satisfy Express 5's
string | string[] union type. Restore .gitea/workflows/deploy.yml that
was accidentally removed during FOSS sanitization.

Bryan Brooks (Feb 25, 2026, 10:27 AM -0600) 6ab88749 15cc2f39

+194 -50
+132
.gitea/workflows/deploy.yml
··· 1 + name: Deploy ATAuth to DO 2 + # CI/CD: Gitea Actions → DigitalOcean managed k8s 3 + 4 + on: 5 + push: 6 + branches: [main] 7 + paths-ignore: 8 + - '*.md' 9 + - 'docs/**' 10 + 11 + env: 12 + REGISTRY: registry.digitalocean.com/ghostmesh-registry 13 + IMAGE_NAME: atauth 14 + DEPLOYMENT: atauth 15 + NAMESPACE: atauth 16 + 17 + jobs: 18 + test: 19 + name: Test Gateway 20 + runs-on: ubuntu-latest 21 + defaults: 22 + run: 23 + working-directory: gateway 24 + steps: 25 + - name: Checkout code 26 + uses: actions/checkout@v4 27 + 28 + - name: Setup Node.js 29 + uses: actions/setup-node@v4 30 + with: 31 + node-version: '20' 32 + 33 + - name: Install dependencies 34 + run: npm ci 35 + 36 + - name: Type check 37 + run: npm run typecheck 38 + 39 + - name: Lint 40 + run: npm run lint 41 + 42 + - name: Run tests 43 + run: npm run test:run 44 + 45 + - name: Notify test failure 46 + if: failure() 47 + run: | 48 + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) 49 + FIRST_LINE=$(echo "${{ github.event.head_commit.message }}" | head -1) 50 + MSG=$(jq -n --arg body "🧪 ${{ github.repository }}: tests FAILED @ ${SHORT_SHA} 51 + └ ${FIRST_LINE}" '{msgtype: "m.text", body: $body}') 52 + curl -sf -X PUT \ 53 + "${{ secrets.MATRIX_HOMESERVER_URL }}/_matrix/client/v3/rooms/${{ secrets.MATRIX_CICD_ROOM_ID }}/send/m.room.message/cicd-$(date +%s)-$$" \ 54 + -H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \ 55 + -H "Content-Type: application/json" \ 56 + -d "$MSG" || true 57 + 58 + build-and-deploy: 59 + name: Build, Push, Deploy 60 + runs-on: host 61 + needs: [test] 62 + steps: 63 + - name: Checkout code 64 + uses: actions/checkout@v4 65 + 66 + - name: Set short SHA 67 + id: sha 68 + run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> "$GITHUB_OUTPUT" 69 + 70 + - name: Notify build started 71 + env: 72 + COMMIT_MSG: ${{ github.event.head_commit.message }} 73 + run: | 74 + FIRST_LINE=$(echo "$COMMIT_MSG" | head -1) 75 + MSG=$(jq -n --arg body "🔨 ${{ github.repository }}: build started @ ${{ steps.sha.outputs.short }} 76 + ├ Commit: $FIRST_LINE 77 + └ Triggered by: ${{ github.actor }}" '{msgtype: "m.text", body: $body}') 78 + curl -sf -X PUT \ 79 + "${{ secrets.MATRIX_HOMESERVER_URL }}/_matrix/client/v3/rooms/${{ secrets.MATRIX_CICD_ROOM_ID }}/send/m.room.message/cicd-$(date +%s)-$$" \ 80 + -H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \ 81 + -H "Content-Type: application/json" \ 82 + -d "$MSG" || true 83 + 84 + - name: Login to DO Container Registry 85 + run: echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin 86 + 87 + - name: Build Docker image 88 + run: | 89 + docker build \ 90 + --platform linux/amd64 \ 91 + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.short }} \ 92 + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ 93 + gateway/ 94 + 95 + - name: Push Docker image 96 + run: | 97 + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.short }} 98 + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 99 + 100 + - name: Deploy to DO Kubernetes 101 + run: | 102 + echo "${{ secrets.DO_KUBECONFIG }}" | base64 -d > /tmp/kubeconfig 103 + KUBECONFIG=/tmp/kubeconfig kubectl set image deployment/${{ env.DEPLOYMENT }} \ 104 + ${{ env.DEPLOYMENT }}=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.short }} \ 105 + -n ${{ env.NAMESPACE }} 106 + KUBECONFIG=/tmp/kubeconfig kubectl rollout status deployment/${{ env.DEPLOYMENT }} -n ${{ env.NAMESPACE }} --timeout=120s 107 + rm -f /tmp/kubeconfig 108 + 109 + - name: Notify build result 110 + if: always() 111 + env: 112 + COMMIT_MSG: ${{ github.event.head_commit.message }} 113 + run: | 114 + if [ "${{ job.status }}" = "success" ]; then 115 + EMOJI="✅"; STATUS="deployed" 116 + else 117 + EMOJI="❌"; STATUS="FAILED" 118 + fi 119 + FIRST_LINE=$(echo "$COMMIT_MSG" | head -1) 120 + MSG=$(jq -n --arg body "${EMOJI} ${{ github.repository }}: ${STATUS} @ ${{ steps.sha.outputs.short }} 121 + └ $FIRST_LINE" '{msgtype: "m.text", body: $body}') 122 + curl -sf -X PUT \ 123 + "${{ secrets.MATRIX_HOMESERVER_URL }}/_matrix/client/v3/rooms/${{ secrets.MATRIX_CICD_ROOM_ID }}/send/m.room.message/cicd-$(date +%s)-$$" \ 124 + -H "Authorization: Bearer ${{ secrets.MATRIX_ACCESS_TOKEN }}" \ 125 + -H "Content-Type: application/json" \ 126 + -d "$MSG" || true 127 + 128 + - name: Clean up Docker images 129 + if: always() 130 + run: | 131 + docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.short }} 2>/dev/null || true 132 + docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 2>/dev/null || true
+13 -12
gateway/src/routes/admin-dashboard.ts
··· 212 212 213 213 router.post('/origins/:id/delete', (req: Request, res: Response) => { 214 214 if (!requireCsrf(req, res)) return; 215 - db.removeProxyAllowedOrigin(parseInt(req.params.id, 10)); 215 + db.removeProxyAllowedOrigin(parseInt(String(req.params.id), 10)); 216 216 res.redirect('/admin/dashboard/origins?msg=Origin+deleted'); 217 217 }); 218 218 ··· 347 347 348 348 router.post('/access/:id/delete', (req: Request, res: Response) => { 349 349 if (!requireCsrf(req, res)) return; 350 - db.deleteProxyAccessRule(parseInt(req.params.id, 10)); 350 + db.deleteProxyAccessRule(parseInt(String(req.params.id), 10)); 351 351 res.redirect('/admin/dashboard/access?msg=Rule+deleted'); 352 352 }); 353 353 ··· 408 408 409 409 router.post('/sessions/:id/delete', (req: Request, res: Response) => { 410 410 if (!requireCsrf(req, res)) return; 411 - db.deleteProxySession(req.params.id); 411 + db.deleteProxySession(String(req.params.id)); 412 412 res.redirect('/admin/dashboard/sessions?msg=Session+revoked'); 413 413 }); 414 414 ··· 700 700 // --- Edit Client --- 701 701 702 702 router.get('/clients/:id/edit', (req: Request, res: Response) => { 703 - const client = db.getOIDCClient(req.params.id); 703 + const client = db.getOIDCClient(String(req.params.id)); 704 704 if (!client) { 705 705 return res.redirect('/admin/dashboard/clients?msg=Client+not+found'); 706 706 } ··· 798 798 799 799 router.post('/clients/:id/edit', (req: Request, res: Response) => { 800 800 if (!requireCsrf(req, res)) return; 801 - const clientId = req.params.id; 801 + const clientId = String(req.params.id); 802 802 const existing = db.getOIDCClient(clientId); 803 803 if (!existing) { 804 804 return res.redirect('/admin/dashboard/clients?msg=Client+not+found'); ··· 838 838 839 839 router.post('/clients/:id/rotate-secret', (req: Request, res: Response) => { 840 840 if (!requireCsrf(req, res)) return; 841 - const clientId = req.params.id; 841 + const clientId = String(req.params.id); 842 842 const existing = db.getOIDCClient(clientId); 843 843 if (!existing) { 844 844 return res.redirect('/admin/dashboard/clients?msg=Client+not+found'); ··· 857 857 858 858 router.post('/clients/:id/delete', (req: Request, res: Response) => { 859 859 if (!requireCsrf(req, res)) return; 860 - db.deleteApp(req.params.id); 860 + db.deleteApp(String(req.params.id)); 861 861 res.redirect('/admin/dashboard/clients?msg=Client+deleted'); 862 862 }); 863 863 ··· 886 886 }); 887 887 888 888 router.get('/clients/wizard/:preset', (req: Request, res: Response) => { 889 - const presetData = getPresetByKey(req.params.preset); 889 + const presetData = getPresetByKey(String(req.params.preset)); 890 890 if (!presetData) { 891 891 return res.status(404).setHeader('Content-Type', 'text/html; charset=utf-8').send( 892 892 layout('Not Found', '<div class="card"><h2>Preset not found</h2><p><a href="/admin/dashboard/clients/wizard">Back to wizard</a></p></div>') ··· 979 979 980 980 router.post('/clients/wizard/:preset', (req: Request, res: Response) => { 981 981 if (!requireCsrf(req, res)) return; 982 - const presetData = getPresetByKey(req.params.preset); 982 + const presetKey = String(req.params.preset); 983 + const presetData = getPresetByKey(presetKey); 983 984 if (!presetData) { 984 985 return res.redirect('/admin/dashboard/clients/wizard'); 985 986 } ··· 990 991 const require_pkce = req.body.require_pkce === 'on'; 991 992 992 993 if (!id || !name || !domain) { 993 - return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(req.params.preset)}`); 994 + return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(presetKey)}`); 994 995 } 995 996 if (!/^[a-z0-9][a-z0-9_-]*$/.test(id) || id.length > 64) { 996 - return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(req.params.preset)}`); 997 + return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(presetKey)}`); 997 998 } 998 999 999 1000 const existing = db.getOIDCClient(id); 1000 1001 if (existing) { 1001 - return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(req.params.preset)}`); 1002 + return res.redirect(`/admin/dashboard/clients/wizard/${encodeURIComponent(presetKey)}`); 1002 1003 } 1003 1004 1004 1005 // Build redirect URIs from template
+46 -35
gateway/src/routes/admin.ts
··· 188 188 * Get application configuration (without secret) 189 189 */ 190 190 router.get('/apps/:id', requireAdmin, async (req: Request, res: Response) => { 191 - const app = db.getApp(req.params.id); 191 + const id = String(req.params.id); 192 + const app = db.getApp(id); 192 193 if (!app) { 193 - throw httpError.notFound('app_not_found', `Application '${req.params.id}' not found`); 194 + throw httpError.notFound('app_not_found', `Application '${id}' not found`); 194 195 } 195 196 196 197 res.json({ ··· 206 207 * Update application configuration 207 208 */ 208 209 router.put('/apps/:id', requireAdmin, async (req: Request, res: Response) => { 209 - const existing = db.getApp(req.params.id); 210 + const id = String(req.params.id); 211 + const existing = db.getApp(id); 210 212 if (!existing) { 211 - throw httpError.notFound('app_not_found', `Application '${req.params.id}' not found`); 213 + throw httpError.notFound('app_not_found', `Application '${id}' not found`); 212 214 } 213 215 214 216 const { name, token_ttl_seconds, callback_url, rotate_secret } = req.body; 215 217 216 218 const updated = { 217 - id: req.params.id, 219 + id, 218 220 name: name || existing.name, 219 221 hmac_secret: rotate_secret ? generateHmacSecret() : existing.hmac_secret, 220 222 token_ttl_seconds: token_ttl_seconds || existing.token_ttl_seconds, ··· 224 226 db.upsertApp(updated); 225 227 226 228 if (rotate_secret) { 227 - db.logAuditEvent('app.secret_rotate', 'admin', req.params.id, 'HMAC secret rotated', clientIp(req)); 229 + db.logAuditEvent('app.secret_rotate', 'admin', id, 'HMAC secret rotated', clientIp(req)); 228 230 } else { 229 - db.logAuditEvent('app.update', 'admin', req.params.id, `Updated app config`, clientIp(req)); 231 + db.logAuditEvent('app.update', 'admin', id, `Updated app config`, clientIp(req)); 230 232 } 231 233 232 234 const response: Record<string, unknown> = { ··· 360 362 * Get OIDC client details 361 363 */ 362 364 router.get('/oidc/clients/:id', requireAdmin, async (req: Request, res: Response) => { 363 - const client = db.getOIDCClient(req.params.id); 365 + const id = String(req.params.id); 366 + const client = db.getOIDCClient(id); 364 367 if (!client) { 365 - throw httpError.notFound('client_not_found', `OIDC client '${req.params.id}' not found`); 368 + throw httpError.notFound('client_not_found', `OIDC client '${id}' not found`); 366 369 } 367 370 368 371 res.json({ ··· 386 389 * Update OIDC client 387 390 */ 388 391 router.put('/oidc/clients/:id', requireAdmin, async (req: Request, res: Response) => { 389 - const existing = db.getOIDCClient(req.params.id); 392 + const id = String(req.params.id); 393 + const existing = db.getOIDCClient(id); 390 394 if (!existing) { 391 - throw httpError.notFound('client_not_found', `OIDC client '${req.params.id}' not found`); 395 + throw httpError.notFound('client_not_found', `OIDC client '${id}' not found`); 392 396 } 393 397 394 398 const { ··· 403 407 refresh_token_ttl_seconds, 404 408 } = req.body; 405 409 406 - db.updateOIDCClient(req.params.id, { 410 + db.updateOIDCClient(id, { 407 411 redirect_uris: redirect_uris ?? existing.redirect_uris, 408 412 grant_types: grant_types ?? existing.grant_types, 409 413 allowed_scopes: allowed_scopes ?? existing.allowed_scopes, ··· 416 420 417 421 // Update app name if provided 418 422 if (name) { 419 - const app = db.getApp(req.params.id); 423 + const app = db.getApp(id); 420 424 if (app) { 421 425 db.upsertApp({ ...app, name }); 422 426 } 423 427 } 424 428 425 - db.logAuditEvent('oidc_client.update', 'admin', req.params.id, 'Updated OIDC client config', clientIp(req)); 429 + db.logAuditEvent('oidc_client.update', 'admin', id, 'Updated OIDC client config', clientIp(req)); 426 430 427 431 res.json({ 428 - id: req.params.id, 432 + id, 429 433 message: 'OIDC client updated', 430 434 }); 431 435 }); ··· 435 439 * Delete OIDC client 436 440 */ 437 441 router.delete('/oidc/clients/:id', requireAdmin, async (req: Request, res: Response) => { 438 - const existing = db.getOIDCClient(req.params.id); 442 + const id = String(req.params.id); 443 + const existing = db.getOIDCClient(id); 439 444 if (!existing) { 440 - throw httpError.notFound('client_not_found', `OIDC client '${req.params.id}' not found`); 445 + throw httpError.notFound('client_not_found', `OIDC client '${id}' not found`); 441 446 } 442 447 443 - db.deleteApp(req.params.id); 444 - db.logAuditEvent('oidc_client.delete', 'admin', req.params.id, `Deleted OIDC client "${existing.name}"`, clientIp(req)); 448 + db.deleteApp(id); 449 + db.logAuditEvent('oidc_client.delete', 'admin', id, `Deleted OIDC client "${existing.name}"`, clientIp(req)); 445 450 446 451 res.json({ 447 452 message: 'OIDC client deleted', ··· 453 458 * Rotate OIDC client secret 454 459 */ 455 460 router.post('/oidc/clients/:id/rotate-secret', requireAdmin, async (req: Request, res: Response) => { 456 - const existing = db.getOIDCClient(req.params.id); 461 + const id = String(req.params.id); 462 + const existing = db.getOIDCClient(id); 457 463 if (!existing) { 458 - throw httpError.notFound('client_not_found', `OIDC client '${req.params.id}' not found`); 464 + throw httpError.notFound('client_not_found', `OIDC client '${id}' not found`); 459 465 } 460 466 461 467 const clientSecret = crypto.randomBytes(32).toString('hex'); 462 468 const clientSecretHash = crypto.createHash('sha256').update(clientSecret).digest('hex'); 463 469 464 - db.updateOIDCClientSecret(req.params.id, clientSecretHash); 465 - db.logAuditEvent('oidc_client.secret_rotate', 'admin', req.params.id, 'Client secret rotated', clientIp(req)); 470 + db.updateOIDCClientSecret(id, clientSecretHash); 471 + db.logAuditEvent('oidc_client.secret_rotate', 'admin', id, 'Client secret rotated', clientIp(req)); 466 472 467 473 res.json({ 468 - id: req.params.id, 474 + id, 469 475 client_secret: clientSecret, 470 476 message: 'Client secret rotated. Update your application configuration!', 471 477 }); ··· 504 510 * Revoke a specific session 505 511 */ 506 512 router.delete('/sessions/:id', requireAdmin, async (req: Request, res: Response) => { 507 - db.deleteSession(req.params.id); 508 - db.logAuditEvent('session.revoke', 'admin', req.params.id, 'Session revoked', clientIp(req)); 513 + const id = String(req.params.id); 514 + db.deleteSession(id); 515 + db.logAuditEvent('session.revoke', 'admin', id, 'Session revoked', clientIp(req)); 509 516 res.json({ message: 'Session revoked' }); 510 517 }); 511 518 ··· 575 582 * Get user details including MFA status and passkeys 576 583 */ 577 584 router.get('/users/:did', requireAdmin, async (req: Request, res: Response) => { 578 - const { did } = req.params; 585 + const did = String(req.params.did); 579 586 580 587 const passkeys = passkeyService?.listPasskeys(did) || []; 581 588 const mfaStatus = mfaService?.getMFAStatus(did, passkeys.length) || { ··· 610 617 * Reset MFA for a user (admin override) 611 618 */ 612 619 router.delete('/users/:did/mfa', requireAdmin, async (req: Request, res: Response) => { 613 - const { did } = req.params; 620 + const did = String(req.params.did); 614 621 615 622 if (mfaService) { 616 623 mfaService.disableTOTP(did); ··· 628 635 * Delete a passkey for a user (admin override) 629 636 */ 630 637 router.delete('/users/:did/passkeys/:passkeyId', requireAdmin, async (req: Request, res: Response) => { 631 - const { did, passkeyId } = req.params; 638 + const did = String(req.params.did); 639 + const passkeyId = String(req.params.passkeyId); 632 640 633 641 if (passkeyService) { 634 642 const success = passkeyService.deletePasskey(did, passkeyId); ··· 699 707 * Remove an allowed origin 700 708 */ 701 709 router.delete('/proxy/origins/:id', requireAdmin, async (req: Request, res: Response) => { 702 - db.removeProxyAllowedOrigin(parseInt(req.params.id, 10)); 703 - db.logAuditEvent('proxy.origin_remove', 'admin', req.params.id, 'Removed proxy origin', clientIp(req)); 710 + const id = String(req.params.id); 711 + db.removeProxyAllowedOrigin(parseInt(id, 10)); 712 + db.logAuditEvent('proxy.origin_remove', 'admin', id, 'Removed proxy origin', clientIp(req)); 704 713 res.json({ message: 'Origin removed' }); 705 714 }); 706 715 ··· 722 731 * Revoke a proxy session 723 732 */ 724 733 router.delete('/proxy/sessions/:id', requireAdmin, async (req: Request, res: Response) => { 725 - db.deleteProxySession(req.params.id); 726 - db.logAuditEvent('proxy.session_revoke', 'admin', req.params.id, 'Proxy session revoked', clientIp(req)); 734 + const id = String(req.params.id); 735 + db.deleteProxySession(id); 736 + db.logAuditEvent('proxy.session_revoke', 'admin', id, 'Proxy session revoked', clientIp(req)); 727 737 res.json({ message: 'Proxy session revoked' }); 728 738 }); 729 739 ··· 806 816 * Delete an access rule. 807 817 */ 808 818 router.delete('/proxy/access/:id', requireAdmin, async (req: Request, res: Response) => { 809 - db.deleteProxyAccessRule(parseInt(req.params.id, 10)); 810 - db.logAuditEvent('proxy.access_rule_delete', 'admin', req.params.id, 'Deleted access rule', clientIp(req)); 819 + const id = String(req.params.id); 820 + db.deleteProxyAccessRule(parseInt(id, 10)); 821 + db.logAuditEvent('proxy.access_rule_delete', 'admin', id, 'Deleted access rule', clientIp(req)); 811 822 res.json({ message: 'Access rule deleted' }); 812 823 }); 813 824
+1 -1
gateway/src/routes/email.ts
··· 128 128 throw new HttpError(401, 'unauthorized', 'Authentication required'); 129 129 } 130 130 131 - const { email } = req.params; 131 + const email = String(req.params.email); 132 132 133 133 if (!email || !isValidEmail(email)) { 134 134 throw new HttpError(400, 'invalid_request', 'Invalid email address');
+2 -2
gateway/src/routes/passkey.ts
··· 196 196 throw new HttpError(401, 'unauthorized', 'Authentication required'); 197 197 } 198 198 199 - const { id } = req.params; 199 + const id = String(req.params.id); 200 200 const { name } = req.body as { name: string }; 201 201 202 202 if (!name) { ··· 227 227 throw new HttpError(401, 'unauthorized', 'Authentication required'); 228 228 } 229 229 230 - const { id } = req.params; 230 + const id = String(req.params.id); 231 231 232 232 // Prevent deleting last passkey if MFA is required 233 233 // (This is a safety check - can be configured based on policy)