From 915b44b387fdb395da4c4f92f9ea49dccdf1384d Mon Sep 17 00:00:00 2001 From: MO Thibault <103271673+MO-Thibault@users.noreply.github.com> Date: Sat, 9 May 2026 19:51:35 -0400 Subject: [PATCH] feat: add uid2Refresh to SDK Adds client-side UID2 token refresh via the UID2 v2 API. The response is AES-GCM encrypted and decrypted using the browser WebCrypto API. Exposes: - `Uid2Refresh(refreshToken, refreshResponseKey)` from edge/uid2_token - `sdk.uid2Refresh(refreshToken, refreshResponseKey)` instance method - `Uid2RefreshBody` type for the decrypted response body Co-Authored-By: Claude Sonnet 4.6 (1M context) --- lib/edge/uid2_token.ts | 36 ++++++++++++++++++++++++++++++++++-- lib/sdk.ts | 6 +++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/edge/uid2_token.ts b/lib/edge/uid2_token.ts index cb3d08ae..a7091ce0 100644 --- a/lib/edge/uid2_token.ts +++ b/lib/edge/uid2_token.ts @@ -10,6 +10,15 @@ type Uid2TokenResponse = { RefreshResponseKey: string; }; +type Uid2RefreshBody = { + advertising_token: string; + refresh_token: string; + identity_expires: number; + refresh_from: number; + refresh_expires: number; + refresh_response_key: string; +}; + function Uid2Token(config: ResolvedConfig, id: string): Promise { return fetch("/uid2/token", config, { method: "POST", @@ -20,6 +29,29 @@ function Uid2Token(config: ResolvedConfig, id: string): Promise { + const encryptedBytes = Uint8Array.from(atob(encryptedBase64), (c) => c.charCodeAt(0)); + const keyBytes = Uint8Array.from(atob(responseKeyBase64), (c) => c.charCodeAt(0)); + const nonce = encryptedBytes.slice(0, 12); + const ciphertext = encryptedBytes.slice(12); + const cryptoKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["decrypt"]); + const decryptedBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv: nonce }, cryptoKey, ciphertext); + return new TextDecoder().decode(decryptedBuffer); +} + +async function Uid2Refresh(refreshToken: string, refreshResponseKey: string): Promise { + const response = await globalThis.fetch("https://prod.uidapi.com/v2/token/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: refreshToken, + }); + if (!response.ok) return null; + const encrypted = await response.text(); + const decrypted = await decryptUid2Response(encrypted, refreshResponseKey); + const { body } = JSON.parse(decrypted) as { body?: Uid2RefreshBody }; + return body ?? null; +} + +export { Uid2Token, Uid2Refresh }; export default Uid2Token; -export type { Uid2TokenResponse }; +export type { Uid2TokenResponse, Uid2RefreshBody }; diff --git a/lib/sdk.ts b/lib/sdk.ts index 3f4ba409..3b69198a 100644 --- a/lib/sdk.ts +++ b/lib/sdk.ts @@ -6,7 +6,7 @@ import type { ProfileTraits } from "./edge/profile"; import type { PageContextConfig, ContextData } from "./core/context"; import { extractContext, normalizeContextConfig } from "./core/context"; import { Identify } from "./edge/identify"; -import { Uid2Token, Uid2TokenResponse } from "./edge/uid2_token"; +import { Uid2Token, Uid2TokenResponse, Uid2Refresh, Uid2RefreshBody } from "./edge/uid2_token"; import { Resolve, ResolveResponse } from "./edge/resolve"; import { Site, SiteResponse, SiteFromCache } from "./edge/site"; import { isObject } from "./core/utils"; @@ -73,6 +73,10 @@ class OptableSDK { return Uid2Token(this.dcn, id); } + async uid2Refresh(refreshToken: string, refreshResponseKey: string): Promise { + return Uid2Refresh(refreshToken, refreshResponseKey); + } + async targeting(input: string | TargetingRequest = "__passport__"): Promise { const request = normalizeTargetingRequest(input);