diff --git a/packages/audience/sdk/src/consent.test.ts b/packages/audience/sdk/src/consent.test.ts new file mode 100644 index 0000000000..626950f39b --- /dev/null +++ b/packages/audience/sdk/src/consent.test.ts @@ -0,0 +1,112 @@ +import type { ConsentTransport } from './consent'; +import { ConsentManager } from './consent'; + +function createMockTransport(): ConsentTransport & { calls: any[] } { + const calls: any[] = []; + return { + calls, + async syncConsent(url, publishableKey, body) { + calls.push({ url, publishableKey, body }); + }, + }; +} + +describe('ConsentManager', () => { + it('initialises with the provided consent level', () => { + const transport = createMockTransport(); + const manager = new ConsentManager('sandbox', 'pk_test', 'anonymous', 'TestSDK', transport); + expect(manager.getLevel()).toBe('anonymous'); + }); + + it('calls onPurgeQueue and onClearIdentity when downgrading to none', () => { + const transport = createMockTransport(); + const manager = new ConsentManager('sandbox', 'pk_test', 'full', 'TestSDK', transport); + const onPurge = jest.fn(); + const onClear = jest.fn(); + + manager.setLevel('none', 'anon-123', { + onPurgeQueue: onPurge, + onClearIdentity: onClear, + }); + + expect(onPurge).toHaveBeenCalled(); + expect(onClear).toHaveBeenCalled(); + expect(manager.getLevel()).toBe('none'); + }); + + it('calls onStripIdentity when downgrading from full to anonymous', () => { + const transport = createMockTransport(); + const manager = new ConsentManager('sandbox', 'pk_test', 'full', 'TestSDK', transport); + const onStrip = jest.fn(); + + manager.setLevel('anonymous', 'anon-123', { onStripIdentity: onStrip }); + + expect(onStrip).toHaveBeenCalled(); + expect(manager.getLevel()).toBe('anonymous'); + }); + + it('does not call onStripIdentity when upgrading from anonymous to full', () => { + const transport = createMockTransport(); + const manager = new ConsentManager('sandbox', 'pk_test', 'anonymous', 'TestSDK', transport); + const onStrip = jest.fn(); + + manager.setLevel('full', 'anon-123', { onStripIdentity: onStrip }); + + expect(onStrip).not.toHaveBeenCalled(); + expect(manager.getLevel()).toBe('full'); + }); + + it('syncs consent to server via transport on setLevel', () => { + const transport = createMockTransport(); + const manager = new ConsentManager('sandbox', 'pk_test', 'none', 'TestSDK', transport); + manager.setLevel('full', 'anon-123'); + + expect(transport.calls).toHaveLength(1); + expect(transport.calls[0]).toEqual({ + url: 'https://api.sandbox.immutable.com/v1/audience/tracking-consent', + publishableKey: 'pk_test', + body: { + anonymousId: 'anon-123', + status: 'full', + source: 'TestSDK', + }, + }); + }); + + it('truncates source to 128 characters', () => { + const transport = createMockTransport(); + const longSource = 'x'.repeat(200); + const manager = new ConsentManager('sandbox', 'pk_test', 'none', longSource, transport); + manager.setLevel('full', 'anon-123'); + + expect(transport.calls[0].body.source).toHaveLength(128); + }); + + it('uses correct base URL per environment', () => { + const devTransport = createMockTransport(); + const devManager = new ConsentManager('dev', 'pk_test', 'none', 'SDK', devTransport); + devManager.setLevel('full', 'anon-123'); + expect(devTransport.calls[0].url).toContain('api.dev.immutable.com'); + + const prodTransport = createMockTransport(); + const prodManager = new ConsentManager('production', 'pk_test', 'none', 'SDK', prodTransport); + prodManager.setLevel('full', 'anon-123'); + expect(prodTransport.calls[0].url).toContain('api.immutable.com'); + }); + + it('does not throw when transport rejects', async () => { + const transport: ConsentTransport = { + async syncConsent() { + throw new Error('network error'); + }, + }; + const manager = new ConsentManager('sandbox', 'pk_test', 'none', 'SDK', transport); + + // Should not throw — fire-and-forget + expect(() => manager.setLevel('full', 'anon-123')).not.toThrow(); + + // Let the rejected promise settle + await Promise.resolve(); + await Promise.resolve(); + }); +}); diff --git a/packages/audience/sdk/src/consent.ts b/packages/audience/sdk/src/consent.ts new file mode 100644 index 0000000000..c0add632ea --- /dev/null +++ b/packages/audience/sdk/src/consent.ts @@ -0,0 +1,91 @@ +import type { + ConsentLevel, + Environment, +} from '@imtbl/audience-core'; +import { + CONSENT_PATH, + getBaseUrl, + truncateSource, +} from '@imtbl/audience-core'; + +/** Pluggable transport for syncing consent state to the server. */ +export interface ConsentTransport { + syncConsent( + url: string, + publishableKey: string, + body: { anonymousId: string; status: ConsentLevel; source: string }, + ): Promise; +} + +/** Side-effect callbacks invoked by the consent state machine on transitions. */ +export interface ConsentCallbacks { + /** Called on any downgrade to 'none' — stop queue, purge all messages. */ + onPurgeQueue?: () => void; + /** Called on full → anonymous — remove identify/alias, strip userId. */ + onStripIdentity?: () => void; + /** Called on any downgrade to 'none' — clear persisted identity (cookies, storage). */ + onClearIdentity?: () => void; +} + +/** + * Consent state machine shared across all surface SDKs. + * + * Owns the three-tier consent model (none/anonymous/full) and transition + * semantics. Platform-specific I/O (HTTP transport, cookie/storage clearing) + * is injected via ConsentTransport and ConsentCallbacks. + */ +export class ConsentManager { + private level: ConsentLevel; + + private readonly baseUrl: string; + + private readonly source: string; + + constructor( + environment: Environment, + private readonly publishableKey: string, + initialConsent: ConsentLevel, + rawSource: string, + private readonly transport: ConsentTransport, + ) { + this.baseUrl = getBaseUrl(environment); + this.source = truncateSource(rawSource); + this.level = initialConsent; + } + + getLevel(): ConsentLevel { + return this.level; + } + + setLevel( + level: ConsentLevel, + anonymousId: string, + callbacks?: ConsentCallbacks, + ): void { + const previous = this.level; + this.level = level; + + // Downgrade: any → none — purge everything + clear persisted identity + if (level === 'none') { + callbacks?.onPurgeQueue?.(); + callbacks?.onClearIdentity?.(); + } else if (level === 'anonymous' && previous === 'full') { + // Downgrade: full → anonymous — strip PII, keep anonymous events + callbacks?.onStripIdentity?.(); + } + + // Sync to server (fire-and-forget) + this.syncToServer(anonymousId, level); + } + + private syncToServer(anonymousId: string, status: ConsentLevel): void { + const url = `${this.baseUrl}${CONSENT_PATH}`; + this.transport.syncConsent(url, this.publishableKey, { + anonymousId, + status, + source: this.source, + }).catch(() => { + // Fire-and-forget — transport implementation handles error logging + }); + } +} diff --git a/packages/audience/sdk/src/context.test.ts b/packages/audience/sdk/src/context.test.ts deleted file mode 100644 index 2444425e0e..0000000000 --- a/packages/audience/sdk/src/context.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { collectContext } from './context'; -import { LIBRARY_NAME, LIBRARY_VERSION } from './config'; - -describe('collectContext', () => { - it('should return context with SDK library name and version', () => { - const ctx = collectContext(); - - expect(ctx.library).toBe(LIBRARY_NAME); - expect(ctx.libraryVersion).toBe(LIBRARY_VERSION); - }); -}); diff --git a/packages/audience/sdk/src/context.ts b/packages/audience/sdk/src/context.ts deleted file mode 100644 index 7fec3e8ef6..0000000000 --- a/packages/audience/sdk/src/context.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { EventContext } from '@imtbl/audience-core'; -import { collectContext as coreCollectContext } from '@imtbl/audience-core'; -import { LIBRARY_NAME, LIBRARY_VERSION } from './config'; - -export function collectContext(): EventContext { - return coreCollectContext(LIBRARY_NAME, LIBRARY_VERSION); -} diff --git a/packages/audience/sdk/src/debug.test.ts b/packages/audience/sdk/src/debug.test.ts index afecf4bb04..ac6c8c41d4 100644 --- a/packages/audience/sdk/src/debug.test.ts +++ b/packages/audience/sdk/src/debug.test.ts @@ -1,5 +1,5 @@ import type { Message } from '@imtbl/audience-core'; -import { DebugLogger } from './debug'; +import { DebugLogger, LOG_PREFIX } from './debug'; describe('DebugLogger', () => { let logSpy: jest.SpyInstance; @@ -22,7 +22,7 @@ describe('DebugLogger', () => { anonymousId: 'anon-1', surface: 'web', context: { library: 'test', libraryVersion: '0.0.0' }, - event: 'click', + eventName: 'click', properties: {}, }; @@ -42,7 +42,7 @@ describe('DebugLogger', () => { logger.logEvent('track', stubMessage); expect(logSpy).toHaveBeenCalledWith( - '[Immutable Audience] track', + `${LOG_PREFIX} track`, stubMessage, ); }); @@ -52,12 +52,12 @@ describe('DebugLogger', () => { logger.logFlush(true, 5); expect(logSpy).toHaveBeenCalledWith( - '[Immutable Audience] flush ok (5 messages)', + `${LOG_PREFIX} flush ok (5 messages)`, ); logger.logFlush(false, 3); expect(logSpy).toHaveBeenCalledWith( - '[Immutable Audience] flush failed (3 messages)', + `${LOG_PREFIX} flush failed (3 messages)`, ); }); @@ -66,7 +66,7 @@ describe('DebugLogger', () => { logger.logConsent('none', 'full'); expect(logSpy).toHaveBeenCalledWith( - '[Immutable Audience] consent none → full', + `${LOG_PREFIX} consent none \u2192 full`, ); }); @@ -75,7 +75,7 @@ describe('DebugLogger', () => { logger.logWarning('something went wrong'); expect(warnSpy).toHaveBeenCalledWith( - '[Immutable Audience] something went wrong', + `${LOG_PREFIX} something went wrong`, ); }); }); diff --git a/packages/audience/sdk/src/debug.ts b/packages/audience/sdk/src/debug.ts index 7d67bb335e..dfba00d44e 100644 --- a/packages/audience/sdk/src/debug.ts +++ b/packages/audience/sdk/src/debug.ts @@ -1,6 +1,6 @@ import type { ConsentLevel, Message } from '@imtbl/audience-core'; -const PREFIX = '[Immutable Audience]'; +export const LOG_PREFIX = '[audience-sdk]'; export class DebugLogger { private enabled: boolean; @@ -12,24 +12,24 @@ export class DebugLogger { logEvent(method: string, message: Message): void { if (!this.enabled) return; // eslint-disable-next-line no-console - console.log(`${PREFIX} ${method}`, message); + console.log(`${LOG_PREFIX} ${method}`, message); } logFlush(ok: boolean, count: number): void { if (!this.enabled) return; // eslint-disable-next-line no-console - console.log(`${PREFIX} flush ${ok ? 'ok' : 'failed'} (${count} messages)`); + console.log(`${LOG_PREFIX} flush ${ok ? 'ok' : 'failed'} (${count} messages)`); } logConsent(from: ConsentLevel, to: ConsentLevel): void { if (!this.enabled) return; // eslint-disable-next-line no-console - console.log(`${PREFIX} consent ${from} → ${to}`); + console.log(`${LOG_PREFIX} consent ${from} → ${to}`); } logWarning(msg: string): void { if (!this.enabled) return; // eslint-disable-next-line no-console - console.warn(`${PREFIX} ${msg}`); + console.warn(`${LOG_PREFIX} ${msg}`); } } diff --git a/packages/audience/sdk/src/index.ts b/packages/audience/sdk/src/index.ts index 23dad10615..3d0a2b321e 100644 --- a/packages/audience/sdk/src/index.ts +++ b/packages/audience/sdk/src/index.ts @@ -1,3 +1,4 @@ export type { AudienceSDKConfig } from './types'; export { DebugLogger } from './debug'; -export { collectContext } from './context'; +export { ConsentManager } from './consent'; +export type { ConsentTransport, ConsentCallbacks } from './consent'; diff --git a/packages/audience/sdk/src/types.ts b/packages/audience/sdk/src/types.ts index 2126c90c57..bc1a1cf250 100644 --- a/packages/audience/sdk/src/types.ts +++ b/packages/audience/sdk/src/types.ts @@ -1,13 +1,22 @@ import type { Environment, ConsentLevel } from '@imtbl/audience-core'; -/** Configuration for the Immutable Audience SDK. */ +/** + * Base configuration shared by all Audience surface SDKs. + * Surface-specific configs (e.g. WebSDKConfig) extend this. + */ export interface AudienceSDKConfig { + /** Publishable API key from Immutable Hub (pk_imtbl_...). */ publishableKey: string; + /** Target environment — controls which backend receives events. */ environment: Environment; - /** Defaults to 'none' — no tracking until explicitly opted in. */ + /** Initial consent level. Defaults to 'none' (no tracking until opted in). */ consent?: ConsentLevel; + /** Enable console logging of all events, flushes, and consent changes. */ debug?: boolean; + /** Cookie domain for cross-subdomain sharing (e.g. '.studio.com'). */ cookieDomain?: string; + /** Queue flush interval in milliseconds. Defaults to 5000. */ flushInterval?: number; + /** Number of queued messages that triggers an automatic flush. Defaults to 20. */ flushSize?: number; } diff --git a/packages/audience/web/.eslintrc.cjs b/packages/audience/web/.eslintrc.cjs new file mode 100644 index 0000000000..b48069b5a7 --- /dev/null +++ b/packages/audience/web/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + extends: ['../../../.eslintrc'], + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: __dirname, + }, +}; diff --git a/packages/audience/web/README.md b/packages/audience/web/README.md new file mode 100644 index 0000000000..512e6f1d1b --- /dev/null +++ b/packages/audience/web/README.md @@ -0,0 +1,153 @@ +# @imtbl/audience-web-sdk + +Track player activity on your website — page views, purchases, sign-ups — and tie it to player identity when they log in. + +## Install + +```bash +npm install @imtbl/audience-web-sdk +``` + +## Quick Start + +```typescript +import { ImmutableWebSDK } from '@imtbl/audience-web-sdk'; + +const sdk = ImmutableWebSDK.init({ + publishableKey: 'pk_imtbl_...', + environment: 'production', + consent: 'anonymous', +}); + +sdk.track('purchase', { currency: 'USD', value: 9.99, itemId: 'sword_01' }); +sdk.page(); +``` + +## Initialisation + +```typescript +const sdk = ImmutableWebSDK.init({ + publishableKey: 'pk_imtbl_...', // Required — from Immutable Hub + environment: 'production', // 'dev' | 'sandbox' | 'production' + consent: 'none', // 'none' | 'anonymous' | 'full' (default: 'none') + consentSource: 'CookieBannerV2', // Identifies the consent source (default: 'WebSDK') + debug: false, // Log all events to console (default: false) + cookieDomain: '.studio.com', // Cross-subdomain cookie sharing (optional) + flushInterval: 5000, // Queue flush interval in ms (default: 5000) + flushSize: 20, // Queue flush size threshold (default: 20) +}); +``` + +## Consent + +The SDK defaults to `none` — no events are collected until consent is explicitly set. + +```typescript +sdk.setConsent('anonymous'); // Anonymous tracking (no PII) +sdk.setConsent('full'); // Full tracking (PII via identify) +sdk.setConsent('none'); // Stop tracking, purge queue, clear cookies +``` + +Update consent whenever your consent management platform reports a change. Every call syncs the new level to the server via `PUT /v1/audience/tracking-consent`. + +| Level | Behaviour | +|-------|-----------| +| `none` | SDK is inert. No events collected. Queue purged on downgrade. | +| `anonymous` | Events collected with anonymous ID only. `identify()` calls are discarded. | +| `full` | Full collection. `identify()` sends. `userId` included on events. | + +**On downgrade to `none`:** queue purged, `imtbl_anon_id` and `_imtbl_sid` cookies cleared. +**On downgrade from `full` to `anonymous`:** identify messages purged, `userId` stripped from queued events. + +## Auto-Tracked Events + +The SDK automatically fires these events. Studios do not call them. + +| Event | When | Properties | +|-------|------|------------| +| `session_start` | SDK init with no active session cookie | `sessionId`, plus attribution (UTMs, click IDs, referrer) | +| `session_end` | `shutdown()` called | `sessionId`, `duration` (seconds) | + +`session_end` only fires on explicit `shutdown()` calls — not on tab close or navigation. Compute session duration from timestamp gaps between the last event and `session_start`, not from `session_end` alone. + +## Event Tracking + +```typescript +sdk.track('sign_up', { method: 'google' }); +sdk.track('purchase', { currency: 'USD', value: 9.99 }); +sdk.track('wishlist_add', { gameId: 'game_123', source: 'landing_page' }); +sdk.track('beta_key_redeemed', { source: 'influencer' }); +``` + +## Page Tracking + +Call `sdk.page()` on route changes. Attribution context (UTMs, click IDs, referrer, landing page) is automatically attached to the first page view. + +```typescript +sdk.page(); +sdk.page({ section: 'shop', category: 'weapons' }); +``` + +## Identity + +```typescript +// Identify a known user (requires full consent) +sdk.identify('user@example.com', 'email'); +sdk.identify('76561198012345', 'steam'); +sdk.identify('passport_sub_abc', 'passport', { + email: 'user@example.com', + name: 'Player One', +}); + +// Identify with traits only (anonymous, no userId) +sdk.identify({ source: 'steam', steamId: '76561198012345' }); + +// Link two identities (same player, different providers) +sdk.alias( + { uid: '76561198012345', provider: 'steam' }, + { uid: 'user@example.com', provider: 'email' }, +); + +// Reset on logout (new anonymous ID, clears userId) +sdk.reset(); +``` + +## Queue & Lifecycle + +```typescript +await sdk.flush(); // Force flush all queued events +sdk.shutdown(); // Flush remaining events, stop the SDK +``` + +Events are batched and flushed every 5 seconds or when 20 messages accumulate. On page unload (`visibilitychange` / `pagehide`), remaining events are flushed via `fetch` with `keepalive: true`. + +## CDN Usage + +For sites without a bundler: + +```html + + +``` + +## Cookies + +All cookies are first-party, `SameSite=Lax`, `Secure` on HTTPS, and shared with the pixel: + +| Cookie | Lifetime | Purpose | +|--------|----------|---------| +| `imtbl_anon_id` | 2 years | Anonymous device ID | +| `_imtbl_sid` | 30 min (rolling) | Session continuity | + +## Wire Format + +Events are sent to `POST /v1/audience/messages` with the `x-immutable-publishable-key` header. All messages include `surface: 'web'` and follow the backend OpenAPI spec. diff --git a/packages/audience/web/demo/index.html b/packages/audience/web/demo/index.html new file mode 100644 index 0000000000..b9792e569a --- /dev/null +++ b/packages/audience/web/demo/index.html @@ -0,0 +1,293 @@ + + + + + + Audience Web SDK — Demo + + + + + +

Audience Web SDK — Demo

+

End-to-end testing against dev/sandbox backend. Open DevTools Network tab to see requests.

+ +
+ Environment: + Consent: + Anonymous ID: + User ID: none +
+ + +
+

1. Initialise

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ + +
+
+ + +
+

2. Consent

+
+ + + +
+
+ + +
+

3. Page Tracking

+
+ + +
+
+ + +
+

4. Track Events

+
+ + + + + + + +
+
+
+ + +
+ +
+
+ + +
+

5. Identity

+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+

6. Queue

+
+ +
+
+ + +
+

Event Log

+
+ +
+ + + + + diff --git a/packages/audience/web/jest.config.ts b/packages/audience/web/jest.config.ts new file mode 100644 index 0000000000..faf109b6c1 --- /dev/null +++ b/packages/audience/web/jest.config.ts @@ -0,0 +1,16 @@ +import type { Config } from 'jest'; + +const config: Config = { + roots: ['/src'], + moduleDirectories: ['node_modules', 'src'], + testEnvironment: 'jsdom', + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, + moduleNameMapper: { + '^@imtbl/audience-core$': '/../core/src/index.ts', + '^@imtbl/audience-sdk$': '/../sdk/src/index.ts', + }, +}; + +export default config; diff --git a/packages/audience/web/package.json b/packages/audience/web/package.json new file mode 100644 index 0000000000..268f9d3210 --- /dev/null +++ b/packages/audience/web/package.json @@ -0,0 +1,62 @@ +{ + "name": "@imtbl/audience-web-sdk", + "description": "Immutable Audience Web SDK — consent-aware event tracking and identity management", + "version": "0.0.0", + "author": "Immutable", + "bugs": "https://github.com/immutable/ts-immutable-sdk/issues", + "dependencies": { + "@imtbl/audience-core": "workspace:*", + "@imtbl/audience-sdk": "workspace:*" + }, + "devDependencies": { + "@swc/core": "^1.4.2", + "@swc/jest": "^0.2.37", + "@types/jest": "^29.5.12", + "@types/node": "^22.10.7", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.4.3", + "ts-jest": "^29.1.0", + "tsup": "^8.3.0", + "typescript": "^5.6.2" + }, + "engines": { + "node": ">=20.11.0" + }, + "exports": { + "development": { + "types": "./src/index.ts", + "browser": "./dist/browser/index.js", + "require": "./dist/node/index.cjs", + "default": "./dist/node/index.js" + }, + "default": { + "types": "./dist/types/index.d.ts", + "browser": "./dist/browser/index.js", + "require": "./dist/node/index.cjs", + "default": "./dist/node/index.js" + } + }, + "files": ["dist"], + "homepage": "https://github.com/immutable/ts-immutable-sdk#readme", + "main": "dist/node/index.cjs", + "module": "dist/node/index.js", + "browser": "dist/browser/index.js", + "publishConfig": { + "access": "public" + }, + "repository": "immutable/ts-immutable-sdk.git", + "scripts": { + "build": "pnpm transpile && pnpm transpile:cdn && pnpm typegen", + "transpile": "tsup src/index.ts --config ../../../tsup.config.js", + "transpile:cdn": "tsup --config tsup.cdn.js", + "typegen": "tsc --customConditions default --emitDeclarationOnly --outDir dist/types", + "lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0", + "test": "jest --passWithNoTests", + "test:watch": "jest --watch", + "demo": "pnpm build && npx serve -l 3456 --cors ..", + "typecheck": "tsc --customConditions development --noEmit --jsx preserve" + }, + "type": "module", + "types": "./dist/types/index.d.ts" +} diff --git a/packages/audience/web/src/attribution.test.ts b/packages/audience/web/src/attribution.test.ts new file mode 100644 index 0000000000..7666e50690 --- /dev/null +++ b/packages/audience/web/src/attribution.test.ts @@ -0,0 +1,132 @@ +import { parseAttribution } from './attribution'; + +const originalLocation = window.location; + +beforeEach(() => { + sessionStorage.clear(); +}); + +afterEach(() => { + Object.defineProperty(window, 'location', { + value: originalLocation, + writable: true, + configurable: true, + }); + Object.defineProperty(document, 'referrer', { + value: '', + configurable: true, + }); +}); + +describe('parseAttribution', () => { + it('parses UTM params from the URL', () => { + Object.defineProperty(window, 'location', { + value: { + search: '?utm_source=youtube&utm_medium=influencer&utm_campaign=launch', + href: 'https://studio.com/shop?utm_source=youtube&utm_medium=influencer&utm_campaign=launch', + }, + writable: true, + configurable: true, + }); + + const ctx = parseAttribution(); + expect(ctx.utm_source).toBe('youtube'); + expect(ctx.utm_medium).toBe('influencer'); + expect(ctx.utm_campaign).toBe('launch'); + expect(ctx.landing_page).toBe(window.location.href); + }); + + it('parses click IDs from the URL', () => { + Object.defineProperty(window, 'location', { + value: { + search: '?gclid=abc&fbclid=def&ttclid=ghi&msclkid=jkl&dclid=mno&li_fat_id=pqr', + href: 'https://studio.com/?gclid=abc&fbclid=def&ttclid=ghi&msclkid=jkl&dclid=mno&li_fat_id=pqr', + }, + writable: true, + configurable: true, + }); + + const ctx = parseAttribution(); + expect(ctx.gclid).toBe('abc'); + expect(ctx.fbclid).toBe('def'); + expect(ctx.ttclid).toBe('ghi'); + expect(ctx.msclkid).toBe('jkl'); + expect(ctx.dclid).toBe('mno'); + expect(ctx.li_fat_id).toBe('pqr'); + }); + + it('parses ref param as referral_code', () => { + Object.defineProperty(window, 'location', { + value: { + search: '?ref=creator_handle', + href: 'https://studio.com/?ref=creator_handle', + }, + writable: true, + configurable: true, + }); + + const ctx = parseAttribution(); + expect(ctx.referral_code).toBe('creator_handle'); + }); + + it('returns cached attribution on subsequent calls within session', () => { + Object.defineProperty(window, 'location', { + value: { + search: '?utm_source=first', + href: 'https://studio.com/?utm_source=first', + }, + writable: true, + configurable: true, + }); + + const first = parseAttribution(); + expect(first.utm_source).toBe('first'); + + // Change URL (simulating SPA navigation) + Object.defineProperty(window, 'location', { + value: { + search: '', + href: 'https://studio.com/shop', + }, + writable: true, + configurable: true, + }); + + const second = parseAttribution(); + expect(second.utm_source).toBe('first'); // Still the original + }); + + it('captures document.referrer', () => { + Object.defineProperty(document, 'referrer', { + value: 'https://google.com/search?q=immutable', + configurable: true, + }); + Object.defineProperty(window, 'location', { + value: { + search: '', + href: 'https://studio.com/', + }, + writable: true, + configurable: true, + }); + + const ctx = parseAttribution(); + expect(ctx.referrer).toBe('https://google.com/search?q=immutable'); + }); + + it('only includes params that are present', () => { + Object.defineProperty(window, 'location', { + value: { + search: '', + href: 'https://studio.com/', + }, + writable: true, + configurable: true, + }); + + const ctx = parseAttribution(); + expect(ctx.utm_source).toBeUndefined(); + expect(ctx.gclid).toBeUndefined(); + expect(ctx.landing_page).toBe('https://studio.com/'); + }); +}); diff --git a/packages/audience/web/src/attribution.ts b/packages/audience/web/src/attribution.ts new file mode 100644 index 0000000000..56ee4ca20d --- /dev/null +++ b/packages/audience/web/src/attribution.ts @@ -0,0 +1,76 @@ +import { isBrowser } from '@imtbl/audience-core'; + +/** + * Attribution signals captured from the landing URL on first page load. + * Stored in wire format (snake_case) — no intermediate conversion needed. + * Cached in sessionStorage so SPA navigations don't lose the original params. + */ +export type Attribution = Record; + +/** + * URL query parameters to capture as attribution. + * Keys are the URL param names, values are the wire-format property names. + * Adding a new tracked param is a single line here. + */ +const TRACKED_PARAMS: Record = { + // UTM campaign parameters (Google Analytics spec) + utm_source: 'utm_source', + utm_medium: 'utm_medium', + utm_campaign: 'utm_campaign', + utm_content: 'utm_content', + utm_term: 'utm_term', + // Ad platform click IDs (at most one per visit) + gclid: 'gclid', + fbclid: 'fbclid', + ttclid: 'ttclid', + msclkid: 'msclkid', + dclid: 'dclid', + li_fat_id: 'li_fat_id', + // Referral + ref: 'referral_code', +}; + +const SESSION_KEY = '__imtbl_attribution'; + +function getCached(): Attribution | undefined { + try { + const raw = sessionStorage.getItem(SESSION_KEY); + return raw ? JSON.parse(raw) : undefined; + } catch { + return undefined; + } +} + +function persist(ctx: Attribution): void { + try { + sessionStorage.setItem(SESSION_KEY, JSON.stringify(ctx)); + } catch { + // sessionStorage unavailable — attribution won't persist across SPA navigations + } +} + +/** + * Capture attribution signals from the current URL. + * Parsed once per session and cached in sessionStorage so SPA + * route changes don't lose the original landing params. + */ +export function parseAttribution(): Attribution { + if (!isBrowser()) return {}; + + const cached = getCached(); + if (cached) return cached; + + const params = new URLSearchParams(window.location.search); + const ctx: Attribution = {}; + + for (const [param, prop] of Object.entries(TRACKED_PARAMS)) { + const value = params.get(param); + if (value) ctx[prop] = value; + } + + if (document.referrer) ctx.referrer = document.referrer; + ctx.landing_page = window.location.href; + + persist(ctx); + return ctx; +} diff --git a/packages/audience/web/src/cdn.ts b/packages/audience/web/src/cdn.ts new file mode 100644 index 0000000000..69ca415cf9 --- /dev/null +++ b/packages/audience/web/src/cdn.ts @@ -0,0 +1,17 @@ +/** + * Audience web SDK CDN entry point — self-contained IIFE bundle. + * Assigns ImmutableWebSDK to window for script-tag usage. + */ +import { ImmutableWebSDK } from './index'; + +if (typeof window !== 'undefined') { + (window as any).ImmutableWebSDK = ImmutableWebSDK; +} + +export { ImmutableWebSDK }; +export type { + WebSDKConfig, + ConsentLevel, + UserTraits, + Environment, +} from './index'; diff --git a/packages/audience/web/src/config.ts b/packages/audience/web/src/config.ts new file mode 100644 index 0000000000..1596eabab7 --- /dev/null +++ b/packages/audience/web/src/config.ts @@ -0,0 +1,20 @@ +// Web SDK-specific constants. +// Backend endpoints and base URLs come from @imtbl/audience-core. + +export const LIBRARY_NAME = '@imtbl/audience-web-sdk'; +/** Replaced at build time by esbuild replace plugin. */ +export const LIBRARY_VERSION = '__SDK_VERSION__'; + +/** Log prefix for console messages from this package. */ +export const LOG_PREFIX = '[audience-web-sdk]'; + +/** Default consent source when consentSource is not provided in config. */ +export const DEFAULT_CONSENT_SOURCE = 'WebSDK'; + +// --- Auto-tracked event names --- +// These are fired by the SDK lifecycle, not by studio code. + +/** Fired on init (or consent upgrade from none) when no active session cookie exists. */ +export const SESSION_START = 'session_start'; +/** Fired on explicit shutdown(). Not fired on tab close or consent revocation. */ +export const SESSION_END = 'session_end'; diff --git a/packages/audience/web/src/consent-transport.ts b/packages/audience/web/src/consent-transport.ts new file mode 100644 index 0000000000..0ff7c1bf80 --- /dev/null +++ b/packages/audience/web/src/consent-transport.ts @@ -0,0 +1,25 @@ +import type { ConsentTransport } from '@imtbl/audience-sdk'; +import { LOG_PREFIX } from './config'; + +/** Fetch-based consent transport for browser environments. */ +export const webConsentTransport: ConsentTransport = { + async syncConsent(url, publishableKey, body) { + try { + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'x-immutable-publishable-key': publishableKey, + }, + body: JSON.stringify(body), + }); + if (!response.ok) { + // eslint-disable-next-line no-console + console.warn(`${LOG_PREFIX} consent sync failed: HTTP ${response.status}`); + } + } catch (err) { + // eslint-disable-next-line no-console + console.warn(`${LOG_PREFIX} consent sync failed:`, err); + } + }, +}; diff --git a/packages/audience/web/src/cookie.test.ts b/packages/audience/web/src/cookie.test.ts new file mode 100644 index 0000000000..6ef6e2fcd4 --- /dev/null +++ b/packages/audience/web/src/cookie.test.ts @@ -0,0 +1,34 @@ +import { + getOrCreateSessionId, + renewSession, +} from './cookie'; + +beforeEach(() => { + document.cookie.split(';').forEach((c) => { + document.cookie = `${c.trim().split('=')[0]}=; max-age=0; path=/`; + }); +}); + +describe('getOrCreateSessionId', () => { + it('creates a new session ID and reports isNew', () => { + const result = getOrCreateSessionId(); + expect(result.sessionId).toBeDefined(); + expect(typeof result.sessionId).toBe('string'); + expect(result.isNew).toBe(true); + }); + + it('returns existing session and isNew=false', () => { + const first = getOrCreateSessionId(); + const second = getOrCreateSessionId(); + expect(second.sessionId).toBe(first.sessionId); + expect(second.isNew).toBe(false); + }); +}); + +describe('renewSession', () => { + it('preserves existing session cookie', () => { + const { sessionId } = getOrCreateSessionId(); + renewSession(); + expect(getOrCreateSessionId().sessionId).toBe(sessionId); + }); +}); diff --git a/packages/audience/web/src/cookie.ts b/packages/audience/web/src/cookie.ts new file mode 100644 index 0000000000..359e77bce5 --- /dev/null +++ b/packages/audience/web/src/cookie.ts @@ -0,0 +1,32 @@ +import { + SESSION_COOKIE, + getCookie, + setCookie, + generateId, +} from '@imtbl/audience-core'; + +/** 30 minutes in seconds — session cookie rolls on every SDK interaction. */ +const SESSION_MAX_AGE = 30 * 60; + +/** Return value from getOrCreateSessionId. */ +export interface SessionResult { + /** The session UUID (persisted in _imtbl_sid cookie). */ + sessionId: string; + /** True if this call created a new session (no existing cookie). */ + isNew: boolean; +} + +export function getOrCreateSessionId(domain?: string): SessionResult { + const existing = getCookie(SESSION_COOKIE); + const isNew = !existing; + const sid = existing ?? generateId(); + setCookie(SESSION_COOKIE, sid, SESSION_MAX_AGE, domain); + return { sessionId: sid, isNew }; +} + +export function renewSession(domain?: string): void { + const sid = getCookie(SESSION_COOKIE); + if (sid) { + setCookie(SESSION_COOKIE, sid, SESSION_MAX_AGE, domain); + } +} diff --git a/packages/audience/web/src/index.ts b/packages/audience/web/src/index.ts new file mode 100644 index 0000000000..27c22328e4 --- /dev/null +++ b/packages/audience/web/src/index.ts @@ -0,0 +1,3 @@ +export { ImmutableWebSDK } from './sdk'; +export type { WebSDKConfig } from './types'; +export type { Environment, ConsentLevel, UserTraits } from '@imtbl/audience-core'; diff --git a/packages/audience/web/src/sdk.test.ts b/packages/audience/web/src/sdk.test.ts new file mode 100644 index 0000000000..44e627391f --- /dev/null +++ b/packages/audience/web/src/sdk.test.ts @@ -0,0 +1,723 @@ +import { + COOKIE_NAME, SESSION_COOKIE, INGEST_PATH, CONSENT_PATH, +} from '@imtbl/audience-core'; +import { ImmutableWebSDK } from './sdk'; +import { LIBRARY_NAME, SESSION_START, SESSION_END } from './config'; + +// --- Test fixtures --- +const TEST_USER = { uid: 'user@example.com', provider: 'email' } as const; +const TEST_STEAM = { uid: '76561198012345', provider: 'steam' } as const; + +function createSDK(overrides: Record = {}) { + return ImmutableWebSDK.init({ + publishableKey: 'pk_imtbl_test', + environment: 'sandbox', + consent: 'full', + ...overrides, + }); +} + +const originalLocation = window.location; +const fetchCalls: { url: string; init: RequestInit }[] = []; + +const mockFetch = jest.fn().mockImplementation( + async (url: string, init?: RequestInit) => { + fetchCalls.push({ url: url as string, init: init ?? {} }); + return { ok: true, json: async () => ({}) }; + }, +); +global.fetch = mockFetch; + +function sentMessages(): any[] { + return fetchCalls + .filter((c) => c.url.includes(INGEST_PATH)) + .flatMap((c) => JSON.parse(c.init.body as string).messages); +} + +beforeEach(() => { + jest.clearAllMocks(); + fetchCalls.length = 0; + jest.useFakeTimers(); + document.cookie.split(';').forEach((c) => { + document.cookie = `${c.trim().split('=')[0]}=;max-age=0;path=/`; + }); + localStorage.clear(); + sessionStorage.clear(); +}); + +afterEach(() => { + // Reset instance counter so each test starts fresh + (ImmutableWebSDK as any).liveInstances = 0; + jest.useRealTimers(); + Object.defineProperty(window, 'location', { + value: originalLocation, + writable: true, + configurable: true, + }); +}); + +describe('ImmutableWebSDK', () => { + describe('init', () => { + it('creates an SDK instance via static init()', () => { + const sdk = createSDK(); + expect(sdk).toBeInstanceOf(ImmutableWebSDK); + sdk.shutdown(); + }); + + it('creates anonymous ID cookie when consent allows', () => { + const sdk = createSDK({ consent: 'anonymous' }); + expect(document.cookie).toContain(`${COOKIE_NAME}=`); + sdk.shutdown(); + }); + + it('does not create identity cookies at none consent', () => { + const sdk = createSDK({ consent: 'none' }); + expect(document.cookie).not.toContain(`${COOKIE_NAME}=`); + sdk.shutdown(); + }); + + it('throws if publishableKey is empty', () => { + expect(() => ImmutableWebSDK.init({ + publishableKey: '', + environment: 'sandbox', + })).toThrow('publishableKey is required'); + }); + + it('throws if publishableKey is whitespace only', () => { + expect(() => ImmutableWebSDK.init({ + publishableKey: ' ', + environment: 'sandbox', + })).toThrow('publishableKey is required'); + }); + + it('warns on double init but still creates a new instance', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const first = createSDK(); + const second = ImmutableWebSDK.init({ + publishableKey: 'pk_imtbl_other', + environment: 'production', + consent: 'none', + }); + + expect(second).not.toBe(first); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Multiple SDK instances detected'), + ); + + warnSpy.mockRestore(); + first.shutdown(); + second.shutdown(); + }); + + it('does not warn after previous instance is shut down', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const first = createSDK(); + first.shutdown(); + warnSpy.mockClear(); + + const second = createSDK(); + expect(warnSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Multiple SDK instances'), + ); + + warnSpy.mockRestore(); + second.shutdown(); + }); + + it('emits session_start on new session', async () => { + const sdk = createSDK({ consent: 'full' }); + await sdk.flush(); + + const msg = sentMessages().find( + (m: any) => m.type === 'track' && m.eventName === SESSION_START, + ); + expect(msg).toBeDefined(); + expect(msg.properties).toHaveProperty('sessionId'); + + sdk.shutdown(); + }); + + it('includes attribution on session_start', async () => { + Object.defineProperty(window, 'location', { + value: { + ...window.location, + search: '?utm_source=youtube&utm_campaign=launch', + href: 'https://studio.com/?utm_source=youtube&utm_campaign=launch', + protocol: 'https:', + pathname: '/', + }, + writable: true, + configurable: true, + }); + sessionStorage.clear(); + + const sdk = createSDK({ consent: 'full' }); + await sdk.flush(); + + const msg = sentMessages().find( + (m: any) => m.type === 'track' && m.eventName === SESSION_START, + ); + expect(msg).toBeDefined(); + expect(msg.properties).toHaveProperty('sessionId'); + expect(msg.properties).toHaveProperty('utm_source', 'youtube'); + expect(msg.properties).toHaveProperty('utm_campaign', 'launch'); + + sdk.shutdown(); + }); + }); + + describe('track', () => { + it('enqueues an event and flushes', async () => { + const sdk = createSDK(); + + sdk.track('purchase', { + currency: 'USD', + value: 9.99, + itemId: 'sword_01', + }); + + await sdk.flush(); + + const msgs = sentMessages(); + const msg = msgs.find( + (m: any) => m.type === 'track' && m.eventName === 'purchase', + ); + + expect(msg).toBeDefined(); + expect(msg.properties).toEqual({ + currency: 'USD', + value: 9.99, + itemId: 'sword_01', + }); + expect(msg.surface).toBe('web'); + expect(msg.context.library).toBe(LIBRARY_NAME); + + sdk.shutdown(); + }); + + it('is a no-op at none consent', async () => { + const sdk = createSDK({ consent: 'none' }); + + sdk.track('sign_up', { method: 'google' }); + await sdk.flush(); + + expect(sentMessages()).toHaveLength(0); + sdk.shutdown(); + }); + + it('excludes userId at anonymous consent', async () => { + const sdk = createSDK({ consent: 'anonymous' }); + + sdk.track('sign_in', { method: 'passport' }); + await sdk.flush(); + + const msg = sentMessages().find( + (m: any) => m.type === 'track' && m.eventName === 'sign_in', + ); + expect(msg).toBeDefined(); + expect(msg.userId).toBeUndefined(); + + sdk.shutdown(); + }); + + it('includes userId at full consent after identify', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.identify(TEST_USER.uid, TEST_USER.provider); + sdk.track('level_up', { level: 5 }); + await sdk.flush(); + + const msg = sentMessages().find( + (m: any) => m.type === 'track' && m.eventName === 'level_up', + ); + expect(msg).toBeDefined(); + expect(msg.userId).toBe(TEST_USER.uid); + + sdk.shutdown(); + }); + }); + + describe('page', () => { + it('enqueues a page message', async () => { + const sdk = createSDK(); + + sdk.page({ section: 'shop' }); + await sdk.flush(); + + const msg = sentMessages().find((m: any) => m.type === 'page'); + expect(msg).toBeDefined(); + expect(msg.properties).toMatchObject({ section: 'shop' }); + + sdk.shutdown(); + }); + + it('is a no-op at none consent', async () => { + const sdk = createSDK({ consent: 'none' }); + + sdk.page({ section: 'shop' }); + await sdk.flush(); + + const pages = sentMessages().filter((m: any) => m.type === 'page'); + expect(pages).toHaveLength(0); + + sdk.shutdown(); + }); + + it('excludes userId at anonymous consent', async () => { + const sdk = createSDK({ consent: 'anonymous' }); + + sdk.page({ section: 'shop' }); + await sdk.flush(); + + const msg = sentMessages().find((m: any) => m.type === 'page'); + expect(msg).toBeDefined(); + expect(msg.userId).toBeUndefined(); + + sdk.shutdown(); + }); + + it('includes userId at full consent after identify', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.identify(TEST_USER.uid, TEST_USER.provider); + sdk.page({ section: 'shop' }); + await sdk.flush(); + + const msg = sentMessages().find((m: any) => m.type === 'page'); + expect(msg).toBeDefined(); + expect(msg.userId).toBe(TEST_USER.uid); + + sdk.shutdown(); + }); + + it('attaches attribution to the first page view', async () => { + Object.defineProperty(window, 'location', { + value: { + ...window.location, + search: '?utm_source=youtube', + href: 'https://studio.com/?utm_source=youtube', + protocol: 'https:', + pathname: '/', + }, + writable: true, + configurable: true, + }); + sessionStorage.clear(); + + const sdk = createSDK(); + sdk.page(); + sdk.page(); + await sdk.flush(); + + const pages = sentMessages().filter( + (m: any) => m.type === 'page', + ); + expect(pages[0].properties).toHaveProperty( + 'utm_source', + 'youtube', + ); + if (pages[1]) { + expect(pages[1].properties?.utm_source).toBeUndefined(); + } + + sdk.shutdown(); + }); + }); + + describe('identify', () => { + it('sends an identify message at full consent', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.identify(TEST_USER.uid, TEST_USER.provider, { + name: 'Player One', + }); + await sdk.flush(); + + const msg = sentMessages().find( + (m: any) => m.type === 'identify', + ); + expect(msg).toBeDefined(); + expect(msg.userId).toBe(TEST_USER.uid); + expect(msg.identityType).toBe(TEST_USER.provider); + expect(msg.traits).toEqual({ name: 'Player One' }); + + sdk.shutdown(); + }); + + it('is a no-op at none consent', async () => { + const sdk = createSDK({ consent: 'none' }); + + sdk.identify(TEST_USER.uid, TEST_USER.provider); + await sdk.flush(); + + const ids = sentMessages().filter((m: any) => m.type === 'identify'); + expect(ids).toHaveLength(0); + + sdk.shutdown(); + }); + + it('is a no-op at anonymous consent', async () => { + const sdk = createSDK({ consent: 'anonymous' }); + + sdk.identify(TEST_USER.uid, TEST_USER.provider); + await sdk.flush(); + + const ids = sentMessages().filter( + (m: any) => m.type === 'identify', + ); + expect(ids).toHaveLength(0); + sdk.shutdown(); + }); + + it('ignores null passed as traits', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.identify(null as any); + await sdk.flush(); + + const ids = sentMessages().filter((m: any) => m.type === 'identify'); + // null is not a valid traits object — should not enqueue + expect(ids).toHaveLength(0); + + sdk.shutdown(); + }); + + it('ignores array passed as traits', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.identify(['not', 'traits'] as any); + await sdk.flush(); + + const ids = sentMessages().filter((m: any) => m.type === 'identify'); + expect(ids).toHaveLength(0); + + sdk.shutdown(); + }); + + it('sends anonymous identify with traits only', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.identify({ + source: 'steam', + steamId: TEST_STEAM.uid, + }); + await sdk.flush(); + + const msg = sentMessages().find( + (m: any) => m.type === 'identify', + ); + expect(msg).toBeDefined(); + expect(msg.userId).toBeUndefined(); + expect(msg.traits).toEqual({ + source: 'steam', + steamId: TEST_STEAM.uid, + }); + + sdk.shutdown(); + }); + }); + + describe('alias', () => { + it('sends alias with fromId/fromType/toId/toType', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.alias(TEST_STEAM, TEST_USER); + await sdk.flush(); + + const msg = sentMessages().find( + (m: any) => m.type === 'alias', + ); + expect(msg).toBeDefined(); + expect(msg.fromId).toBe(TEST_STEAM.uid); + expect(msg.fromType).toBe(TEST_STEAM.provider); + expect(msg.toId).toBe(TEST_USER.uid); + expect(msg.toType).toBe(TEST_USER.provider); + + sdk.shutdown(); + }); + + it('rejects alias when from and to are identical', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.alias( + { uid: 'same_id', provider: 'steam' }, + { uid: 'same_id', provider: 'steam' }, + ); + await sdk.flush(); + + const aliases = sentMessages().filter((m: any) => m.type === 'alias'); + expect(aliases).toHaveLength(0); + + sdk.shutdown(); + }); + + it('is a no-op at none consent', async () => { + const sdk = createSDK({ consent: 'none' }); + + sdk.alias(TEST_STEAM, TEST_USER); + await sdk.flush(); + + const aliases = sentMessages().filter((m: any) => m.type === 'alias'); + expect(aliases).toHaveLength(0); + + sdk.shutdown(); + }); + + it('is a no-op at anonymous consent', async () => { + const sdk = createSDK({ consent: 'anonymous' }); + + sdk.alias(TEST_STEAM, TEST_USER); + await sdk.flush(); + + const aliases = sentMessages().filter((m: any) => m.type === 'alias'); + expect(aliases).toHaveLength(0); + + sdk.shutdown(); + }); + }); + + describe('setConsent', () => { + it('is a no-op when setting the same level', async () => { + const sdk = createSDK({ consent: 'full' }); + await sdk.flush(); + fetchCalls.length = 0; + + sdk.setConsent('full'); + await sdk.flush(); + + // No consent sync PUT should fire for same-level call + const consentCalls = fetchCalls.filter( + (c) => c.url.includes(CONSENT_PATH), + ); + expect(consentCalls).toHaveLength(0); + + sdk.shutdown(); + }); + + it('creates cookies and enables tracking on upgrade from none to full', async () => { + const sdk = createSDK({ consent: 'none' }); + expect(document.cookie).not.toContain(`${COOKIE_NAME}=`); + + sdk.setConsent('full'); + expect(document.cookie).toContain(`${COOKIE_NAME}=`); + + sdk.identify(TEST_USER.uid, TEST_USER.provider); + sdk.track('purchase', { value: 9.99 }); + await sdk.flush(); + + const trackMsg = sentMessages().find( + (m: any) => m.type === 'track' && m.eventName === 'purchase', + ); + expect(trackMsg).toBeDefined(); + expect(trackMsg.userId).toBe(TEST_USER.uid); + + sdk.shutdown(); + }); + + it('starts queue and emits session_start when upgrading from none', async () => { + const sdk = createSDK({ consent: 'none' }); + + sdk.track('sign_up', { method: 'google' }); + await sdk.flush(); + expect(sentMessages()).toHaveLength(0); + + sdk.setConsent('anonymous'); + sdk.track('sign_up', { method: 'google' }); + await sdk.flush(); + + const msgs = sentMessages(); + const sessionStart = msgs.find( + (m: any) => m.type === 'track' && m.eventName === SESSION_START, + ); + expect(sessionStart).toBeDefined(); + expect(sessionStart.properties).toHaveProperty('sessionId'); + + const signUp = msgs.find( + (m: any) => m.type === 'track' && m.eventName === 'sign_up', + ); + expect(signUp).toBeDefined(); + + sdk.shutdown(); + }); + + it('purges identify/alias, strips userId on downgrade', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.identify(TEST_USER.uid, TEST_USER.provider); + sdk.alias(TEST_STEAM, TEST_USER); + sdk.track('purchase', { currency: 'USD', value: 9.99 }); + + sdk.setConsent('anonymous'); + await sdk.flush(); + + const msgs = sentMessages(); + expect( + msgs.every((m: any) => m.type !== 'identify'), + ).toBe(true); + expect( + msgs.every((m: any) => m.type !== 'alias'), + ).toBe(true); + const trackMsg = msgs.find( + (m: any) => m.type === 'track' && m.eventName === 'purchase', + ); + expect(trackMsg).toBeDefined(); + expect(trackMsg.userId).toBeUndefined(); + + sdk.shutdown(); + }); + + it('clears identity cookies on downgrade to none', () => { + const sdk = createSDK({ consent: 'anonymous' }); + expect(document.cookie).toContain(`${COOKIE_NAME}=`); + expect(document.cookie).toContain(`${SESSION_COOKIE}=`); + + sdk.setConsent('none'); + expect(document.cookie).not.toContain(`${COOKIE_NAME}=`); + expect(document.cookie).not.toContain(`${SESSION_COOKIE}=`); + + sdk.shutdown(); + }); + + it('stops queue and makes track no-op on anonymous to none downgrade', async () => { + const sdk = createSDK({ consent: 'anonymous' }); + + sdk.track('before', { step: 1 }); + await sdk.flush(); + const beforeMsgs = sentMessages().filter( + (m: any) => m.type === 'track' && m.eventName === 'before', + ); + expect(beforeMsgs.length).toBeGreaterThan(0); + fetchCalls.length = 0; + + sdk.setConsent('none'); + sdk.track('after', { step: 2 }); + await sdk.flush(); + + const afterMsgs = sentMessages().filter( + (m: any) => m.type === 'track' && m.eventName === 'after', + ); + expect(afterMsgs).toHaveLength(0); + + sdk.shutdown(); + }); + + it('re-attaches attribution after consent downgrade and re-upgrade', async () => { + Object.defineProperty(window, 'location', { + value: { + ...window.location, + search: '?utm_source=tiktok', + href: 'https://studio.com/?utm_source=tiktok', + protocol: 'https:', + pathname: '/', + }, + writable: true, + configurable: true, + }); + sessionStorage.clear(); + + const sdk = createSDK({ consent: 'anonymous' }); + sdk.page(); + await sdk.flush(); + fetchCalls.length = 0; + + // Downgrade then re-upgrade + sdk.setConsent('none'); + sdk.setConsent('anonymous'); + sdk.page(); + await sdk.flush(); + + const pages = sentMessages().filter((m: any) => m.type === 'page'); + expect(pages[0]?.properties).toHaveProperty('utm_source', 'tiktok'); + + sdk.shutdown(); + }); + }); + + describe('shutdown', () => { + it('emits session_end with duration', async () => { + const sdk = createSDK({ consent: 'full' }); + await sdk.flush(); + fetchCalls.length = 0; + + jest.advanceTimersByTime(5000); + sdk.shutdown(); + + // destroy() calls flushUnload() which fires a keepalive fetch synchronously. + // Yield to ensure all microtasks settle before reading fetchCalls. + await Promise.resolve(); + await Promise.resolve(); + + const msg = sentMessages().find( + (m: any) => m.type === 'track' && m.eventName === SESSION_END, + ); + expect(msg).toBeDefined(); + expect(msg.properties).toHaveProperty('sessionId'); + expect(msg.properties.duration).toBe(5); + }); + + it('does not emit session_end at none consent', async () => { + const sdk = createSDK({ consent: 'none' }); + sdk.shutdown(); + + expect(sentMessages().filter( + (m: any) => m.eventName === SESSION_END, + )).toHaveLength(0); + }); + }); + + describe('reset', () => { + it('clears pending messages from the queue', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.track('before_reset', { step: 1 }); + sdk.reset(); + await sdk.flush(); + + const msgs = sentMessages().filter( + (m: any) => m.type === 'track' && m.eventName === 'before_reset', + ); + expect(msgs).toHaveLength(0); + + sdk.shutdown(); + }); + + it('clears userId and generates new anonymousId', async () => { + const sdk = createSDK({ consent: 'full' }); + + sdk.track('sign_in', { method: 'passport' }); + await sdk.flush(); + const originalAnonId = sentMessages().find( + (m: any) => m.type === 'track', + )?.anonymousId; + fetchCalls.length = 0; + + sdk.identify(TEST_USER.uid, TEST_USER.provider); + await sdk.flush(); + fetchCalls.length = 0; + + sdk.reset(); + sdk.track('sign_up', { method: 'google' }); + await sdk.flush(); + + const msg = sentMessages().find( + (m: any) => m.type === 'track', + ); + expect(msg).toBeDefined(); + expect(msg.userId).toBeUndefined(); + expect(msg.anonymousId).toBeDefined(); + expect(msg.anonymousId).not.toBe(originalAnonId); + + sdk.shutdown(); + }); + + it('works at none consent without creating cookies', () => { + const sdk = createSDK({ consent: 'none' }); + + sdk.reset(); + expect(document.cookie).not.toContain(`${COOKIE_NAME}=`); + expect(document.cookie).not.toContain(`${SESSION_COOKIE}=`); + + sdk.shutdown(); + }); + }); +}); diff --git a/packages/audience/web/src/sdk.ts b/packages/audience/web/src/sdk.ts new file mode 100644 index 0000000000..724035c505 --- /dev/null +++ b/packages/audience/web/src/sdk.ts @@ -0,0 +1,430 @@ +import type { + ConsentLevel, + Message, + UserTraits, +} from '@imtbl/audience-core'; +import { + INGEST_PATH, + FLUSH_INTERVAL_MS, + FLUSH_SIZE, + COOKIE_NAME, + SESSION_COOKIE, + MessageQueue, + httpTransport, + getBaseUrl, + getOrCreateAnonymousId, + getCookie, + deleteCookie, + generateId, + getTimestamp, + isAliasValid, + isTimestampValid, + truncate, + collectContext, +} from '@imtbl/audience-core'; +import { ConsentManager, DebugLogger } from '@imtbl/audience-sdk'; +import type { WebSDKConfig } from './types'; +import { + LIBRARY_NAME, LIBRARY_VERSION, LOG_PREFIX, DEFAULT_CONSENT_SOURCE, SESSION_START, SESSION_END, +} from './config'; +import { webConsentTransport } from './consent-transport'; +import { parseAttribution, type Attribution } from './attribution'; +import { + getOrCreateSessionId, + renewSession, +} from './cookie'; + +/** + * Track player activity on your website — page views, purchases, sign-ups — + * and tie it to player identity when they log in. + * + * Create via `ImmutableWebSDK.init()`. Call `shutdown()` when done. + */ +export class ImmutableWebSDK { + private static liveInstances = 0; + + private readonly queue: MessageQueue; + + private readonly consent: ConsentManager; + + private readonly attribution: Attribution; + + private readonly debug: DebugLogger; + + private readonly cookieDomain?: string; + + private anonymousId: string; + + private sessionId: string | undefined; + + private sessionStartTime: number | undefined; + + private userId: string | undefined; + + private isFirstPage = true; + + private constructor(config: WebSDKConfig) { + const { + cookieDomain, + environment, + publishableKey, + } = config; + const consentLevel = config.consent ?? 'none'; + const consentSource = config.consentSource ?? DEFAULT_CONSENT_SOURCE; + const flushInterval = config.flushInterval ?? FLUSH_INTERVAL_MS; + const flushSize = config.flushSize ?? FLUSH_SIZE; + + this.cookieDomain = cookieDomain; + this.debug = new DebugLogger(config.debug ?? false); + + this.consent = new ConsentManager( + environment, + publishableKey, + consentLevel, + consentSource, + webConsentTransport, + ); + + let isNewSession = false; + if (!this.isTrackingDisabled()) { + this.anonymousId = getOrCreateAnonymousId(cookieDomain); + isNewSession = this.startSession(); + } else { + this.anonymousId = getCookie(COOKIE_NAME) ?? generateId(); + } + + const endpointUrl = `${getBaseUrl(environment)}${INGEST_PATH}`; + this.queue = new MessageQueue( + httpTransport, + endpointUrl, + publishableKey, + flushInterval, + flushSize, + { + onFlush: (ok, count) => this.debug.logFlush(ok, count), + staleFilter: (m) => isTimestampValid(m.eventTimestamp), + storagePrefix: '__imtbl_web_', + }, + ); + + this.attribution = parseAttribution(); + + if (!this.isTrackingDisabled()) { + this.queue.start(); + if (isNewSession) this.trackSessionStart(); + } + } + + /** + * Create and start the SDK. Warns if another instance is already active — + * call `shutdown()` on the previous one first. + */ + static init(config: WebSDKConfig): ImmutableWebSDK { + if (!config.publishableKey?.trim()) { + throw new Error(`${LOG_PREFIX} publishableKey is required`); + } + if (ImmutableWebSDK.liveInstances > 0) { + // eslint-disable-next-line no-console + console.warn( + `${LOG_PREFIX} Multiple SDK instances detected.` + + ' Ensure previous instances are shut down to avoid duplicate events.', + ); + } + ImmutableWebSDK.liveInstances += 1; + return new ImmutableWebSDK(config); + } + + // --- Helpers --- + + /** True when consent is 'none' — SDK should not enqueue anything. */ + private isTrackingDisabled(): boolean { + return this.consent.getLevel() === 'none'; + } + + /** Returns userId if consent is full, undefined otherwise. */ + private effectiveUserId(): string | undefined { + return this.consent.getLevel() === 'full' ? this.userId : undefined; + } + + /** Create or resume a session, returning whether it's new. */ + private startSession(): boolean { + const session = getOrCreateSessionId(this.cookieDomain); + this.sessionId = session.sessionId; + this.sessionStartTime = Date.now(); + return session.isNew; + } + + // --- Message factory --- + + /** Common fields shared by every message. */ + private baseMessage() { + return { + messageId: generateId(), + eventTimestamp: getTimestamp(), + anonymousId: this.anonymousId, + surface: 'web' as const, + context: collectContext(LIBRARY_NAME, LIBRARY_VERSION), + }; + } + + private enqueue(label: string, message: Message): void { + this.queue.enqueue(message); + this.debug.logEvent(label, message); + } + + // --- Session lifecycle --- + + private trackSessionStart(): void { + if (!this.sessionId) return; + this.enqueue('track(session_start)', { + ...this.baseMessage(), + type: 'track', + eventName: SESSION_START, + properties: { + sessionId: this.sessionId, + ...this.attribution, + }, + }); + } + + private trackSessionEnd(): void { + if (!this.sessionId) return; + this.enqueue('track(session_end)', { + ...this.baseMessage(), + type: 'track', + eventName: SESSION_END, + properties: { + sessionId: this.sessionId, + ...(this.sessionStartTime && { + duration: Math.round((Date.now() - this.sessionStartTime) / 1000), + }), + }, + }); + } + + // --- Page tracking --- + + /** + * Record a page view. Call this on every route change in your app. + * The first call automatically captures how the player arrived + * (UTM params, ad click IDs, referrer). No-op when consent is 'none'. + */ + page(properties?: Record): void { + if (this.isTrackingDisabled()) return; + renewSession(this.cookieDomain); + + const mergedProps: Record = { ...properties }; + if (this.isFirstPage) { + Object.assign(mergedProps, this.attribution); + this.isFirstPage = false; + } + + this.enqueue('page', { + ...this.baseMessage(), + type: 'page', + properties: Object.keys(mergedProps).length > 0 ? mergedProps : undefined, + userId: this.effectiveUserId(), + }); + } + + // --- Event tracking --- + + /** + * Record a player action like a purchase, sign-up, or game launch. + * Pass the event name and any properties you want to analyse later. + * No-op when consent is 'none'. + */ + track(event: string, properties?: Record): void { + if (this.isTrackingDisabled()) return; + renewSession(this.cookieDomain); + + this.enqueue('track', { + ...this.baseMessage(), + type: 'track', + eventName: truncate(event), + properties, + userId: this.effectiveUserId(), + }); + } + + // --- Identity --- + + /** + * Tell the SDK who this player is. Call when a player logs in or links + * an account. Before identify(), the SDK only knows an anonymous cookie ID. + * After, all future events are tied to this player. + * + * Named: `sdk.identify('user@example.com', 'email', { name: 'Jane' })` + * Traits only: `sdk.identify({ source: 'steam', steamId: '765...' })` + * + * Requires 'full' consent. + */ + identify(uid: string, provider: string, traits?: UserTraits): void; + + identify(traits: UserTraits): void; + + identify( + uidOrTraits: string | UserTraits, + provider?: string, + traits?: UserTraits, + ): void { + if (this.consent.getLevel() !== 'full') { + this.debug.logWarning('identify() requires full consent — call ignored.'); + return; + } + renewSession(this.cookieDomain); + + if (uidOrTraits !== null && typeof uidOrTraits === 'object' && !Array.isArray(uidOrTraits)) { + this.enqueue('identify', { + ...this.baseMessage(), + type: 'identify', + traits: uidOrTraits, + }); + return; + } + + if (typeof uidOrTraits !== 'string') return; + + const uid = truncate(uidOrTraits); + this.userId = uid; + this.enqueue('identify', { + ...this.baseMessage(), + type: 'identify', + userId: uid, + identityType: provider, + traits, + }); + } + + /** + * Connect two accounts that belong to the same player. Use when a player + * previously known by one identity (e.g. Steam ID) creates or links a + * different account (e.g. Passport email). This tells the backend they're + * the same person so analytics aren't split across two profiles. + * + * Requires 'full' consent. `from` and `to` must differ. + */ + alias( + from: { uid: string; provider: string }, + to: { uid: string; provider: string }, + ): void { + if (this.consent.getLevel() !== 'full') { + this.debug.logWarning('alias() requires full consent — call ignored.'); + return; + } + if (!isAliasValid(from.uid, from.provider, to.uid, to.provider)) { + this.debug.logWarning('alias() from and to are identical — call ignored.'); + return; + } + renewSession(this.cookieDomain); + + this.enqueue('alias', { + ...this.baseMessage(), + type: 'alias', + fromId: truncate(from.uid), + fromType: from.provider, + toId: truncate(to.uid), + toType: to.provider, + }); + } + + // --- Consent --- + + /** + * Update tracking consent, typically in response to a cookie banner. + * Call whenever your consent management platform reports a change. + * + * - 'none': all tracking stops, cookies are cleared. + * - 'anonymous': track activity without knowing who the player is. + * - 'full': track everything including player identity. + */ + setConsent(level: ConsentLevel): void { + const previous = this.consent.getLevel(); + if (level === previous) return; + + this.debug.logConsent(previous, level); + + const isUpgradeFromNone = previous === 'none' && level !== 'none'; + + // When upgrading from none, create the persisted anonymousId first + // so the consent sync sends the correct ID to the server. + if (isUpgradeFromNone) { + this.anonymousId = getOrCreateAnonymousId(this.cookieDomain); + } + + this.consent.setLevel(level, this.anonymousId, { + // Note: session_end is intentionally not emitted on consent revocation. + // The queue is purged immediately — no events should be sent after opt-out. + onPurgeQueue: () => { + this.queue.stop(); + this.queue.clear(); + }, + onStripIdentity: () => { + this.userId = undefined; + this.queue.purge( + (m) => m.type === 'identify' || m.type === 'alias', + ); + this.queue.transform((m) => { + if ('userId' in m && m.userId) { + const cleaned = { ...m }; + delete (cleaned as Record).userId; + return cleaned as Message; + } + return m; + }); + }, + onClearIdentity: () => { + deleteCookie(COOKIE_NAME, this.cookieDomain); + deleteCookie(SESSION_COOKIE, this.cookieDomain); + }, + }); + + if (isUpgradeFromNone) { + this.isFirstPage = true; + const isNewSession = this.startSession(); + this.queue.start(); + if (isNewSession) this.trackSessionStart(); + } + } + + // --- Lifecycle --- + + /** + * Call on player logout. Generates a fresh anonymous ID so the next + * player on this device isn't confused with the previous one. Queued + * events from the previous session are discarded. + */ + reset(): void { + this.userId = undefined; + this.queue.clear(); + deleteCookie(COOKIE_NAME, this.cookieDomain); + deleteCookie(SESSION_COOKIE, this.cookieDomain); + if (!this.isTrackingDisabled()) { + this.anonymousId = getOrCreateAnonymousId(this.cookieDomain); + this.startSession(); + } else { + this.anonymousId = generateId(); + this.sessionId = undefined; + this.sessionStartTime = undefined; + } + this.isFirstPage = true; + } + + /** + * Send all queued events now instead of waiting for the next automatic + * flush. Useful before navigating away from a critical page. + */ + async flush(): Promise { + await this.queue.flush(); + } + + /** + * Stop the SDK and send any remaining events. Call when your app + * unmounts or the player leaves. + */ + shutdown(): void { + if (!this.isTrackingDisabled()) this.trackSessionEnd(); + this.queue.destroy(); + ImmutableWebSDK.liveInstances = Math.max(0, ImmutableWebSDK.liveInstances - 1); + } +} diff --git a/packages/audience/web/src/types.ts b/packages/audience/web/src/types.ts new file mode 100644 index 0000000000..2bb5dcb903 --- /dev/null +++ b/packages/audience/web/src/types.ts @@ -0,0 +1,7 @@ +import type { AudienceSDKConfig } from '@imtbl/audience-sdk'; + +/** Configuration for the Immutable Web SDK. */ +export interface WebSDKConfig extends AudienceSDKConfig { + /** Identifies the consent source for server-side audit trail (e.g. 'CookieBannerV2'). Defaults to 'WebSDK'. */ + consentSource?: string; +} diff --git a/packages/audience/web/tsconfig.eslint.json b/packages/audience/web/tsconfig.eslint.json new file mode 100644 index 0000000000..7a70f2c77d --- /dev/null +++ b/packages/audience/web/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": [] +} diff --git a/packages/audience/web/tsconfig.json b/packages/audience/web/tsconfig.json new file mode 100644 index 0000000000..f5b8d5a351 --- /dev/null +++ b/packages/audience/web/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDirs": ["src"], + "customConditions": ["development"] + }, + "include": ["src"], + "exclude": ["dist", "jest.config.ts", "node_modules", "src/**/*.test.ts"] +} diff --git a/packages/audience/web/tsup.cdn.js b/packages/audience/web/tsup.cdn.js new file mode 100644 index 0000000000..847b3baefc --- /dev/null +++ b/packages/audience/web/tsup.cdn.js @@ -0,0 +1,27 @@ +// @ts-check +import { defineConfig } from 'tsup'; +import { replace } from 'esbuild-plugin-replace'; +import pkg from './package.json' assert { type: 'json' }; + +/** + * Audience web SDK CDN bundle — self-contained IIFE exposing window.ImmutableWebSDK. + * All dependencies (including @imtbl/audience-core) are inlined. + * + * Output: dist/cdn/imtbl-web.global.js + */ +export default defineConfig({ + entry: { 'imtbl-web': 'src/cdn.ts' }, + outDir: 'dist/cdn', + format: 'iife', + platform: 'browser', + target: 'es2020', + minify: true, + bundle: true, + treeshake: true, + noExternal: [/.*/], + esbuildPlugins: [ + replace({ + '__SDK_VERSION__': pkg.version === '0.0.0' ? '0.1.0' : pkg.version, + }), + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94ad8bd3a4..395ddcf61d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,13 +120,13 @@ importers: version: 29.7.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.14.13)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@20.14.13)(typescript@5.6.2)) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.23.1)(jest@29.7.0(@types/node@20.14.13)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@20.14.13)(typescript@5.6.2)))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)))(typescript@5.6.2) ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@20.14.13)(typescript@5.6.2) + version: 10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2) typescript: specifier: ^5 version: 5.6.2 @@ -940,7 +940,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) jest-environment-jsdom: specifier: ^29.4.3 version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -967,7 +967,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) jest-environment-jsdom: specifier: ^29.4.3 version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -1001,13 +1001,53 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) + jest-environment-jsdom: + specifier: ^29.4.3 + version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + ts-jest: + specifier: ^29.1.0 + version: 29.2.5(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)))(typescript@5.6.2) + tsup: + specifier: ^8.3.0 + version: 8.3.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(jiti@1.21.0)(postcss@8.4.49)(typescript@5.6.2)(yaml@2.5.0) + typescript: + specifier: ^5.6.2 + version: 5.6.2 + + packages/audience/web: + dependencies: + '@imtbl/audience-core': + specifier: workspace:* + version: link:../core + '@imtbl/audience-sdk': + specifier: workspace:* + version: link:../sdk + devDependencies: + '@swc/core': + specifier: ^1.4.2 + version: 1.15.3(@swc/helpers@0.5.15) + '@swc/jest': + specifier: ^0.2.37 + version: 0.2.37(@swc/core@1.15.3(@swc/helpers@0.5.15)) + '@types/jest': + specifier: ^29.5.12 + version: 29.5.14 + '@types/node': + specifier: ^22.10.7 + version: 22.19.7 + eslint: + specifier: ^8.56.0 + version: 8.57.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) jest-environment-jsdom: specifier: ^29.4.3 version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)))(typescript@5.6.2) tsup: specifier: ^8.3.0 version: 8.3.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(jiti@1.21.0)(postcss@8.4.49)(typescript@5.6.2)(yaml@2.5.0) @@ -1096,7 +1136,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) next: specifier: ^15.2.6 version: 15.5.10(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1132,7 +1172,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) next: specifier: ^15.2.6 version: 15.5.10(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) @@ -1172,7 +1212,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) jest-environment-jsdom: specifier: ^29.4.3 version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -1251,7 +1291,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) jest-environment-jsdom: specifier: ^29.4.3 version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -1348,7 +1388,7 @@ importers: version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) react-scripts: specifier: 5.0.1 - version: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/babel__core@7.20.5)(bufferutil@4.0.8)(esbuild@0.23.1)(eslint@9.16.0(jiti@1.21.0))(node-notifier@8.0.2)(react@18.3.1)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(utf-8-validate@5.0.10) + version: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/babel__core@7.20.5)(bufferutil@4.0.8)(esbuild@0.23.1)(eslint@9.16.0(jiti@1.21.0))(node-notifier@8.0.2)(react@18.3.1)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(utf-8-validate@5.0.10) typescript: specifier: ^5.6.2 version: 5.6.2 @@ -1514,7 +1554,7 @@ importers: version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) react-scripts: specifier: 5.0.1 - version: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/babel__core@7.20.5)(bufferutil@4.0.8)(esbuild@0.23.1)(eslint@8.57.0)(node-notifier@8.0.2)(react@18.3.1)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(utf-8-validate@5.0.10) + version: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/babel__core@7.20.5)(bufferutil@4.0.8)(esbuild@0.23.1)(eslint@8.57.0)(node-notifier@8.0.2)(react@18.3.1)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(utf-8-validate@5.0.10) rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -1526,7 +1566,7 @@ importers: version: 0.13.0(rollup@4.28.0) ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.26.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.9))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)))(typescript@5.6.2) typescript: specifier: ^5.6.2 version: 5.6.2 @@ -1951,7 +1991,7 @@ importers: version: 22.19.7 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -1988,13 +2028,13 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) jest-environment-jsdom: specifier: ^29.4.3 version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2))(typescript@5.6.2) + version: 29.2.5(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)))(typescript@5.6.2) tsup: specifier: ^8.3.0 version: 8.3.0(@swc/core@1.15.3(@swc/helpers@0.5.15))(jiti@1.21.0)(postcss@8.4.49)(typescript@5.6.2)(yaml@2.5.0) @@ -2126,7 +2166,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) jest-environment-jsdom: specifier: ^29.4.3 version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -2184,7 +2224,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) jest-environment-jsdom: specifier: ^29.4.3 version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -2467,7 +2507,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + version: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) jest-environment-jsdom: specifier: ^29.4.3 version: 29.6.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -20746,43 +20786,6 @@ snapshots: - ts-node - utf-8-validate - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0(node-notifier@8.0.2) - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.14.13 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.8.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.14.13)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@20.14.13)(typescript@5.6.2)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.5 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - optionalDependencies: - node-notifier: 8.0.2 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@20.14.13)(typescript@5.6.2))': dependencies: '@jest/console': 29.7.0 @@ -26259,6 +26262,20 @@ snapshots: transitivePeerDependencies: - supports-color + babel-jest@29.7.0(@babel/core@7.26.9): + dependencies: + '@babel/core': 7.26.9 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.26.9) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + optional: true + babel-loader@8.3.0(@babel/core@7.26.9)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)): dependencies: '@babel/core': 7.26.9 @@ -26451,6 +26468,13 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.26.10) + babel-preset-jest@29.6.3(@babel/core@7.26.9): + dependencies: + '@babel/core': 7.26.9 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.26.9) + optional: true + babel-preset-react-app@10.0.1: dependencies: '@babel/core': 7.26.10 @@ -27396,21 +27420,6 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - create-jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)): dependencies: '@jest/types': 29.6.3 @@ -28362,7 +28371,7 @@ snapshots: dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0) object.assign: 4.1.5 object.entries: 1.1.8 semver: 6.3.1 @@ -28373,13 +28382,13 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.6.2) eslint: 8.57.0 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0) eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.9.0(eslint@8.57.0))(eslint-plugin-react-hooks@5.0.0(eslint@8.57.0))(eslint-plugin-react@7.35.0(eslint@8.57.0))(eslint@8.57.0): dependencies: eslint: 8.57.0 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 5.0.0(eslint@8.57.0) @@ -28393,7 +28402,7 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.6.2) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) @@ -28430,7 +28439,7 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.6.2) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) @@ -28448,7 +28457,7 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.6.2) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) @@ -28459,23 +28468,23 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@8.57.0)(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2): + eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@9.16.0(jiti@1.21.0))(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2): dependencies: '@babel/core': 7.26.10 - '@babel/eslint-parser': 7.22.9(@babel/core@7.26.10)(eslint@8.57.0) + '@babel/eslint-parser': 7.22.9(@babel/core@7.26.10)(eslint@9.16.0(jiti@1.21.0)) '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2) - '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2) + '@typescript-eslint/parser': 5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2) babel-preset-react-app: 10.0.1 confusing-browser-globals: 1.0.11 - eslint: 8.57.0 - eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.0) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2) - eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) - eslint-plugin-react: 7.35.0(eslint@8.57.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) - eslint-plugin-testing-library: 5.11.0(eslint@8.57.0)(typescript@5.6.2) + eslint: 9.16.0(jiti@1.21.0) + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@9.16.0(jiti@1.21.0)) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0)) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0))(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2) + eslint-plugin-jsx-a11y: 6.9.0(eslint@9.16.0(jiti@1.21.0)) + eslint-plugin-react: 7.35.0(eslint@9.16.0(jiti@1.21.0)) + eslint-plugin-react-hooks: 4.6.0(eslint@9.16.0(jiti@1.21.0)) + eslint-plugin-testing-library: 5.11.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2) optionalDependencies: typescript: 5.6.2 transitivePeerDependencies: @@ -28486,23 +28495,23 @@ snapshots: - jest - supports-color - eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(eslint@9.16.0(jiti@1.21.0))(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2): + eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(eslint@8.57.0)(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2): dependencies: '@babel/core': 7.26.10 - '@babel/eslint-parser': 7.22.9(@babel/core@7.26.10)(eslint@9.16.0(jiti@1.21.0)) + '@babel/eslint-parser': 7.22.9(@babel/core@7.26.10)(eslint@8.57.0) '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2) - '@typescript-eslint/parser': 5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.6.2) babel-preset-react-app: 10.0.1 confusing-browser-globals: 1.0.11 - eslint: 9.16.0(jiti@1.21.0) - eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(eslint@9.16.0(jiti@1.21.0)) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0)) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0))(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2) - eslint-plugin-jsx-a11y: 6.9.0(eslint@9.16.0(jiti@1.21.0)) - eslint-plugin-react: 7.35.0(eslint@9.16.0(jiti@1.21.0)) - eslint-plugin-react-hooks: 4.6.0(eslint@9.16.0(jiti@1.21.0)) - eslint-plugin-testing-library: 5.11.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2) + eslint: 8.57.0 + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2) + eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) + eslint-plugin-react: 7.35.0(eslint@8.57.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) + eslint-plugin-testing-library: 5.11.0(eslint@8.57.0)(typescript@5.6.2) optionalDependencies: typescript: 5.6.2 transitivePeerDependencies: @@ -28539,24 +28548,6 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): - dependencies: - debug: 4.3.7(supports-color@8.1.1) - enhanced-resolve: 5.15.0 - eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.0) - get-tsconfig: 4.6.2 - globby: 13.2.2 - is-core-module: 2.15.0 - is-glob: 4.0.3 - synckit: 0.8.5 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-node - - eslint-import-resolver-webpack - - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 @@ -28568,17 +28559,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.6.2) - eslint: 8.57.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) - transitivePeerDependencies: - - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.16.0(jiti@1.21.0)): dependencies: debug: 3.2.7 @@ -28589,19 +28569,19 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@8.57.0): + eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@9.16.0(jiti@1.21.0)): dependencies: '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.26.10) '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.10) - eslint: 8.57.0 + eslint: 9.16.0(jiti@1.21.0) lodash: 4.17.21 string-natural-compare: 3.0.1 - eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(eslint@9.16.0(jiti@1.21.0)): + eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(eslint@8.57.0): dependencies: '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.26.9) '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.9) - eslint: 9.16.0(jiti@1.21.0) + eslint: 8.57.0 lodash: 4.17.21 string-natural-compare: 3.0.1 @@ -28632,6 +28612,33 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0): + dependencies: + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + hasown: 2.0.2 + is-core-module: 2.15.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.6.2) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0)): dependencies: array-includes: 3.1.8 @@ -28670,12 +28677,12 @@ snapshots: - supports-color - typescript - eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0))(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2): + eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0))(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2): dependencies: '@typescript-eslint/experimental-utils': 5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2) eslint: 9.16.0(jiti@1.21.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2))(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2) jest: 27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10) transitivePeerDependencies: - supports-color @@ -31021,27 +31028,6 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2): - dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0) - exit: 0.1.2 - import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - optionalDependencies: - node-notifier: 8.0.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest-cli@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) @@ -32089,20 +32075,6 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2): - dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) - '@jest/types': 29.6.3 - import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) - optionalDependencies: - node-notifier: 8.0.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) @@ -35133,7 +35105,7 @@ snapshots: '@remix-run/router': 1.7.2 react: 18.3.1 - react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/babel__core@7.20.5)(bufferutil@4.0.8)(esbuild@0.23.1)(eslint@8.57.0)(node-notifier@8.0.2)(react@18.3.1)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(utf-8-validate@5.0.10): + react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/babel__core@7.20.5)(bufferutil@4.0.8)(esbuild@0.23.1)(eslint@9.16.0(jiti@1.21.0))(node-notifier@8.0.2)(react@18.3.1)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(utf-8-validate@5.0.10): dependencies: '@babel/core': 7.26.9 '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@4.15.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)))(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) @@ -35150,9 +35122,9 @@ snapshots: css-minimizer-webpack-plugin: 3.4.1(esbuild@0.23.1)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) dotenv: 10.0.0 dotenv-expand: 5.1.0 - eslint: 8.57.0 - eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@8.57.0)(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2) - eslint-webpack-plugin: 3.2.0(eslint@8.57.0)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) + eslint: 9.16.0(jiti@1.21.0) + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@9.16.0(jiti@1.21.0))(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2) + eslint-webpack-plugin: 3.2.0(eslint@9.16.0(jiti@1.21.0))(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) file-loader: 6.2.0(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) fs-extra: 10.1.0 html-webpack-plugin: 5.5.3(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) @@ -35169,7 +35141,7 @@ snapshots: prompts: 2.4.2 react: 18.3.1 react-app-polyfill: 3.0.0 - react-dev-utils: 12.0.1(eslint@8.57.0)(typescript@5.6.2)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) + react-dev-utils: 12.0.1(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) react-refresh: 0.11.0 resolve: 1.22.8 resolve-url-loader: 4.0.0 @@ -35219,7 +35191,7 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/babel__core@7.20.5)(bufferutil@4.0.8)(esbuild@0.23.1)(eslint@9.16.0(jiti@1.21.0))(node-notifier@8.0.2)(react@18.3.1)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(utf-8-validate@5.0.10): + react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/babel__core@7.20.5)(bufferutil@4.0.8)(esbuild@0.23.1)(eslint@8.57.0)(node-notifier@8.0.2)(react@18.3.1)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(utf-8-validate@5.0.10): dependencies: '@babel/core': 7.26.9 '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@4.15.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)))(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) @@ -35236,9 +35208,9 @@ snapshots: css-minimizer-webpack-plugin: 3.4.1(esbuild@0.23.1)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) dotenv: 10.0.0 dotenv-expand: 5.1.0 - eslint: 9.16.0(jiti@1.21.0) - eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(eslint@9.16.0(jiti@1.21.0))(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2) - eslint-webpack-plugin: 3.2.0(eslint@9.16.0(jiti@1.21.0))(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) + eslint: 8.57.0 + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.26.9))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9))(eslint@8.57.0)(jest@27.5.1(bufferutil@4.0.8)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2))(utf-8-validate@5.0.10))(typescript@5.6.2) + eslint-webpack-plugin: 3.2.0(eslint@8.57.0)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) file-loader: 6.2.0(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) fs-extra: 10.1.0 html-webpack-plugin: 5.5.3(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) @@ -35255,7 +35227,7 @@ snapshots: prompts: 2.4.2 react: 18.3.1 react-app-polyfill: 3.0.0 - react-dev-utils: 12.0.1(eslint@9.16.0(jiti@1.21.0))(typescript@5.6.2)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) + react-dev-utils: 12.0.1(eslint@8.57.0)(typescript@5.6.2)(webpack@5.88.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.23.1)) react-refresh: 0.11.0 resolve: 1.22.8 resolve-url-loader: 4.0.0 @@ -37132,12 +37104,12 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.26.10) esbuild: 0.23.1 - ts-jest@29.2.5(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2))(typescript@5.6.2): + ts-jest@29.2.5(@babel/core@7.26.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.9))(esbuild@0.23.1)(jest@29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)))(typescript@5.6.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2) + jest: 29.7.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(node-notifier@8.0.2)(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/node@22.19.7)(typescript@5.6.2)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -37146,10 +37118,10 @@ snapshots: typescript: 5.6.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.26.10 + '@babel/core': 7.26.9 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.26.10) + babel-jest: 29.7.0(@babel/core@7.26.9) esbuild: 0.23.1 ts-mockito@2.6.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 09f5e8fee2..ad69fd3914 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,6 +24,7 @@ packages: - "packages/audience/core" - "packages/audience/pixel" - "packages/audience/sdk" + - "packages/audience/web" - "packages/game-bridge" - "packages/webhook/sdk" - "packages/minting-backend/sdk"