@teamamw/public-route-gate
v0.2.0
Published
AMW shared helpers for public route gating — access-code, portal-token HMAC, finalize-token
Readme
@amw/public-route-gate
Shared helpers for AMW public routes. Solves the recurring public-surface bugs from autonomous-sweep r13-r15 (May 23 2026):
- Mutator-no-access-code: public POST/PATCH/DELETE on slug resources that fire side effects without requiring access_code. →
requireAccessCodeIfSet. - Public-route-no-token: public routes that accept
?email=as sole auth, leading to email enumeration. →mintPortalToken+verifyPortalToken. - Finalize-style flows: random per-row session token issued at create-time. →
mintSessionToken+verifySessionToken. - Timing oracles: →
timingSafeStringEqual.
Install
npm install @amw/public-route-gateQuick start
Pattern A — human-shared access code (proposals, marketing docs)
import { requireAccessCodeIfSet } from '@amw/public-route-gate'
const gateProposal = requireAccessCodeIfSet({
lookupAccessCode: async (slug) => {
const r = await pool.query('SELECT access_code FROM proposals WHERE slug=$1', [slug])
return r.rows[0]?.access_code ?? null
},
})
router.post('/proposals/:slug/approve', async (req, res) => {
const gate = await gateProposal(req.params.slug, req)
if (!gate.ok) return res.status(gate.status).json({ error: gate.error })
// ... safe to UPDATE / send email / kick off billing
})Pattern B — HMAC portal token (billing portal, customer self-service)
import { mintPortalToken, verifyPortalToken } from '@amw/public-route-gate'
// Mint at link-creation time (admin widget / billing email):
const token = mintPortalToken({
resourceId: customer.id,
identifier: customer.email.toLowerCase(),
secret: process.env.BILLING_PORTAL_SECRET!,
})
const url = `https://amworldgroup.com/billing?email=${enc(customer.email)}&token=${token}`
// Verify on the public route:
if (!verifyPortalToken({
token: req.query.token,
resourceId: customer.id,
identifier: email,
secret: process.env.BILLING_PORTAL_SECRET!,
})) {
return res.status(401).json({ error: 'Invalid token' })
}Pattern C — random session token (finalize-style flows)
import { mintSessionToken, verifySessionToken } from '@amw/public-route-gate'
// At /submit (create):
const finalizeToken = mintSessionToken()
await pool.query(
'INSERT INTO funnel_submissions(..., finalize_token) VALUES (...,$N)',
[..., finalizeToken],
)
res.json({ submission_id: id, finalize_token: finalizeToken })
// At /finalize (action):
const row = await pool.query('SELECT finalize_token FROM funnel_submissions WHERE id=$1', [id])
if (!verifySessionToken(req.body.finalize_token, row.rows[0]?.finalize_token)) {
return res.status(401).json({ error: 'Invalid token' })
}License
MIT
