Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,12 @@ function parseExemptionRecord(value: unknown): ExemptionRecord {
throw new Error('invalid exemption record');
}

const { advertiserHost, policyIds, networkIds, grantedAt } = value;
const { advertiserHost, policyIds, networkIds, grantedAt, expiresAt } = value;

if (
typeof advertiserHost !== 'string' ||
typeof grantedAt !== 'number' ||
(expiresAt !== undefined && typeof expiresAt !== 'number') ||
!Array.isArray(policyIds) ||
!policyIds.every((id) => typeof id === 'string') ||
!Array.isArray(networkIds) ||
Expand All @@ -397,12 +398,18 @@ function parseExemptionRecord(value: unknown): ExemptionRecord {
throw new Error('invalid exemption record');
}

return {
const record: ExemptionRecord = {
advertiserHost,
policyIds: [...policyIds],
networkIds: [...networkIds],
grantedAt,
};

if (expiresAt !== undefined) {
record.expiresAt = expiresAt;
}

return record;
}

function parseSessionRecord(value: unknown): SessionRecord {
Expand Down
51 changes: 51 additions & 0 deletions tests/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
validatePolicy,
} from '../src';
import { cjPolicy } from '../src/policies';
import { SessionStorageStateStore, type WebStorageLike } from '../src/stores';

const behaviors = [
'suppress-prompts',
Expand Down Expand Up @@ -404,6 +405,40 @@ describe('StanddownSession', () => {
).resolves.toMatchObject({ standDown: false });
});

it('bounds a session self-exemption by the default TTL across a persisted reload', async () => {
const storage = new FakeWebStorage();
const selfPatterns = [
{ name: 'cjevent', value: 'own', match: 'equals' as const, policyId: 'cj' },
];

const first = new StanddownSession(new SessionStorageStateStore(storage), {
selfExemptionScope: 'session',
});
await first.ingest(
{ url: 'https://merchant.example/?cjevent=own', now: 0, selfPatterns },
[cjPolicy],
);

// A fresh session over the same persisted state: the exemption is reloaded
// through the store's parser rather than held in memory.
const revived = new StanddownSession(new SessionStorageStateStore(storage), {
selfExemptionScope: 'session',
});

// Past the 30-minute default TTL the exemption has lapsed, so the lingering
// cookie stands down exactly as it does without persistence.
await expect(
revived.ingest(
{
url: 'https://merchant.example/checkout',
now: 1_800_001,
cookieNames: ['cjevent_dc'],
},
[cjPolicy],
),
).resolves.toMatchObject({ standDown: true, policyId: 'cj' });
});

it('validates the test policy fixture', () => {
expect(() =>
validatePolicy(
Expand Down Expand Up @@ -607,3 +642,19 @@ function testPolicy(standdown: {
function clonePolicy(policy: StanddownPolicy): StanddownPolicy {
return JSON.parse(JSON.stringify(policy)) as StanddownPolicy;
}

class FakeWebStorage implements WebStorageLike {
readonly #items = new Map<string, string>();

getItem(key: string): string | null {
return this.#items.get(key) ?? null;
}

setItem(key: string, value: string): void {
this.#items.set(key, value);
}

removeItem(key: string): void {
this.#items.delete(key);
}
}
26 changes: 26 additions & 0 deletions tests/stores.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,32 @@ describe('StateStore implementations', () => {
]);
});

it('round-trips a session exemption expiresAt through persistence', async () => {
const local = new FakeChromeStorageArea();
await new ChromeLocalStateStore(local, {
sessionId: 'browser-session-1',
now: () => 0,
}).save({
...stateWithSessionAndInactivityRecords(),
exemptions: {
'session.example': {
advertiserHost: 'session.example',
policyIds: ['alfa'],
networkIds: ['alfa'],
grantedAt: 0,
expiresAt: 1_800_000,
},
},
});

const loaded = await new ChromeLocalStateStore(local, {
sessionId: 'browser-session-1',
now: () => 1_000,
}).load();

expect(loaded?.exemptions?.['session.example']?.expiresAt).toBe(1_800_000);
});

it('drops session exemptions when the browser session changes', async () => {
const local = new FakeChromeStorageArea();
await new ChromeLocalStateStore(local, {
Expand Down