@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.
- package/README.md +152 -0
- package/browser/dist/sdk.js +1 -1
- package/lib/dist/addons/commands.d.ts +6 -0
- package/lib/dist/addons/commands.js +25 -0
- package/lib/dist/addons/uid2-refresh.d.ts +34 -0
- package/lib/dist/addons/uid2-refresh.js +119 -0
- package/lib/dist/build.json +1 -1
- package/lib/dist/config.d.ts +2 -0
- package/lib/dist/config.js +1 -0
- package/lib/dist/core/flags.d.ts +3 -1
- package/lib/dist/core/flags.js +41 -5
- package/lib/dist/core/log.d.ts +4 -0
- package/lib/dist/core/log.js +20 -0
- package/lib/dist/core/network.js +21 -3
- package/lib/dist/core/ois.d.ts +13 -0
- package/lib/dist/core/ois.js +55 -0
- package/lib/dist/core/prebid/rtd.js +6 -12
- package/lib/dist/core/regs/consent.js +11 -0
- package/lib/dist/core/storage-keys.d.ts +2 -1
- package/lib/dist/core/storage-keys.js +7 -1
- package/lib/dist/core/storage.d.ts +6 -0
- package/lib/dist/core/storage.js +40 -1
- package/lib/dist/sdk.d.ts +6 -2
- package/lib/dist/sdk.js +29 -8
- package/package.json +1 -1
|
@@ -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 };
|
package/lib/dist/build.json
CHANGED
package/lib/dist/config.d.ts
CHANGED
|
@@ -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
|
};
|
package/lib/dist/config.js
CHANGED
package/lib/dist/core/flags.d.ts
CHANGED
|
@@ -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 {};
|
package/lib/dist/core/flags.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
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
|
|
22
|
+
for (const key of keys) {
|
|
17
23
|
if (params.has(key)) {
|
|
18
|
-
|
|
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 (
|
|
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 };
|
package/lib/dist/core/network.js
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
273
|
-
if (flags.optableForceGlobalRouting || options.forceGlobalRouting) {
|
|
267
|
+
if (flagEnabled("optableForceGlobalRouting") || options.forceGlobalRouting) {
|
|
274
268
|
forceGlobalRouting();
|
|
275
269
|
}
|
|
276
270
|
return {
|
|
277
|
-
enableLogging:
|
|
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
|
-
|
|
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:
|
|
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;
|
package/lib/dist/core/storage.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
89
|
-
this.
|
|
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
|
|
99
|
-
this.
|
|
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
|
}
|