{"version":3,"file":"preview.mjs","names":[],"sources":["../../../../../src/astro/routes/api/themes/preview.ts"],"sourcesContent":["/**\n * Theme preview signing endpoint\n *\n * POST /_emdash/api/themes/preview\n *\n * Generates a signed preview URL for the \"Try with my data\" feature.\n *\n * Uses the resolved preview secret: env override (`EMDASH_PREVIEW_SECRET`)\n * wins, otherwise an auto-generated stable per-site value persisted in the\n * options table is used. Processes that share the same database converge on\n * the same auto-generated value; only set `EMDASH_PREVIEW_SECRET` in both\n * processes when the verifying side runs without access to the EmDash DB\n * (e.g. a remote preview Worker).\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { requirePerm } from \"#api/authorize.js\";\nimport { apiError, apiSuccess } from \"#api/error.js\";\nimport { getPublicOrigin } from \"#api/public-url.js\";\nimport { resolveSecretsCached } from \"#config/secrets.js\";\n\nexport const prerender = false;\n\nexport const POST: APIRoute = async ({ request, url, locals }) => {\n\tconst { emdash, user } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\tconst denied = requirePerm(user, \"plugins:read\");\n\tif (denied) return denied;\n\n\t// Always non-empty after resolution; env override wins, otherwise a\n\t// stable DB-stored value is used.\n\tconst { previewSecret: secret } = await resolveSecretsCached(emdash.db);\n\n\tlet body: { previewUrl: string };\n\ttry {\n\t\tbody = await request.json();\n\t} catch {\n\t\treturn apiError(\"INVALID_REQUEST\", \"Invalid JSON body\", 400);\n\t}\n\n\tif (!body.previewUrl || typeof body.previewUrl !== \"string\") {\n\t\treturn apiError(\"INVALID_REQUEST\", \"previewUrl is required\", 400);\n\t}\n\n\t// Validate previewUrl is a valid HTTPS URL\n\tlet parsedPreviewUrl: URL;\n\ttry {\n\t\tparsedPreviewUrl = new URL(body.previewUrl);\n\t} catch {\n\t\treturn apiError(\"INVALID_REQUEST\", \"previewUrl must be a valid URL\", 400);\n\t}\n\n\tif (parsedPreviewUrl.protocol !== \"https:\") {\n\t\treturn apiError(\"INVALID_REQUEST\", \"previewUrl must use HTTPS\", 400);\n\t}\n\n\tconst source = getPublicOrigin(url, emdash?.config);\n\tconst ttl = 3600; // 1 hour\n\tconst exp = Math.floor(Date.now() / 1000) + ttl;\n\n\t// HMAC-SHA256 sign: message = \"source:exp\"\n\tconst encoder = new TextEncoder();\n\tconst key = await crypto.subtle.importKey(\n\t\t\"raw\",\n\t\tencoder.encode(secret),\n\t\t{ name: \"HMAC\", hash: \"SHA-256\" },\n\t\tfalse,\n\t\t[\"sign\"],\n\t);\n\tconst buffer = await crypto.subtle.sign(\"HMAC\", key, encoder.encode(`${source}:${exp}`));\n\tconst sig = Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\n\tconst previewUrl = new URL(body.previewUrl);\n\tpreviewUrl.searchParams.set(\"source\", source);\n\tpreviewUrl.searchParams.set(\"exp\", String(exp));\n\tpreviewUrl.searchParams.set(\"sig\", sig);\n\n\treturn apiSuccess({ url: previewUrl.toString() });\n};\n"],"mappings":";;;;;;;;;;AAsBA,MAAa,YAAY;AAEzB,MAAa,OAAiB,OAAO,EAAE,SAAS,KAAK,aAAa;CACjE,MAAM,EAAE,QAAQ,SAAS;AAEzB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAGpE,MAAM,SAAS,YAAY,MAAM,eAAe;AAChD,KAAI,OAAQ,QAAO;CAInB,MAAM,EAAE,eAAe,WAAW,MAAM,qBAAqB,OAAO,GAAG;CAEvE,IAAI;AACJ,KAAI;AACH,SAAO,MAAM,QAAQ,MAAM;SACpB;AACP,SAAO,SAAS,mBAAmB,qBAAqB,IAAI;;AAG7D,KAAI,CAAC,KAAK,cAAc,OAAO,KAAK,eAAe,SAClD,QAAO,SAAS,mBAAmB,0BAA0B,IAAI;CAIlE,IAAI;AACJ,KAAI;AACH,qBAAmB,IAAI,IAAI,KAAK,WAAW;SACpC;AACP,SAAO,SAAS,mBAAmB,kCAAkC,IAAI;;AAG1E,KAAI,iBAAiB,aAAa,SACjC,QAAO,SAAS,mBAAmB,6BAA6B,IAAI;CAGrE,MAAM,SAAS,gBAAgB,KAAK,QAAQ,OAAO;CAEnD,MAAM,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK,GAD7B;CAIZ,MAAM,UAAU,IAAI,aAAa;CACjC,MAAM,MAAM,MAAM,OAAO,OAAO,UAC/B,OACA,QAAQ,OAAO,OAAO,EACtB;EAAE,MAAM;EAAQ,MAAM;EAAW,EACjC,OACA,CAAC,OAAO,CACR;CACD,MAAM,SAAS,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,QAAQ,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;CACxF,MAAM,MAAM,MAAM,KAAK,IAAI,WAAW,OAAO,GAAG,MAAM,EAAE,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG;CAE/F,MAAM,aAAa,IAAI,IAAI,KAAK,WAAW;AAC3C,YAAW,aAAa,IAAI,UAAU,OAAO;AAC7C,YAAW,aAAa,IAAI,OAAO,OAAO,IAAI,CAAC;AAC/C,YAAW,aAAa,IAAI,OAAO,IAAI;AAEvC,QAAO,WAAW,EAAE,KAAK,WAAW,UAAU,EAAE,CAAC"}