| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export const config = { runtime: 'edge' }; |
|
|
| |
| import { getCorsHeaders, isDisallowedOrigin } from '../_cors.js'; |
| |
| import { jsonResponse } from '../_json-response.js'; |
| import { readRawJsonFromUpstash, redisPipeline } from '../_upstash-json.js'; |
| |
| import { captureSilentError } from '../_sentry-edge.js'; |
| import { validateBearerToken } from '../../server/auth-session'; |
| import { checkProEntitlement } from '../../server/_shared/pro-entitlement'; |
| import { |
| BriefShareUrlError, |
| BRIEF_PUBLIC_POINTER_PREFIX, |
| buildPublicBriefUrl, |
| encodePublicPointer, |
| } from '../../server/_shared/brief-share-url'; |
|
|
| const ISSUE_SLOT_RE = /^\d{4}-\d{2}-\d{2}-\d{4}$/; |
|
|
| |
| |
| |
| |
| |
| const BRIEF_TTL_SECONDS = 7 * 24 * 60 * 60; |
|
|
| |
| |
| |
| |
| |
| |
| function publicBaseUrl(req: Request): string { |
| const pinned = process.env.WORLDMONITOR_PUBLIC_BASE_URL; |
| if (pinned) return pinned.replace(/\/+$/, ''); |
| return new URL(req.url).origin; |
| } |
|
|
| export default async function handler( |
| req: Request, |
| ctx?: { waitUntil: (p: Promise<unknown>) => void }, |
| ): Promise<Response> { |
| if (isDisallowedOrigin(req)) { |
| return jsonResponse({ error: 'Origin not allowed' }, 403); |
| } |
|
|
| const cors = getCorsHeaders(req, 'POST, OPTIONS'); |
|
|
| if (req.method === 'OPTIONS') { |
| return new Response(null, { status: 204, headers: cors }); |
| } |
| if (req.method !== 'POST') { |
| return jsonResponse({ error: 'Method not allowed' }, 405, cors); |
| } |
|
|
| const authHeader = req.headers.get('Authorization') ?? ''; |
| const jwt = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''; |
| if (!jwt) return jsonResponse({ error: 'UNAUTHENTICATED' }, 401, cors); |
|
|
| const session = await validateBearerToken(jwt); |
| if (!session.valid || !session.userId) { |
| return jsonResponse({ error: 'UNAUTHENTICATED' }, 401, cors); |
| } |
|
|
| const proAccess = await checkProEntitlement(session.userId, session.role, cors); |
| if (!proAccess.allowed) { |
| |
| |
| |
| |
| |
| |
| const { billingDenial } = proAccess; |
| if (billingDenial) return billingDenial; |
| return jsonResponse( |
| { error: 'pro_required', message: 'Sharing is available on the Pro plan.' }, |
| 403, |
| cors, |
| ); |
| } |
|
|
| const secret = process.env.BRIEF_SHARE_SECRET ?? ''; |
| if (!secret) { |
| console.error('[api/brief/share-url] BRIEF_SHARE_SECRET is not configured'); |
| return jsonResponse({ error: 'service_unavailable' }, 503, cors); |
| } |
|
|
| |
| |
| |
| |
| |
| const url = new URL(req.url); |
| let issueSlot = url.searchParams.get('slot'); |
| let refCode: string | undefined; |
| if (!issueSlot || req.headers.get('content-type')?.includes('application/json')) { |
| try { |
| const body = (await req.json().catch(() => null)) as |
| | { slot?: unknown; refCode?: unknown } |
| | null; |
| if (!issueSlot && typeof body?.slot === 'string') issueSlot = body.slot; |
| if (typeof body?.refCode === 'string' && body.refCode.length > 0 && body.refCode.length <= 32) { |
| refCode = body.refCode; |
| } |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| |
| const callerProvidedSlot = |
| typeof issueSlot === 'string' && issueSlot.trim().length > 0; |
|
|
| if (!callerProvidedSlot) { |
| |
| try { |
| const latest = await readRawJsonFromUpstash(`brief:latest:${session.userId}`); |
| const slot = (latest as { issueSlot?: unknown } | null)?.issueSlot; |
| if (typeof slot === 'string' && ISSUE_SLOT_RE.test(slot)) { |
| issueSlot = slot; |
| } else { |
| |
| |
| |
| |
| |
| return jsonResponse({ error: 'brief_not_found' }, 404, cors); |
| } |
| } catch (err) { |
| console.error('[api/brief/share-url] latest pointer read failed:', (err as Error).message); |
| captureSilentError(err, { tags: { route: 'api/brief/share-url', step: 'latest-pointer-read' }, ctx }); |
| return jsonResponse({ error: 'service_unavailable' }, 503, cors); |
| } |
| } |
|
|
| if (!issueSlot || !ISSUE_SLOT_RE.test(issueSlot)) { |
| return jsonResponse({ error: 'invalid_slot_shape' }, 400, cors); |
| } |
|
|
| |
| |
| |
| |
| let existing: unknown; |
| try { |
| existing = await readRawJsonFromUpstash(`brief:${session.userId}:${issueSlot}`); |
| } catch (err) { |
| console.error('[api/brief/share-url] Upstash read failed:', (err as Error).message); |
| captureSilentError(err, { tags: { route: 'api/brief/share-url', step: 'envelope-read' }, ctx }); |
| return jsonResponse({ error: 'service_unavailable' }, 503, cors); |
| } |
| if (existing == null) { |
| return jsonResponse({ error: 'brief_not_found' }, 404, cors); |
| } |
|
|
| let shareUrl: string; |
| let hash: string; |
| try { |
| const built = await buildPublicBriefUrl({ |
| userId: session.userId, |
| issueDate: issueSlot, |
| baseUrl: publicBaseUrl(req), |
| secret, |
| refCode, |
| }); |
| shareUrl = built.url; |
| hash = built.hash; |
| } catch (err) { |
| if (err instanceof BriefShareUrlError) { |
| console.error(`[api/brief/share-url] ${err.code}: ${err.message}`); |
| return jsonResponse({ error: 'service_unavailable' }, 503, cors); |
| } |
| throw err; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const pointerKey = `${BRIEF_PUBLIC_POINTER_PREFIX}${hash}`; |
| const pointerValue = JSON.stringify(encodePublicPointer(session.userId, issueSlot)); |
| const writeResult = await redisPipeline([ |
| ['SET', pointerKey, pointerValue, 'EX', String(BRIEF_TTL_SECONDS)], |
| ]); |
| if (writeResult == null) { |
| console.error('[api/brief/share-url] pointer write failed'); |
| return jsonResponse({ error: 'service_unavailable' }, 503, cors); |
| } |
|
|
| return jsonResponse({ shareUrl, hash, issueSlot }, 200, cors); |
| } |
|
|