// The reference producer of ad-presentation claims (wai/extensions/ad-presentation.md §10), // against the corpus wai/live-conformance/presentation/ (`node --test web/ads/presentation.test.mjs`): // observation sequences recorded into spans, and the claims written and signed from them, // byte for byte as the corpus holds them. import test from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import * as ap from "./presentation.js"; const doc = JSON.parse(readFileSync(new URL("../../../../../../wai/live-conformance/presentation/vectors.json", import.meta.url), "utf8")); const vector = (name) => doc.vectors.find((v) => v.name === name); const hex = (s) => Uint8Array.from(s.match(/../g) ?? [], (h) => parseInt(h, 16)); const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); const creative = (k) => doc.creatives[k].objects.map((o) => ({ contentHash: hex(o.content_hash), durationMs: o.duration_ms })); /** Session key `i` of the corpus: SHA-256 of its label and `i` (its README). */ const key = async (i) => ap.sessionKeyFromSeed(await ap.sha256(new TextEncoder().encode("wai:ad-presentation/corpus/player\x00"), Uint8Array.of(i))); /** Observations every 100 ms of media time from `from` to `to`, playing at normal speed. */ function play(rec, from, to, visibleBp, audible = true) { for (let p = from; p <= to; p += 100) rec.observe({ positionMs: p, playing: true, visibleBp, audible }); } async function claimFrom(vectorName, i, slot, objects, spans, geometry = ap.GEOMETRY.AD) { const v = vector(vectorName); const k = await key(i); const room = hex(v.room); const c = { room, cue: hex(v.breaks[0].digest), session: await ap.sessionOf(room, k.publicKey), slot, creativeHash: await ap.creativeDigest(objects), player: k.publicKey, geometry, frames: ap.NOT_EXPOSED, objects: ap.touched(objects, spans), spans, }; return toHex(await ap.signClaim(c, k.privateKey)); } test("a viewer in view and audible throughout: one span, the corpus's claim", async () => { const rec = new ap.SpanRecorder(); play(rec, 0, 15_000, 10_000); rec.observe({ positionMs: 15_000, playing: false, visibleBp: 10_000, audible: true }); // ended const spans = rec.spans(); assert.deepEqual(spans, [{ startMs: 0, lengthMs: 15_000, visibleBp: 10_000, flags: ap.AUDIBLE }]); assert.equal(await claimFrom("video_rule", 1, 0, creative(0), spans), vector("video_rule").claims[0]); }); test("scrolled partly out of view: a span at each change, the least fraction of each", async () => { const rec = new ap.SpanRecorder(); play(rec, 0, 3_900, 3_000); play(rec, 4_000, 4_000, 6_000); play(rec, 4_100, 6_900, 6_050); // within the same whole percent as 60.00%: one span, its least play(rec, 7_000, 12_000, 2_000); rec.observe({ positionMs: 12_000, playing: false, visibleBp: 2_000, audible: true }); const spans = rec.spans(); assert.deepEqual(spans.map((s) => [s.startMs, s.lengthMs, s.visibleBp, s.flags]), [[0, 4_000, 3_000, 1], [4_000, 3_000, 6_000, 1], [7_000, 5_000, 2_000, 1]]); assert.equal(await claimFrom("video_rule", 1, 1, creative(1), spans), vector("video_rule").claims[1]); }); test("a pause: the span after it is a resumption, and the run is broken", async () => { const rec = new ap.SpanRecorder(); play(rec, 0, 1_500, 10_000); rec.observe({ positionMs: 1_500, playing: false, visibleBp: 10_000, audible: true }); play(rec, 1_500, 2_900, 10_000); play(rec, 3_000, 4_000, 1_000); rec.observe({ positionMs: 4_000, playing: false, visibleBp: 1_000, audible: true }); const spans = rec.spans(); assert.deepEqual(spans.map((s) => [s.startMs, s.lengthMs, s.flags]), [[0, 1_500, 1], [1_500, 1_500, 3], [3_000, 1_000, 1]]); assert.equal(await claimFrom("video_rule", 2, 1, creative(1), spans), vector("video_rule").claims[3]); }); test("a seek back states no media time twice, and play past it resumes", () => { const rec = new ap.SpanRecorder(); play(rec, 0, 5_000, 10_000); rec.observe({ positionMs: 2_000, playing: true, visibleBp: 10_000, audible: true, seeked: true }); play(rec, 2_100, 6_000, 10_000); const spans = rec.spans(); assert.deepEqual(spans.map((s) => [s.startMs, s.lengthMs, s.flags]), [[0, 5_000, 1], [5_000, 1_000, 3]]); }); test("the attestation challenge is the corpus's: an attested claim's token answers it", async () => { const v = vector("cross_media_attested"); const b = hex(v.claims[0]); // The claim's fields at their offsets (§5.1), and its token: the long field before the signature. const c = { room: b.subarray(5, 37), cue: b.subarray(37, 69), session: b.subarray(69, 85), slot: b[85], creativeHash: b.subarray(86, 118), player: b.subarray(118, 150) }; const tokenLen = 98 + 256; const token = b.subarray(b.length - 64 - tokenLen, b.length - 64); const challenge = ap.tokenChallenge(2, doc.issuers[0].name, await ap.redemptionContext(c)); assert.equal(toHex(token.subarray(34, 66)), toHex(await ap.sha256(challenge))); assert.equal(toHex(await ap.sessionOf(c.room, c.player)), toHex(c.session)); });