@aglyn/plugins-video-delivery 1.0.0-beta.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/video-delivery/src/lib/worker/video-worker.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DELIVERY_TOKEN_PARAM, verifyDeliveryToken } from '../delivery-token'\nimport type { R2BucketBinding, R2ObjectMetadata } from './r2-binding'\n\n/**\n * The delivery Worker (AGL-2824): serves one R2 object to the holder of a\n * token minted for it.\n *\n * `wrangler.jsonc` at the plugin's root deploys this file as the Worker named\n * `video`, with the `aglyn-video` bucket bound as `VIDEO_BUCKET` and the\n * delivery secret set as the Worker secret `MEDIA_VIDEO_DELIVERY_SECRET` —\n * the same value the platform mints with.\n *\n * ## One request\n *\n * `GET|HEAD /{key}?token={token}`. The token is verified before the bucket is\n * touched, and it must name exactly the key in the path:\n *\n * - no secret configured: `503`;\n * - a token that does not verify — malformed, altered, expired, signed with\n * another secret, or claiming a lifetime no minter issues: `403`;\n * - a genuine token for another object, or an object the bucket does not\n * hold: `404`.\n *\n * A refusal has no body, so it says nothing about which of those it was\n * beyond its status.\n *\n * ## Ranges\n *\n * A single `bytes=` range is answered `206` with `Content-Range`, the same\n * three forms the platform's CDN route honors (`a-b`, `a-`, `-n`); a range\n * past the end is `416`; anything else — several ranges, another unit,\n * malformed syntax, an `If-Range` that does not match — is ignored and the\n * whole object is sent, which RFC 9110 allows and no client can misread.\n * Every response says `Accept-Ranges: bytes`.\n *\n * ## Caching\n *\n * `private`, for at most as long as the token has left: a viewer's browser\n * may keep the ranges it fetched for the life of the URL that fetched them,\n * and no shared cache — Cloudflare's included — holds anything. So a\n * takedown that removes the object leaves nothing to purge.\n */\n\nexport interface VideoWorkerEnv {\n VIDEO_BUCKET: R2BucketBinding\n MEDIA_VIDEO_DELIVERY_SECRET?: string\n}\n\n/**\n * The same base policy the platform's media CDN sets on every response\n * (`MEDIA_CDN_BASE_CSP`), restated because the Worker bundles nothing from\n * the platform: a film opened as a top-level document can run nothing.\n */\nconst CONTENT_SECURITY_POLICY =\n \"default-src 'none'; script-src 'none'; object-src 'none'; \" +\n \"base-uri 'none'; form-action 'none'\"\n\n/** The longest a browser may keep a response, whatever the token has left. */\nconst MAX_BROWSER_CACHE_SECONDS = 60 * 60\n\n/** Headers every answer carries, refusals included. */\nfunction baseHeaders(): Headers {\n return new Headers({\n 'access-control-allow-origin': '*',\n 'x-content-type-options': 'nosniff',\n 'content-security-policy': CONTENT_SECURITY_POLICY,\n })\n}\n\nfunction refusal(status: number, extra: Record<string, string> = {}): Response {\n const headers = baseHeaders()\n headers.set('cache-control', 'no-store')\n for (const [name, value] of Object.entries(extra)) headers.set(name, value)\n return new Response(null, { status, headers })\n}\n\ntype ParsedRange = { start: number; end: number } | 'unsatisfiable' | null\n\n/**\n * One `bytes=` range against an object of `size` bytes, both ends inclusive,\n * with the platform CDN's semantics: `null` means serve the whole object.\n */\nexport function parseByteRange(header: string | null, size: number): ParsedRange {\n if (!header) return null\n const unit = /^bytes=(.*)$/i.exec(header.trim())\n if (!unit) return null\n const specs = (unit[1] ?? '').split(',')\n if (specs.length !== 1) return null\n const spec = /^(\\d*)-(\\d*)$/.exec((specs[0] ?? '').trim())\n if (!spec) return null\n const [, startRaw = '', endRaw = ''] = spec\n if (!startRaw && !endRaw) return null\n if (!startRaw) {\n const suffix = Number(endRaw)\n if (!Number.isSafeInteger(suffix)) return null\n if (suffix === 0 || size === 0) return 'unsatisfiable'\n return { start: Math.max(0, size - suffix), end: size - 1 }\n }\n const start = Number(startRaw)\n if (!Number.isSafeInteger(start)) return null\n const end = endRaw ? Number(endRaw) : size - 1\n if (!Number.isSafeInteger(end)) return null\n if (endRaw && end < start) return null\n if (start >= size) return 'unsatisfiable'\n return { start, end: Math.min(end, size - 1) }\n}\n\n/** The object key a path names, or null for one that cannot be decoded. */\nfunction keyFromPath(pathname: string): string | null {\n try {\n const key = pathname.replace(/^\\/+/, '').split('/').map(decodeURIComponent).join('/')\n return key && !key.split('/').includes('..') ? key : null\n } catch {\n return null\n }\n}\n\nfunction objectHeaders(object: R2ObjectMetadata, maxAgeSeconds: number): Headers {\n const headers = baseHeaders()\n headers.set('content-type', object.httpMetadata?.contentType || 'application/octet-stream')\n headers.set('accept-ranges', 'bytes')\n headers.set('etag', object.httpEtag)\n headers.set('cache-control', `private, max-age=${maxAgeSeconds}`)\n headers.set(\n 'access-control-expose-headers',\n 'accept-ranges, content-length, content-range, etag',\n )\n return headers\n}\n\nexport async function handleVideoRequest(\n request: Request,\n env: VideoWorkerEnv,\n nowMs: number = Date.now(),\n): Promise<Response> {\n if (request.method === 'OPTIONS') {\n const headers = baseHeaders()\n headers.set('access-control-allow-methods', 'GET, HEAD, OPTIONS')\n headers.set('access-control-allow-headers', 'range, if-range')\n headers.set('access-control-max-age', '86400')\n return new Response(null, { status: 204, headers })\n }\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n return refusal(405, { allow: 'GET, HEAD, OPTIONS' })\n }\n if (!env.MEDIA_VIDEO_DELIVERY_SECRET) return refusal(503)\n\n const url = new URL(request.url)\n const key = keyFromPath(url.pathname)\n if (!key) return refusal(404)\n const verdict = await verifyDeliveryToken(\n url.searchParams.get(DELIVERY_TOKEN_PARAM),\n env.MEDIA_VIDEO_DELIVERY_SECRET,\n { key, nowMs },\n )\n if (verdict.ok === false) {\n if (verdict.refusal === 'secret') return refusal(503)\n return refusal(verdict.refusal === 'key' ? 404 : 403)\n }\n const maxAgeSeconds = Math.max(\n 0,\n Math.min(MAX_BROWSER_CACHE_SECONDS, Math.floor((verdict.claims.expiresAtMs - nowMs) / 1000)),\n )\n\n if (request.method === 'HEAD') {\n const object = await env.VIDEO_BUCKET.head(key)\n if (!object) return refusal(404)\n const headers = objectHeaders(object, maxAgeSeconds)\n headers.set('content-length', String(object.size))\n return new Response(null, { status: 200, headers })\n }\n\n const rangeHeader = request.headers.get('range')\n if (rangeHeader) {\n const object = await env.VIDEO_BUCKET.head(key)\n if (!object) return refusal(404)\n const ifRange = request.headers.get('if-range')\n const parsed =\n ifRange && ifRange !== object.httpEtag ? null : parseByteRange(rangeHeader, object.size)\n if (parsed === 'unsatisfiable') {\n return refusal(416, { 'content-range': `bytes */${object.size}` })\n }\n if (parsed) {\n const length = parsed.end - parsed.start + 1\n const ranged = await env.VIDEO_BUCKET.get(key, {\n range: { offset: parsed.start, length },\n })\n if (!ranged) return refusal(404)\n const headers = objectHeaders(ranged, maxAgeSeconds)\n headers.set('content-range', `bytes ${parsed.start}-${parsed.end}/${ranged.size}`)\n headers.set('content-length', String(length))\n return new Response(ranged.body, { status: 206, headers })\n }\n }\n\n const object = await env.VIDEO_BUCKET.get(key)\n if (!object) return refusal(404)\n const headers = objectHeaders(object, maxAgeSeconds)\n headers.set('content-length', String(object.size))\n return new Response(object.body, { status: 200, headers })\n}\n\nexport default {\n fetch(request: Request, env: VideoWorkerEnv): Promise<Response> {\n return handleVideoRequest(request, env)\n },\n}\n"],"names":["DELIVERY_TOKEN_PARAM","verifyDeliveryToken","CONTENT_SECURITY_POLICY","MAX_BROWSER_CACHE_SECONDS","baseHeaders","Headers","refusal","status","extra","headers","set","name","value","Object","entries","Response","parseByteRange","header","size","unit","specs","exec","trim","split","length","spec","startRaw","endRaw","suffix","Number","isSafeInteger","start","Math","max","end","min","keyFromPath","pathname","key","replace","map","decodeURIComponent","join","includes","objectHeaders","object","maxAgeSeconds","httpMetadata","contentType","httpEtag","handleVideoRequest","request","env","nowMs","Date","now","method","allow","MEDIA_VIDEO_DELIVERY_SECRET","url","URL","verdict","searchParams","get","ok","floor","claims","expiresAtMs","VIDEO_BUCKET","head","String","rangeHeader","ifRange","parsed","ranged","range","offset","body","fetch"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,EAAEC,mBAAmB,QAAQ,uBAAmB;AAgD7E;;;;CAIC,GACD,MAAMC,0BACJ,+DACA;AAEF,4EAA4E,GAC5E,MAAMC,4BAA4B,KAAK;AAEvC,qDAAqD,GACrD,SAASC;IACP,OAAO,IAAIC,QAAQ;QACjB,+BAA+B;QAC/B,0BAA0B;QAC1B,2BAA2BH;IAC7B;AACF;AAEA,SAASI,QAAQC,MAAc,EAAEC,QAAgC,CAAC,CAAC;IACjE,MAAMC,UAAUL;IAChBK,QAAQC,GAAG,CAAC,iBAAiB;IAC7B,KAAK,MAAM,CAACC,MAAMC,MAAM,IAAIC,OAAOC,OAAO,CAACN,OAAQC,QAAQC,GAAG,CAACC,MAAMC;IACrE,OAAO,IAAIG,SAAS,MAAM;QAAER;QAAQE;IAAQ;AAC9C;AAIA;;;CAGC,GACD,OAAO,SAASO,eAAeC,MAAqB,EAAEC,IAAY;QAIjDC,QAEoBC;IALnC,IAAI,CAACH,QAAQ,OAAO;IACpB,MAAME,OAAO,gBAAgBE,IAAI,CAACJ,OAAOK,IAAI;IAC7C,IAAI,CAACH,MAAM,OAAO;IAClB,MAAMC,QAAQ,EAACD,SAAAA,IAAI,CAAC,EAAE,YAAPA,SAAW,IAAII,KAAK,CAAC;IACpC,IAAIH,MAAMI,MAAM,KAAK,GAAG,OAAO;IAC/B,MAAMC,OAAO,gBAAgBJ,IAAI,CAAC,EAACD,UAAAA,KAAK,CAAC,EAAE,YAARA,UAAY,IAAIE,IAAI;IACvD,IAAI,CAACG,MAAM,OAAO;IAClB,MAAM,GAAGC,WAAW,EAAE,EAAEC,SAAS,EAAE,CAAC,GAAGF;IACvC,IAAI,CAACC,YAAY,CAACC,QAAQ,OAAO;IACjC,IAAI,CAACD,UAAU;QACb,MAAME,SAASC,OAAOF;QACtB,IAAI,CAACE,OAAOC,aAAa,CAACF,SAAS,OAAO;QAC1C,IAAIA,WAAW,KAAKV,SAAS,GAAG,OAAO;QACvC,OAAO;YAAEa,OAAOC,KAAKC,GAAG,CAAC,GAAGf,OAAOU;YAASM,KAAKhB,OAAO;QAAE;IAC5D;IACA,MAAMa,QAAQF,OAAOH;IACrB,IAAI,CAACG,OAAOC,aAAa,CAACC,QAAQ,OAAO;IACzC,MAAMG,MAAMP,SAASE,OAAOF,UAAUT,OAAO;IAC7C,IAAI,CAACW,OAAOC,aAAa,CAACI,MAAM,OAAO;IACvC,IAAIP,UAAUO,MAAMH,OAAO,OAAO;IAClC,IAAIA,SAASb,MAAM,OAAO;IAC1B,OAAO;QAAEa;QAAOG,KAAKF,KAAKG,GAAG,CAACD,KAAKhB,OAAO;IAAG;AAC/C;AAEA,yEAAyE,GACzE,SAASkB,YAAYC,QAAgB;IACnC,IAAI;QACF,MAAMC,MAAMD,SAASE,OAAO,CAAC,QAAQ,IAAIhB,KAAK,CAAC,KAAKiB,GAAG,CAACC,oBAAoBC,IAAI,CAAC;QACjF,OAAOJ,OAAO,CAACA,IAAIf,KAAK,CAAC,KAAKoB,QAAQ,CAAC,QAAQL,MAAM;IACvD,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA,SAASM,cAAcC,MAAwB,EAAEC,aAAqB;QAExCD;IAD5B,MAAMpC,UAAUL;IAChBK,QAAQC,GAAG,CAAC,gBAAgBmC,EAAAA,uBAAAA,OAAOE,YAAY,qBAAnBF,qBAAqBG,WAAW,KAAI;IAChEvC,QAAQC,GAAG,CAAC,iBAAiB;IAC7BD,QAAQC,GAAG,CAAC,QAAQmC,OAAOI,QAAQ;IACnCxC,QAAQC,GAAG,CAAC,iBAAiB,CAAC,iBAAiB,EAAEoC,eAAe;IAChErC,QAAQC,GAAG,CACT,iCACA;IAEF,OAAOD;AACT;AAEA,OAAO,eAAeyC,mBACpBC,OAAgB,EAChBC,GAAmB,EACnBC,QAAgBC,KAAKC,GAAG,EAAE;IAE1B,IAAIJ,QAAQK,MAAM,KAAK,WAAW;QAChC,MAAM/C,UAAUL;QAChBK,QAAQC,GAAG,CAAC,gCAAgC;QAC5CD,QAAQC,GAAG,CAAC,gCAAgC;QAC5CD,QAAQC,GAAG,CAAC,0BAA0B;QACtC,OAAO,IAAIK,SAAS,MAAM;YAAER,QAAQ;YAAKE;QAAQ;IACnD;IACA,IAAI0C,QAAQK,MAAM,KAAK,SAASL,QAAQK,MAAM,KAAK,QAAQ;QACzD,OAAOlD,QAAQ,KAAK;YAAEmD,OAAO;QAAqB;IACpD;IACA,IAAI,CAACL,IAAIM,2BAA2B,EAAE,OAAOpD,QAAQ;IAErD,MAAMqD,MAAM,IAAIC,IAAIT,QAAQQ,GAAG;IAC/B,MAAMrB,MAAMF,YAAYuB,IAAItB,QAAQ;IACpC,IAAI,CAACC,KAAK,OAAOhC,QAAQ;IACzB,MAAMuD,UAAU,MAAM5D,oBACpB0D,IAAIG,YAAY,CAACC,GAAG,CAAC/D,uBACrBoD,IAAIM,2BAA2B,EAC/B;QAAEpB;QAAKe;IAAM;IAEf,IAAIQ,QAAQG,EAAE,KAAK,OAAO;QACxB,IAAIH,QAAQvD,OAAO,KAAK,UAAU,OAAOA,QAAQ;QACjD,OAAOA,QAAQuD,QAAQvD,OAAO,KAAK,QAAQ,MAAM;IACnD;IACA,MAAMwC,gBAAgBd,KAAKC,GAAG,CAC5B,GACAD,KAAKG,GAAG,CAAChC,2BAA2B6B,KAAKiC,KAAK,CAAC,AAACJ,CAAAA,QAAQK,MAAM,CAACC,WAAW,GAAGd,KAAI,IAAK;IAGxF,IAAIF,QAAQK,MAAM,KAAK,QAAQ;QAC7B,MAAMX,SAAS,MAAMO,IAAIgB,YAAY,CAACC,IAAI,CAAC/B;QAC3C,IAAI,CAACO,QAAQ,OAAOvC,QAAQ;QAC5B,MAAMG,UAAUmC,cAAcC,QAAQC;QACtCrC,QAAQC,GAAG,CAAC,kBAAkB4D,OAAOzB,OAAO3B,IAAI;QAChD,OAAO,IAAIH,SAAS,MAAM;YAAER,QAAQ;YAAKE;QAAQ;IACnD;IAEA,MAAM8D,cAAcpB,QAAQ1C,OAAO,CAACsD,GAAG,CAAC;IACxC,IAAIQ,aAAa;QACf,MAAM1B,SAAS,MAAMO,IAAIgB,YAAY,CAACC,IAAI,CAAC/B;QAC3C,IAAI,CAACO,QAAQ,OAAOvC,QAAQ;QAC5B,MAAMkE,UAAUrB,QAAQ1C,OAAO,CAACsD,GAAG,CAAC;QACpC,MAAMU,SACJD,WAAWA,YAAY3B,OAAOI,QAAQ,GAAG,OAAOjC,eAAeuD,aAAa1B,OAAO3B,IAAI;QACzF,IAAIuD,WAAW,iBAAiB;YAC9B,OAAOnE,QAAQ,KAAK;gBAAE,iBAAiB,CAAC,QAAQ,EAAEuC,OAAO3B,IAAI,EAAE;YAAC;QAClE;QACA,IAAIuD,QAAQ;YACV,MAAMjD,SAASiD,OAAOvC,GAAG,GAAGuC,OAAO1C,KAAK,GAAG;YAC3C,MAAM2C,SAAS,MAAMtB,IAAIgB,YAAY,CAACL,GAAG,CAACzB,KAAK;gBAC7CqC,OAAO;oBAAEC,QAAQH,OAAO1C,KAAK;oBAAEP;gBAAO;YACxC;YACA,IAAI,CAACkD,QAAQ,OAAOpE,QAAQ;YAC5B,MAAMG,UAAUmC,cAAc8B,QAAQ5B;YACtCrC,QAAQC,GAAG,CAAC,iBAAiB,CAAC,MAAM,EAAE+D,OAAO1C,KAAK,CAAC,CAAC,EAAE0C,OAAOvC,GAAG,CAAC,CAAC,EAAEwC,OAAOxD,IAAI,EAAE;YACjFT,QAAQC,GAAG,CAAC,kBAAkB4D,OAAO9C;YACrC,OAAO,IAAIT,SAAS2D,OAAOG,IAAI,EAAE;gBAAEtE,QAAQ;gBAAKE;YAAQ;QAC1D;IACF;IAEA,MAAMoC,SAAS,MAAMO,IAAIgB,YAAY,CAACL,GAAG,CAACzB;IAC1C,IAAI,CAACO,QAAQ,OAAOvC,QAAQ;IAC5B,MAAMG,UAAUmC,cAAcC,QAAQC;IACtCrC,QAAQC,GAAG,CAAC,kBAAkB4D,OAAOzB,OAAO3B,IAAI;IAChD,OAAO,IAAIH,SAAS8B,OAAOgC,IAAI,EAAE;QAAEtE,QAAQ;QAAKE;IAAQ;AAC1D;AAEA,eAAe;IACbqE,OAAM3B,OAAgB,EAAEC,GAAmB;QACzC,OAAOF,mBAAmBC,SAASC;IACrC;AACF,EAAC"}