@optable/web-sdk 0.59.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { AgentType } from "iab-adcom";
11
+ import { LocalStorage } from "../core/storage";
12
+ import { sendTargetingUpdateEvent } from "../core/events/cache-refresh";
13
+ const UID2_REFRESH_ENDPOINT = "https://prod.uidapi.com/v2/token/refresh";
14
+ function isUid2RefData(body) {
15
+ const b = body;
16
+ return (!!b &&
17
+ typeof b.advertising_token === "string" &&
18
+ typeof b.refresh_token === "string" &&
19
+ typeof b.refresh_response_key === "string" &&
20
+ typeof b.refresh_from === "number" &&
21
+ typeof b.refresh_expires === "number" &&
22
+ typeof b.identity_expires === "number");
23
+ }
24
+ // Refresh responses are base64(12-byte nonce || AES-GCM ciphertext), keyed by
25
+ // the refresh_response_key issued alongside the refresh token.
26
+ //
27
+ // A response that cannot be decoded or decrypted throws; error policy stays
28
+ // with the caller.
29
+ function refreshUid2Token(refreshToken_1, refreshResponseKey_1) {
30
+ return __awaiter(this, arguments, void 0, function* (refreshToken, refreshResponseKey, endpoint = UID2_REFRESH_ENDPOINT) {
31
+ const response = yield fetch(endpoint, {
32
+ method: "POST",
33
+ headers: { "Content-Type": "text/plain" },
34
+ body: refreshToken,
35
+ });
36
+ if (!response.ok) {
37
+ // Error responses (400/401) are unencrypted JSON carrying a documented
38
+ // status (client_error, invalid_token, expired_token, unauthorized) and a
39
+ // free-form message.
40
+ let reason = `HTTP ${response.status}`;
41
+ let message;
42
+ try {
43
+ const body = JSON.parse(yield response.text());
44
+ if (typeof (body === null || body === void 0 ? void 0 : body.status) === "string") {
45
+ reason = body.status;
46
+ }
47
+ if (typeof (body === null || body === void 0 ? void 0 : body.message) === "string") {
48
+ message = body.message;
49
+ }
50
+ }
51
+ catch (_a) {
52
+ // Non-JSON error body; keep the HTTP status as the reason.
53
+ }
54
+ return { status: "error", reason, message };
55
+ }
56
+ const encrypted = yield response.text();
57
+ const encryptedBytes = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0));
58
+ const keyBytes = Uint8Array.from(atob(refreshResponseKey), (c) => c.charCodeAt(0));
59
+ const nonce = encryptedBytes.slice(0, 12);
60
+ const ciphertext = encryptedBytes.slice(12);
61
+ const cryptoKey = yield crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["decrypt"]);
62
+ const decrypted = yield crypto.subtle.decrypt({ name: "AES-GCM", iv: nonce }, cryptoKey, ciphertext);
63
+ const parsed = JSON.parse(new TextDecoder().decode(decrypted));
64
+ if ((parsed === null || parsed === void 0 ? void 0 : parsed.status) === "optout") {
65
+ return { status: "optout" };
66
+ }
67
+ if ((parsed === null || parsed === void 0 ? void 0 : parsed.status) !== "success") {
68
+ return { status: "error", reason: `operator status "${parsed === null || parsed === void 0 ? void 0 : parsed.status}"` };
69
+ }
70
+ if (!isUid2RefData(parsed.body)) {
71
+ return { status: "error", reason: "malformed response body" };
72
+ }
73
+ return { status: "success", body: parsed.body };
74
+ });
75
+ }
76
+ // Operator rejections that mean the cached identity is definitively dead.
77
+ const EVICTION_REASONS = new Set(["invalid_token", "expired_token"]);
78
+ /**
79
+ * Applies a refresh outcome to the targeting cache: success rewrites the
80
+ * matching EID in place, optout and definitive rejections evict it, any other
81
+ * error leaves the cache untouched for retry on the next page load. Sends the
82
+ * targeting change event after each write.
83
+ */
84
+ function applyUid2Refresh(config, source, result) {
85
+ if (result.status === "error" && !EVICTION_REASONS.has(result.reason)) {
86
+ return;
87
+ }
88
+ const updated = new LocalStorage(config).updateTargeting((cached) => {
89
+ var _a, _b;
90
+ const eids = (_b = (_a = cached === null || cached === void 0 ? void 0 : cached.ortb2) === null || _a === void 0 ? void 0 : _a.user) === null || _b === void 0 ? void 0 : _b.eids;
91
+ // If cache does not exist don't try to set.
92
+ if (!eids) {
93
+ return false;
94
+ }
95
+ const idx = eids.findIndex((e) => e.source === source);
96
+ if (idx === -1) {
97
+ return false;
98
+ }
99
+ if (result.status === "success") {
100
+ eids[idx].uids = [{ atype: AgentType.PERSON_BASED, id: result.body.advertising_token }];
101
+ eids[idx]._ref = {
102
+ advertising_token: result.body.advertising_token,
103
+ refresh_token: result.body.refresh_token,
104
+ refresh_response_key: result.body.refresh_response_key,
105
+ refresh_from: result.body.refresh_from,
106
+ refresh_expires: result.body.refresh_expires,
107
+ identity_expires: result.body.identity_expires,
108
+ };
109
+ }
110
+ else {
111
+ eids.splice(idx, 1);
112
+ }
113
+ return true;
114
+ });
115
+ if (updated) {
116
+ sendTargetingUpdateEvent(config, updated);
117
+ }
118
+ }
119
+ export { refreshUid2Token, applyUid2Refresh, UID2_REFRESH_ENDPOINT };
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "v0.59.0"
2
+ "version": "v0.61.0"
3
3
  }
@@ -38,6 +38,7 @@ type InitConfig = {
38
38
  abTests?: ABTestConfig[];
39
39
  additionalTargetingSignals?: TargetingSignals;
40
40
  forwardSignals?: boolean;
41
+ ois?: boolean;
41
42
  timeout?: string;
42
43
  insecure?: boolean;
43
44
  pageContext?: PageContextConfig | boolean;
@@ -62,6 +63,7 @@ type ResolvedConfig = {
62
63
  abTests?: ABTestConfig[];
63
64
  additionalTargetingSignals?: TargetingSignals;
64
65
  forwardSignals?: boolean;
66
+ ois?: boolean;
65
67
  timeout?: string;
66
68
  insecure?: boolean;
67
69
  };
@@ -34,6 +34,7 @@ function getConfig(init) {
34
34
  abTests: init.abTests,
35
35
  additionalTargetingSignals: init.additionalTargetingSignals,
36
36
  forwardSignals: init.forwardSignals,
37
+ ois: init.ois,
37
38
  timeout: init.timeout,
38
39
  insecure: init.insecure,
39
40
  };
@@ -1,6 +1,8 @@
1
- declare const FLAG_KEYS: readonly ["optableDebug", "optableDisableConsent", "optableResolve1P", "optableResolve3P", "optableEnableAnalytics", "optableControlGroup", "optableForceTargeting", "optableForceGlobalRouting", "optableForceSkipMerge"];
1
+ declare const FLAG_KEYS: readonly ["optableDebug", "optableDisableConsent", "optableResolve1P", "optableResolve3P", "optableEnableAnalytics", "optableControlGroup", "optableForceTargeting", "optableForceGlobalRouting", "optableForceSkipMerge", "optableForceTokenize", "optableResolveId5", "optableResolveID5ID"];
2
2
  export type FlagKey = (typeof FLAG_KEYS)[number];
3
3
  export type Flags = Partial<Record<FlagKey, string>>;
4
+ export declare function persistFlagsFromURL(keys: readonly string[]): Record<string, string>;
4
5
  export declare function getFlags(): Flags;
5
6
  export declare function resetFlags(): void;
7
+ export declare function flagEnabled(key: FlagKey): boolean;
6
8
  export {};
@@ -8,20 +8,38 @@ const FLAG_KEYS = [
8
8
  "optableForceTargeting",
9
9
  "optableForceGlobalRouting",
10
10
  "optableForceSkipMerge",
11
+ "optableForceTokenize",
12
+ "optableResolveId5",
13
+ "optableResolveID5ID",
11
14
  ];
12
- function parseFlags() {
13
- const flags = {};
15
+ // Reads the given keys from the URL query string (a bare key means "1") and
16
+ // persists them to sessionStorage for the rest of the tab session. Exported
17
+ // for wrapper bundles with keys of their own outside FLAG_KEYS.
18
+ export function persistFlagsFromURL(keys) {
19
+ const found = {};
14
20
  try {
15
21
  const params = new URLSearchParams(window.location.search);
16
- for (const key of FLAG_KEYS) {
22
+ for (const key of keys) {
17
23
  if (params.has(key)) {
18
- flags[key] = params.get(key) || "1";
24
+ found[key] = params.get(key) || "1";
19
25
  }
20
26
  }
21
27
  }
22
28
  catch (_a) {
23
29
  // URL params unavailable
24
30
  }
31
+ try {
32
+ for (const key of Object.keys(found)) {
33
+ sessionStorage.setItem(key, found[key]);
34
+ }
35
+ }
36
+ catch (_b) {
37
+ // sessionStorage unavailable
38
+ }
39
+ return found;
40
+ }
41
+ function parseFlags() {
42
+ const flags = persistFlagsFromURL(FLAG_KEYS);
25
43
  try {
26
44
  for (const key of FLAG_KEYS) {
27
45
  if (!(key in flags)) {
@@ -32,7 +50,7 @@ function parseFlags() {
32
50
  }
33
51
  }
34
52
  }
35
- catch (_b) {
53
+ catch (_a) {
36
54
  // sessionStorage unavailable
37
55
  }
38
56
  return flags;
@@ -47,3 +65,21 @@ export function getFlags() {
47
65
  export function resetFlags() {
48
66
  _flags = null;
49
67
  }
68
+ /*
69
+ * True when a flag carries a value and is not explicitly disabled.
70
+ *
71
+ * Flags carry string values ("?optableDebug" and "?optableDebug=1" both yield
72
+ * "1"), so a bare truthiness test treats the string "0" as enabled. Callers
73
+ * that only care whether a flag is on should use this rather than testing the
74
+ * raw value, so "?optableDebug=0" turns the flag off as a reader would expect.
75
+ *
76
+ * An empty value counts as disabled. A URL cannot produce one, but sessionStorage
77
+ * written by other code can.
78
+ *
79
+ * Flags with more than two states — optableControlGroup, where "1" and "0"
80
+ * select different variants — should read getFlags() and compare explicitly.
81
+ */
82
+ export function flagEnabled(key) {
83
+ const value = getFlags()[key];
84
+ return !!value && value !== "0";
85
+ }
@@ -0,0 +1,4 @@
1
+ declare function consoleLog(prefix: string, level: string, message: string, ...args: any[]): void;
2
+ declare function debugLog(level: string, message: string, ...args: any[]): void;
3
+ declare function optableMessage(...args: any[]): void;
4
+ export { consoleLog, debugLog, optableMessage };
@@ -0,0 +1,20 @@
1
+ import { flagEnabled } from "./flags";
2
+ // Level-aware console writer. Ungated — for callers with their own enablement
3
+ // logic, like the RTD module's enableLogging option.
4
+ function consoleLog(prefix, level, message, ...args) {
5
+ const logMethod = ["error", "warn", "info"].includes(level) ? level : "log";
6
+ console[logMethod](`${prefix} ${message}`, ...args); // eslint-disable-line no-console
7
+ }
8
+ // Debug logger gated on the optableDebug flag (URL param or sessionStorage).
9
+ function debugLog(level, message, ...args) {
10
+ if (flagEnabled("optableDebug")) {
11
+ consoleLog("Optable:", level, message, ...args);
12
+ }
13
+ }
14
+ // Debug logger for wrapper bundles, gated on the optableDebug flag.
15
+ function optableMessage(...args) {
16
+ if (flagEnabled("optableDebug")) {
17
+ console.log("[OPTABLE WRAPPER]", ...args); // eslint-disable-line no-console
18
+ }
19
+ }
20
+ export { consoleLog, debugLog, optableMessage };
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { default as buildInfo } from "../build.json";
11
11
  import { LocalStorage } from "./storage";
12
12
  import { deviceSignals } from "./signals";
13
+ import { oisHeaderName, oisRequestID, readOISHeader } from "./ois";
13
14
  function buildRequest(path, config, init) {
14
15
  const { host, cookies, insecure } = config;
15
16
  const url = new URL(path, `${insecure ? "http" : "https"}://${host}`);
@@ -59,16 +60,33 @@ function buildRequest(path, config, init) {
59
60
  }
60
61
  const requestInit = Object.assign({}, init);
61
62
  requestInit.credentials = config.consent.deviceAccess ? "include" : "omit";
63
+ const headers = new Headers(requestInit.headers);
64
+ requestInit.headers = headers;
62
65
  if (config.mockedIP) {
63
- requestInit.headers = new Headers(requestInit.headers);
64
- requestInit.headers.set("X-Forwarded-For", config.mockedIP);
66
+ headers.set("X-Forwarded-For", config.mockedIP);
67
+ }
68
+ if (config.ois) {
69
+ const oisID = oisRequestID(config, url.pathname);
70
+ if (oisID) {
71
+ try {
72
+ headers.set(oisHeaderName, oisID);
73
+ }
74
+ catch (_a) {
75
+ // A stored value carrying characters a header cannot hold must not brick every call.
76
+ }
77
+ }
65
78
  }
66
79
  const request = new Request(url.toString(), requestInit);
67
80
  return request;
68
81
  }
69
82
  function fetch(path, config, init) {
70
83
  return __awaiter(this, void 0, void 0, function* () {
71
- const response = yield globalThis.fetch(buildRequest(path, config, init));
84
+ const request = buildRequest(path, config, init);
85
+ const response = yield globalThis.fetch(request);
86
+ // Ahead of the error throw below: a non-2xx can still carry a derived id.
87
+ if (config.ois) {
88
+ readOISHeader(config, new URL(request.url).pathname, response.headers);
89
+ }
72
90
  const contentType = response.headers.get("Content-Type");
73
91
  const data = (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("application/json")) ? yield response.json() : yield response.text();
74
92
  if (!response.ok) {
@@ -0,0 +1,13 @@
1
+ import type { ResolvedConfig } from "../config";
2
+ declare const oisHeaderName = "X-Optable-OID";
3
+ type OISState = {
4
+ id: string | null;
5
+ storageKey: string;
6
+ };
7
+ declare function getOISID(config: ResolvedConfig): string | null;
8
+ declare function readOISHeader(config: ResolvedConfig, pathname: string, headers: Headers): void;
9
+ declare function oisRequestID(config: ResolvedConfig, pathname: string): string | null;
10
+ declare function clearOISID(config: ResolvedConfig): void;
11
+ declare function getOISState(config: ResolvedConfig): OISState;
12
+ export { oisHeaderName, readOISHeader, oisRequestID, getOISID, getOISState, clearOISID };
13
+ export type { OISState };
@@ -0,0 +1,55 @@
1
+ import { LocalStorage } from "./storage";
2
+ const oisHeaderName = "X-Optable-OID";
3
+ const oisChangeEventName = "optable-ois:change";
4
+ const HEADER_PATHS = new Set(["/identify", "/uid2/token", "/profile"]);
5
+ function derivesOISID(pathname) {
6
+ return HEADER_PATHS.has(pathname);
7
+ }
8
+ function getOISID(config) {
9
+ return new LocalStorage(config).getOIS();
10
+ }
11
+ function readOISHeader(config, pathname, headers) {
12
+ if (!derivesOISID(pathname) || !config.consent.deviceAccess) {
13
+ return;
14
+ }
15
+ const id = headers.get(oisHeaderName);
16
+ if (!id) {
17
+ return;
18
+ }
19
+ const storage = new LocalStorage(config);
20
+ if (storage.getOIS() === id) {
21
+ return;
22
+ }
23
+ try {
24
+ storage.setOIS(id);
25
+ }
26
+ catch (_a) {
27
+ // Storage full or blocked.
28
+ return;
29
+ }
30
+ notifyChange(config, { id, storageKey: storage.oisKey() });
31
+ }
32
+ function oisRequestID(config, pathname) {
33
+ if (!derivesOISID(pathname) || !config.consent.deviceAccess) {
34
+ return null;
35
+ }
36
+ return getOISID(config);
37
+ }
38
+ function clearOISID(config) {
39
+ const storage = new LocalStorage(config);
40
+ if (storage.getOIS() === null) {
41
+ return;
42
+ }
43
+ storage.clearOIS();
44
+ notifyChange(config, { id: null, storageKey: storage.oisKey() });
45
+ }
46
+ function getOISState(config) {
47
+ const storage = new LocalStorage(config);
48
+ return { id: storage.getOIS(), storageKey: storage.oisKey() };
49
+ }
50
+ function notifyChange(config, state) {
51
+ window.dispatchEvent(new CustomEvent(oisChangeEventName, {
52
+ detail: Object.assign({ instance: config.node || config.host }, state),
53
+ }));
54
+ }
55
+ export { oisHeaderName, readOISHeader, oisRequestID, getOISID, getOISState, clearOISID };
@@ -19,7 +19,8 @@ var __rest = (this && this.__rest) || function (s, e) {
19
19
  return t;
20
20
  };
21
21
  // RTD (Real-Time Data) module for Prebid.js integration
22
- import { getFlags } from "../flags";
22
+ import { flagEnabled } from "../flags";
23
+ import { consoleLog } from "../log";
23
24
  // Merge strategies for EIDs
24
25
  function appendMergeStrategy(existingEids, newEids) {
25
26
  return [...existingEids, ...newEids];
@@ -77,12 +78,6 @@ function forceGlobalRouting() {
77
78
  source.routes = ["global"];
78
79
  });
79
80
  }
80
- // Simple logging utility
81
- function log(level, message, ...args) {
82
- const prefix = "Optable RTD:";
83
- const logMethod = ["error", "warn", "info"].includes(level) ? level : "log";
84
- console[logMethod](`${prefix} ${message}`, ...args); // eslint-disable-line no-console
85
- }
86
81
  // Helper function to get targeting data from cache
87
82
  function targetingFromCache(config = {}) {
88
83
  var _a;
@@ -269,19 +264,18 @@ function liveIntentUID2(ortb2) {
269
264
  }
270
265
  function buildRTD(options = {}) {
271
266
  var _a, _b, _c, _d, _e, _f, _g;
272
- const flags = getFlags();
273
- if (flags.optableForceGlobalRouting || options.forceGlobalRouting) {
267
+ if (flagEnabled("optableForceGlobalRouting") || options.forceGlobalRouting) {
274
268
  forceGlobalRouting();
275
269
  }
276
270
  return {
277
- enableLogging: !!flags.optableDebug || ((_a = options.enableLogging) !== null && _a !== void 0 ? _a : false),
271
+ enableLogging: flagEnabled("optableDebug") || ((_a = options.enableLogging) !== null && _a !== void 0 ? _a : false),
278
272
  log(level, message, ...args) {
279
273
  if (this.enableLogging) {
280
- log(level, message, ...args);
274
+ consoleLog("Optable RTD:", level, message, ...args);
281
275
  }
282
276
  },
283
277
  eidSources: (_b = options.eidSources) !== null && _b !== void 0 ? _b : Object.assign({}, defaultEIDSources),
284
- skipMerge: flags.optableForceSkipMerge
278
+ skipMerge: flagEnabled("optableForceSkipMerge")
285
279
  ? () => true
286
280
  : options.skipMerge !== undefined
287
281
  ? options.skipMerge
@@ -1,4 +1,5 @@
1
1
  import { inferRegulation } from "./regulations";
2
+ import { flagEnabled } from "../flags";
2
3
  import * as gpp from "./gpp";
3
4
  import * as tcf from "./tcf";
4
5
  function applicableReg(defaultReg, cmp) {
@@ -102,6 +103,16 @@ function computeConsent(defaultReg, cmp, conf = {}) {
102
103
  return consent;
103
104
  }
104
105
  function getConsent(defaultReg, conf = {}) {
106
+ // QA bypass: grant all permissions and skip the CMP entirely.
107
+ if (flagEnabled("optableDisableConsent")) {
108
+ return {
109
+ reg: null,
110
+ deviceAccess: true,
111
+ createProfilesForAdvertising: true,
112
+ useProfilesForAdvertising: true,
113
+ measureAdvertisingPerformance: true,
114
+ };
115
+ }
105
116
  const cmp = {};
106
117
  tcf.cmpapi.installFrameProxy();
107
118
  gpp.cmpapi.installFrameProxy();
@@ -7,6 +7,7 @@ export declare function encodeBase64(str: string): string;
7
7
  declare function generateSiteKeys(config: ResolvedConfig): StorageKeys;
8
8
  declare function generateTargetingKeys(config: ResolvedConfig): StorageKeys;
9
9
  declare function generatedPairKeys(): StorageKeys;
10
+ declare function generateOISKeys(config: ResolvedConfig): StorageKeys;
10
11
  declare function generatePassportKeys(config: ResolvedConfig): StorageKeys;
11
12
  export type { StorageKeys };
12
- export { generateSiteKeys, generatedPairKeys, generatePassportKeys, generateTargetingKeys };
13
+ export { generateSiteKeys, generatedPairKeys, generatePassportKeys, generateTargetingKeys, generateOISKeys };
@@ -28,6 +28,12 @@ function generateTargetingKeys(config) {
28
28
  function generatedPairKeys() {
29
29
  return { write: [pairStorageKey], read: [pairStorageKey] };
30
30
  }
31
+ // Generate the keys for the OIS id storage
32
+ // The keys are generated based on the host and node configs
33
+ function generateOISKeys(config) {
34
+ const key = `OPTABLE_OIS_${getWriteKeyBase64FromConfig(config)}`;
35
+ return { write: [key], read: [key] };
36
+ }
31
37
  // Generate the keys for the passport storage
32
38
  // The keys are generated based on the host and node configs
33
39
  // We need to keep backward compatibility with the legacy host cache
@@ -50,4 +56,4 @@ function generatePassportKeys(config) {
50
56
  }
51
57
  return { write, read };
52
58
  }
53
- export { generateSiteKeys, generatedPairKeys, generatePassportKeys, generateTargetingKeys };
59
+ export { generateSiteKeys, generatedPairKeys, generatePassportKeys, generateTargetingKeys, generateOISKeys };
@@ -8,13 +8,19 @@ declare class LocalStorage {
8
8
  private targetingKeys;
9
9
  private siteKeys;
10
10
  private pairKeys;
11
+ private oisKeys;
11
12
  private storage;
12
13
  constructor(config: ResolvedConfig);
13
14
  getPassport(): string | null;
14
15
  setPassport(passport: string): void;
15
16
  getVisitorId(): string | null;
17
+ getOIS(): string | null;
18
+ setOIS(id: string): void;
19
+ clearOIS(): void;
20
+ oisKey(): string;
16
21
  getTargeting(): TargetingResponse | null;
17
22
  setTargeting(targeting?: TargetingResponse | null): void;
23
+ updateTargeting(update: (targeting: TargetingResponse) => boolean): TargetingResponse | null;
18
24
  getSite(): SiteResponse | null;
19
25
  setSite(site?: SiteResponse | null): void;
20
26
  setPairIDs(targeting: TargetingResponse): void;
@@ -1,5 +1,5 @@
1
1
  import { LocalStorageProxy } from "./regs/storage";
2
- import { generatedPairKeys, generatePassportKeys, generateSiteKeys, generateTargetingKeys, } from "./storage-keys";
2
+ import { generatedPairKeys, generateOISKeys, generatePassportKeys, generateSiteKeys, generateTargetingKeys, } from "./storage-keys";
3
3
  const pairEIDSource = "pair-protocol.com";
4
4
  class LocalStorage {
5
5
  constructor(config) {
@@ -8,6 +8,7 @@ class LocalStorage {
8
8
  this.targetingKeys = generateTargetingKeys(config);
9
9
  this.siteKeys = generateSiteKeys(config);
10
10
  this.pairKeys = generatedPairKeys();
11
+ this.oisKeys = generateOISKeys(config);
11
12
  this.storage = new LocalStorageProxy(this.config.consent);
12
13
  }
13
14
  getPassport() {
@@ -36,6 +37,18 @@ class LocalStorage {
36
37
  return null;
37
38
  }
38
39
  }
40
+ getOIS() {
41
+ return this.readStorageKeys(this.oisKeys);
42
+ }
43
+ setOIS(id) {
44
+ this.writeToStorageKeys(this.oisKeys, id);
45
+ }
46
+ clearOIS() {
47
+ this.clearStorageKeys(this.oisKeys);
48
+ }
49
+ oisKey() {
50
+ return this.oisKeys.write[0];
51
+ }
39
52
  getTargeting() {
40
53
  const raw = this.readStorageKeys(this.targetingKeys);
41
54
  return raw ? JSON.parse(raw) : null;
@@ -47,6 +60,32 @@ class LocalStorage {
47
60
  this.writeToStorageKeys(this.targetingKeys, JSON.stringify(targeting));
48
61
  this.setPairIDs(targeting);
49
62
  }
63
+ // Updates every stored copy of the targeting response independently: the
64
+ // private and public copies can hold different representations, so each is
65
+ // read, updated and written back on its own. Returns the last updated copy.
66
+ updateTargeting(update) {
67
+ let updated = null;
68
+ const keys = [...new Set([...this.targetingKeys.read, ...this.targetingKeys.write])].filter(Boolean);
69
+ for (const key of keys) {
70
+ const raw = this.storage.getItem(key);
71
+ if (!raw) {
72
+ continue;
73
+ }
74
+ let targeting;
75
+ try {
76
+ targeting = JSON.parse(raw);
77
+ }
78
+ catch (_a) {
79
+ // Leave an unparseable copy untouched.
80
+ continue;
81
+ }
82
+ if (update(targeting)) {
83
+ this.storage.setItem(key, JSON.stringify(targeting));
84
+ updated = targeting;
85
+ }
86
+ }
87
+ return updated;
88
+ }
50
89
  getSite() {
51
90
  const raw = this.readStorageKeys(this.siteKeys);
52
91
  return raw ? JSON.parse(raw) : null;
package/lib/dist/sdk.d.ts CHANGED
@@ -7,6 +7,7 @@ import { SiteResponse } from "./edge/site";
7
7
  import { TargetingKeyValues, TargetingResponse, TargetingRequest, PrebidORTB2 } from "./edge/targeting";
8
8
  import { ContextualSegmentsResponse, ContextualTargetingKeyValues, ContextualTargetingKeyValuesOptions } from "./edge/contextual_segments";
9
9
  import { TokenizeResponse } from "./edge/tokenize";
10
+ import type { OISState } from "./core/ois";
10
11
  declare class OptableSDK {
11
12
  static version: string;
12
13
  dcn: ResolvedConfig;
@@ -14,8 +15,7 @@ declare class OptableSDK {
14
15
  private contextSent;
15
16
  private contextConfig;
16
17
  private contextualResponse;
17
- private passportNullWarned;
18
- private visitorIdNullWarned;
18
+ private warned;
19
19
  constructor(dcn: InitConfig);
20
20
  initialize(): Promise<void>;
21
21
  identify(...ids: string[]): Promise<void>;
@@ -24,8 +24,12 @@ declare class OptableSDK {
24
24
  targetingFromCache(): TargetingResponse | null;
25
25
  site(): Promise<SiteResponse>;
26
26
  siteFromCache(): SiteResponse | null;
27
+ private warnOnce;
27
28
  passport(): string | null;
28
29
  visitorId(): string | null;
30
+ oisId(): string | null;
31
+ oisState(): OISState;
32
+ oisClear(): void;
29
33
  targetingClearCache(): void;
30
34
  prebidORTB2(): Promise<PrebidORTB2>;
31
35
  prebidORTB2FromCache(): PrebidORTB2;
package/lib/dist/sdk.js CHANGED
@@ -22,13 +22,14 @@ import { ContextualSegments, ContextualTargetingKeyValues, } from "./edge/contex
22
22
  import { sha256 } from "js-sha256";
23
23
  import { Tokenize } from "./edge/tokenize";
24
24
  import { LocalStorage } from "./core/storage";
25
+ import { clearOISID, getOISID, getOISState } from "./core/ois";
26
+ import { consoleLog } from "./core/log";
25
27
  class OptableSDK {
26
28
  constructor(dcn) {
27
29
  this.contextSent = false;
28
30
  this.contextConfig = null;
29
31
  this.contextualResponse = null;
30
- this.passportNullWarned = false;
31
- this.visitorIdNullWarned = false;
32
+ this.warned = new Set();
32
33
  this.dcn = getConfig(dcn);
33
34
  this.contextConfig = normalizeContextConfig(dcn.pageContext);
34
35
  if (this.dcn.initContextual && !this.contextConfig) {
@@ -83,11 +84,17 @@ class OptableSDK {
83
84
  siteFromCache() {
84
85
  return SiteFromCache(this.dcn);
85
86
  }
87
+ warnOnce(key, message) {
88
+ if (this.warned.has(key)) {
89
+ return;
90
+ }
91
+ this.warned.add(key);
92
+ consoleLog("[Optable]", "warn", message);
93
+ }
86
94
  passport() {
87
95
  const value = new LocalStorage(this.dcn).getPassport();
88
- if (value === null && !this.passportNullWarned) {
89
- this.passportNullWarned = true;
90
- console.warn("[Optable] passport() returned null. The passport is cached in localStorage once the DCN returns one. " +
96
+ if (value === null) {
97
+ this.warnOnce("passport", "passport() returned null. The passport is cached in localStorage once the DCN returns one. " +
91
98
  "Call before initialization (await sdk.site() or sdk.targeting()) may return null, and deployments where the DCN " +
92
99
  "does not echo the passport in response bodies will never populate it client-side.");
93
100
  }
@@ -95,14 +102,28 @@ class OptableSDK {
95
102
  }
96
103
  visitorId() {
97
104
  const value = new LocalStorage(this.dcn).getVisitorId();
98
- if (value === null && !this.visitorIdNullWarned) {
99
- this.visitorIdNullWarned = true;
100
- console.warn("[Optable] visitorId() returned null. The visitor ID is derived from the passport JWT in localStorage. " +
105
+ if (value === null) {
106
+ this.warnOnce("visitorId", "visitorId() returned null. The visitor ID is derived from the passport JWT in localStorage. " +
101
107
  "Call before initialization (await sdk.site() or sdk.targeting()) may return null, and deployments where the DCN " +
102
108
  "does not echo the passport in response bodies will never populate it client-side.");
103
109
  }
104
110
  return value;
105
111
  }
112
+ oisId() {
113
+ const value = getOISID(this.dcn);
114
+ if (value === null && this.dcn.ois) {
115
+ this.warnOnce("oisId", "oisId() returned null. The derived OIS id is cached once the DCN returns it on the X-Optable-OID " +
116
+ "response header, which happens on the first identify() or profile() call — not during " +
117
+ "initialization. A node with OIS ID derivation disabled, or a non-residential IP, never returns one.");
118
+ }
119
+ return value;
120
+ }
121
+ oisState() {
122
+ return getOISState(this.dcn);
123
+ }
124
+ oisClear() {
125
+ clearOISID(this.dcn);
126
+ }
106
127
  targetingClearCache() {
107
128
  TargetingClearCache(this.dcn);
108
129
  }