import { buildPublicOrderGroupInvitationView, buildPublicInvitationView, getClientInvitationStateFromOrderGroupStatus, getClientInvitationStateFromOrderStatus, hashInvitationToken, isActiveInvitationState, isInvitationExpired, } from "../_shared/delivery-invitations.ts"; import { createServiceClient } from "../_shared/chatbot.ts"; import { isValidUuid } from "../_shared/security.ts"; import { getClientIp, getCorsHeaders, hashText, jsonResponse, preflightResponse, readJsonBody, requireRateLimit, } from "../_shared/security.ts"; const MAX_BODY_BYTES = 8 * 1024; type InvitationBody = { token?: string; }; const getTokenFromRequest = async (request: Request) => { if (request.method === "GET") { return new URL(request.url).searchParams.get("token") || ""; } const { body } = await readJsonBody(request, { maxBytes: MAX_BODY_BYTES, }); return String(body.token || "").trim(); }; Deno.serve(async (request) => { if (request.method === "OPTIONS") { return preflightResponse(request, "public"); } if (!["GET", "POST"].includes(request.method)) { return jsonResponse({ ok: false, error: "Method not allowed" }, 405); } const corsHeaders = getCorsHeaders(request, "public"); if (!corsHeaders) { return jsonResponse({ ok: false, error: "Origin not allowed" }, 403); } try { const token = await getTokenFromRequest(request); if (!token) { return jsonResponse({ ok: false, error: "token is required" }, 400, corsHeaders); } const tokenHash = await hashInvitationToken(token); const supabase = createServiceClient(); const ipHash = await hashText(getClientIp(request)); await requireRateLimit(supabase, { scope: "invitation-get", key: `${ipHash}:${tokenHash.slice(0, 16)}`, maxCount: 30, windowSeconds: 600, }); const { data: invitation, error: invitationError } = await supabase .from("delivery_invitations") .select("*") .eq("token_hash", tokenHash) .single(); if (invitationError) { if (invitationError.code === "PGRST116") { return jsonResponse({ ok: false, error: "Invitation not found" }, 404, corsHeaders); } throw invitationError; } if (isInvitationExpired(invitation)) { return jsonResponse({ ok: false, error: "Invitation expired" }, 410, corsHeaders); } if (invitation.order_group_id) { const { data: group, error: groupError } = await supabase .from("order_groups") .select("*") .eq("id", invitation.order_group_id) .single(); if (groupError) { throw groupError; } const publicState = getClientInvitationStateFromOrderGroupStatus( group.delivery_status, invitation.state, ); await supabase .from("delivery_invitations") .update({ opened_at: isActiveInvitationState(publicState) && !invitation.opened_at ? new Date().toISOString() : invitation.opened_at, access_count: (invitation.access_count || 0) + 1, last_accessed_at: new Date().toISOString(), }) .eq("id", invitation.id); const invitationView = buildPublicOrderGroupInvitationView(invitation, group); return jsonResponse( { ok: true, invitation: { ...invitationView, token, state: publicState, }, }, 200, corsHeaders, ); } const { data: order, error: orderError } = await supabase .from("orders") .select("id, order_number, status, delivery_agreement_status, customer") .eq("id", invitation.order_id) .single(); if (orderError) { throw orderError; } const publicState = getClientInvitationStateFromOrderStatus(order.status); if (isActiveInvitationState(publicState) && !invitation.opened_at) { await supabase .from("delivery_invitations") .update({ opened_at: new Date().toISOString(), access_count: (invitation.access_count || 0) + 1, last_accessed_at: new Date().toISOString(), }) .eq("id", invitation.id); } else { await supabase .from("delivery_invitations") .update({ access_count: (invitation.access_count || 0) + 1, last_accessed_at: new Date().toISOString(), }) .eq("id", invitation.id); } const invitationView = buildPublicInvitationView(invitation, order); return jsonResponse( { ok: true, invitation: { ...invitationView, token, state: publicState, }, }, 200, corsHeaders, ); } catch (error) { if (error instanceof Error && "status" in error) { const httpError = error as { status: number; message: string }; return jsonResponse({ ok: false, error: httpError.message }, httpError.status, corsHeaders); } return jsonResponse( { ok: false, error: error instanceof Error ? error.message : "Unexpected error", }, 500, corsHeaders, ); } });