| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const USER_ID_RE = /^[A-Za-z0-9_-]{1,128}$/; |
| |
| |
| |
| const ISSUE_DATE_RE = /^\d{4}-\d{2}-\d{2}-\d{4}$/; |
|
|
| export class BriefUrlError extends Error { |
| constructor(code, message) { |
| super(message); |
| this.code = code; |
| this.name = 'BriefUrlError'; |
| } |
| } |
|
|
| function assertShape(userId, issueDate) { |
| if (!USER_ID_RE.test(userId)) { |
| throw new BriefUrlError('invalid_user_id', 'userId must match [A-Za-z0-9_-]{1,128}'); |
| } |
| if (!ISSUE_DATE_RE.test(issueDate)) { |
| throw new BriefUrlError('invalid_issue_date', 'issueDate must match YYYY-MM-DD-HHMM'); |
| } |
| } |
|
|
| function base64url(bytes) { |
| let bin = ''; |
| for (const b of bytes) bin += String.fromCharCode(b); |
| return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); |
| } |
|
|
| async function hmacSha256(secret, message) { |
| const key = await crypto.subtle.importKey( |
| 'raw', |
| new TextEncoder().encode(secret), |
| { name: 'HMAC', hash: 'SHA-256' }, |
| false, |
| ['sign'], |
| ); |
| const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message)); |
| return new Uint8Array(sig); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function signBriefToken(userId, issueDate, secret) { |
| assertShape(userId, issueDate); |
| if (!secret) { |
| throw new BriefUrlError('missing_secret', 'BRIEF_URL_SIGNING_SECRET is not configured'); |
| } |
| const sig = await hmacSha256(secret, `${userId}:${issueDate}`); |
| return base64url(sig); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function signBriefUrl({ userId, issueDate, baseUrl, secret }) { |
| const token = await signBriefToken(userId, issueDate, secret); |
| const trimmed = baseUrl.replace(/\/+$/, ''); |
| return `${trimmed}/api/brief/${encodeURIComponent(userId)}/${encodeURIComponent(issueDate)}?t=${token}`; |
| } |
|
|