Spaces:
Running
Running
| /** | |
| * @file packages/rae1/src/dsse-pae.ts | |
| * @description Canonical DSSE v1 Pre-Authentication Encoding (PAE). | |
| * | |
| * Per https://github.com/secure-systems-lab/dsse/blob/master/protocol.md | |
| * PAE = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body | |
| * SP = 0x20 (single ASCII space) | |
| * LEN = ASCII decimal byte-length of the following field (no leading zeros, | |
| * no negatives) | |
| * type, body are UTF-8 bytes; body is the RAW envelope body, NOT base64. | |
| * | |
| * This is the single source of truth for PAE across all SZL consumers | |
| * (rae1/hmac.ts, vessels/pepr/*). Any change here is a wire-format change and | |
| * must be mirrored byte-for-byte in vessels/pepr/dsse-pae.ts until the module | |
| * is deduplicated via npm publish. | |
| * | |
| * Source of truth frozen against the DSSE spec example: for type | |
| * "http://example.com/HelloWorld" and body "hello world", the canonical PAE is | |
| * "DSSEv1 29 http://example.com/HelloWorld 11 hello world" | |
| * (see protocol.md "Test vectors"). | |
| */ | |
| /** Concatenate a list of byte arrays into a single Uint8Array. */ | |
| function concat(arrs: Uint8Array[]): Uint8Array { | |
| let total = 0; | |
| for (const a of arrs) total += a.length; | |
| const out = new Uint8Array(total); | |
| let off = 0; | |
| for (const a of arrs) { | |
| out.set(a, off); | |
| off += a.length; | |
| } | |
| return out; | |
| } | |
| /** | |
| * Compute the canonical DSSE v1 PAE over a payload type and a raw payload body. | |
| * | |
| * @param payloadType - the DSSE payloadType (e.g. "application/vnd.szl.receipt.v1+json") | |
| * @param payload - the RAW body bytes (NOT base64) | |
| * @returns the PAE byte string | |
| */ | |
| export function dsseV1Pae(payloadType: string, payload: Uint8Array): Uint8Array { | |
| const enc = new TextEncoder(); | |
| const typeBytes = enc.encode(payloadType); | |
| const sp = new Uint8Array([0x20]); | |
| const prefix = enc.encode("DSSEv1"); | |
| const lenType = enc.encode(String(typeBytes.length)); | |
| const lenBody = enc.encode(String(payload.length)); | |
| return concat([prefix, sp, lenType, sp, typeBytes, sp, lenBody, sp, payload]); | |
| } | |
| /** | |
| * Convenience: compute the canonical DSSE v1 PAE when the body is supplied as a | |
| * base64 (standard or base64url) string, as it is in a DSSE envelope's | |
| * `payload` field. The PAE is computed over the RAW decoded body bytes, per the | |
| * spec — the base64 form is never fed into the PAE. | |
| * | |
| * @param payloadType - the DSSE payloadType | |
| * @param payloadB64 - the envelope `payload` field (base64 or base64url) | |
| * @returns the PAE byte string | |
| */ | |
| export function dsseV1PaeFromBase64Body( | |
| payloadType: string, | |
| payloadB64: string | |
| ): Uint8Array { | |
| const raw = base64ToBytes(payloadB64); | |
| return dsseV1Pae(payloadType, raw); | |
| } | |
| /** | |
| * Decode a base64 or base64url string to raw bytes. | |
| * Tolerates missing padding and the URL-safe alphabet. | |
| */ | |
| export function base64ToBytes(s: string): Uint8Array { | |
| const normalized = s.replace(/-/g, "+").replace(/_/g, "/"); | |
| const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4); | |
| // Buffer is available in Node; fall back to atob in browser/edge runtimes. | |
| if (typeof Buffer !== "undefined") { | |
| return new Uint8Array(Buffer.from(padded, "base64")); | |
| } | |
| const bin = atob(padded); | |
| const out = new Uint8Array(bin.length); | |
| for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); | |
| return out; | |
| } | |