From 16f93a82c47d6044bdae42a5d275aa2e60a339ab Mon Sep 17 00:00:00 2001 From: Etienne Latendresse Date: Mon, 14 Sep 2026 17:11:41 -0400 Subject: [PATCH 1/5] secureSignals: add secureSignalsFromEids building GPT secure signals from cached EIDs --- README.md | 12 +++++++++++ lib/core/secure-signals.test.ts | 37 ++++++++++++++++++++++++++++++++ lib/core/secure-signals.ts | 38 +++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 lib/core/secure-signals.test.ts create mode 100644 lib/core/secure-signals.ts diff --git a/README.md b/README.md index e0f08b3d..e737e8c6 100644 --- a/README.md +++ b/README.md @@ -918,6 +918,18 @@ The Optable Web SDK provides a method `installGPTSecureSignals` to pass user-def Please refer to the list of approved Secure Signal [providers](https://support.google.com/admanager/answer/14750072). Please refer to the provider's integration documentation for the exact provider name and value to pass as a signal. +To build the signals from cached EIDs instead of by hand, `secureSignalsFromEids(eids, filter?)` flattens each EID's uids into `{ provider, id }` pairs, optionally filtered by `sources`, `inserters` or `matchers` (an empty or missing list means no constraint on that field): + +```javascript +import { secureSignalsFromEids } from "@optable/web-sdk/lib/dist/core/secure-signals"; + +const eids = JSON.parse(localStorage.getItem("OPTABLE_RESOLVED") || "null")?.ortb2?.user?.eids ?? []; +const signals = secureSignalsFromEids(eids, { sources: ["uidapi.com"] }); +if (signals.length) { + optable.instance.installGPTSecureSignals(...signals); +} +``` + You can verify the signal was correctly passed to GAM by searching for its value cached in `localStorage` under the key `_GESPSK-`. ## Integrating Prebid diff --git a/lib/core/secure-signals.test.ts b/lib/core/secure-signals.test.ts new file mode 100644 index 00000000..c18b3fb3 --- /dev/null +++ b/lib/core/secure-signals.test.ts @@ -0,0 +1,37 @@ +import { secureSignalsFromEids } from "./secure-signals"; + +const EIDS = [ + { source: "uidapi.com", matcher: "m1", inserter: "optable", uids: [{ id: "uid2-a" }, { id: "uid2-b" }] }, + { source: "id5-sync.com", matcher: "m2", uids: [{ id: "id5-a" }] }, + { source: "liveramp.com", uids: [] }, +]; + +describe("secureSignalsFromEids", () => { + it("flattens every uid into a provider/id pair", () => { + expect(secureSignalsFromEids(EIDS)).toEqual([ + { provider: "uidapi.com", id: "uid2-a" }, + { provider: "uidapi.com", id: "uid2-b" }, + { provider: "id5-sync.com", id: "id5-a" }, + ]); + }); + + it("filters by source, inserter and matcher", () => { + expect(secureSignalsFromEids(EIDS, { sources: ["id5-sync.com"] })).toEqual([ + { provider: "id5-sync.com", id: "id5-a" }, + ]); + expect(secureSignalsFromEids(EIDS, { inserters: ["optable"] }).map((s) => s.provider)).toEqual([ + "uidapi.com", + "uidapi.com", + ]); + expect(secureSignalsFromEids(EIDS, { matchers: ["m2"] })).toEqual([{ provider: "id5-sync.com", id: "id5-a" }]); + }); + + it("treats an empty filter list as no constraint", () => { + expect(secureSignalsFromEids(EIDS, { sources: [] })).toHaveLength(3); + }); + + it("skips uids without an id and tolerates empty input", () => { + expect(secureSignalsFromEids([{ source: "a.com", uids: [{ id: "" }, {}] }])).toEqual([]); + expect(secureSignalsFromEids([])).toEqual([]); + }); +}); diff --git a/lib/core/secure-signals.ts b/lib/core/secure-signals.ts new file mode 100644 index 00000000..20325c4b --- /dev/null +++ b/lib/core/secure-signals.ts @@ -0,0 +1,38 @@ +// Converts cached EIDs into GPT secure-signal pairs for +// installGPTSecureSignals, optionally filtered by source, inserter or +// matcher. An empty or missing filter list means no constraint on that field. + +type SecureSignalEid = { + source: string; + inserter?: string; + matcher?: string; + uids?: Array<{ id?: string }>; +}; + +type SecureSignalsFilter = { + sources?: string[]; + inserters?: string[]; + matchers?: string[]; +}; + +type SecureSignal = { provider: string; id: string }; + +export function secureSignalsFromEids(eids: SecureSignalEid[], filter: SecureSignalsFilter = {}): SecureSignal[] { + const allows = (list: string[] | undefined, value: string | undefined) => !list?.length || list.includes(value ?? ""); + + const signals: SecureSignal[] = []; + eids.forEach((eid) => { + if (!allows(filter.sources, eid.source)) return; + if (!allows(filter.inserters, eid.inserter)) return; + if (!allows(filter.matchers, eid.matcher)) return; + + eid.uids?.forEach((uid) => { + if (uid.id) { + signals.push({ provider: eid.source, id: uid.id }); + } + }); + }); + return signals; +} + +export type { SecureSignalEid, SecureSignalsFilter, SecureSignal }; From 9a1e2702d7b06dff5c22580f606c525bb0c781e8 Mon Sep 17 00:00:00 2001 From: Etienne Latendresse Date: Tue, 15 Sep 2026 13:30:36 -0400 Subject: [PATCH 2/5] secureSignals: emit one signal per source since GPT keys providers uniquely --- README.md | 2 +- lib/core/secure-signals.test.ts | 21 ++++++++++++++------- lib/core/secure-signals.ts | 18 +++++++++++------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e737e8c6..38be8e8d 100644 --- a/README.md +++ b/README.md @@ -918,7 +918,7 @@ The Optable Web SDK provides a method `installGPTSecureSignals` to pass user-def Please refer to the list of approved Secure Signal [providers](https://support.google.com/admanager/answer/14750072). Please refer to the provider's integration documentation for the exact provider name and value to pass as a signal. -To build the signals from cached EIDs instead of by hand, `secureSignalsFromEids(eids, filter?)` flattens each EID's uids into `{ provider, id }` pairs, optionally filtered by `sources`, `inserters` or `matchers` (an empty or missing list means no constraint on that field): +To build the signals from cached EIDs instead of by hand, `secureSignalsFromEids(eids, filter?)` builds one `{ provider, id }` pair per EID source — GPT keys secure signals by provider, so the first non-empty uid per source wins — optionally filtered by `sources`, `inserters` or `matchers` (an empty or missing list means no constraint on that field): ```javascript import { secureSignalsFromEids } from "@optable/web-sdk/lib/dist/core/secure-signals"; diff --git a/lib/core/secure-signals.test.ts b/lib/core/secure-signals.test.ts index c18b3fb3..ff1e57ad 100644 --- a/lib/core/secure-signals.test.ts +++ b/lib/core/secure-signals.test.ts @@ -7,31 +7,38 @@ const EIDS = [ ]; describe("secureSignalsFromEids", () => { - it("flattens every uid into a provider/id pair", () => { + it("emits one signal per source, taking the first non-empty uid", () => { expect(secureSignalsFromEids(EIDS)).toEqual([ { provider: "uidapi.com", id: "uid2-a" }, - { provider: "uidapi.com", id: "uid2-b" }, { provider: "id5-sync.com", id: "id5-a" }, ]); }); + it("keeps the first EID when a source appears twice", () => { + const dup = [ + { source: "pair.optable.co", uids: [{ id: "pair-1" }] }, + { source: "pair.optable.co", uids: [{ id: "pair-2" }] }, + ]; + expect(secureSignalsFromEids(dup)).toEqual([{ provider: "pair.optable.co", id: "pair-1" }]); + }); + it("filters by source, inserter and matcher", () => { expect(secureSignalsFromEids(EIDS, { sources: ["id5-sync.com"] })).toEqual([ { provider: "id5-sync.com", id: "id5-a" }, ]); - expect(secureSignalsFromEids(EIDS, { inserters: ["optable"] }).map((s) => s.provider)).toEqual([ - "uidapi.com", - "uidapi.com", - ]); + expect(secureSignalsFromEids(EIDS, { inserters: ["optable"] }).map((s) => s.provider)).toEqual(["uidapi.com"]); expect(secureSignalsFromEids(EIDS, { matchers: ["m2"] })).toEqual([{ provider: "id5-sync.com", id: "id5-a" }]); }); it("treats an empty filter list as no constraint", () => { - expect(secureSignalsFromEids(EIDS, { sources: [] })).toHaveLength(3); + expect(secureSignalsFromEids(EIDS, { sources: [] })).toHaveLength(2); }); it("skips uids without an id and tolerates empty input", () => { expect(secureSignalsFromEids([{ source: "a.com", uids: [{ id: "" }, {}] }])).toEqual([]); + expect(secureSignalsFromEids([{ source: "a.com", uids: [{ id: "" }, { id: "real" }] }])).toEqual([ + { provider: "a.com", id: "real" }, + ]); expect(secureSignalsFromEids([])).toEqual([]); }); }); diff --git a/lib/core/secure-signals.ts b/lib/core/secure-signals.ts index 20325c4b..498c196f 100644 --- a/lib/core/secure-signals.ts +++ b/lib/core/secure-signals.ts @@ -1,6 +1,10 @@ // Converts cached EIDs into GPT secure-signal pairs for // installGPTSecureSignals, optionally filtered by source, inserter or // matcher. An empty or missing filter list means no constraint on that field. +// +// One signal per source: GPT keys secure signals by provider, so registering +// a provider twice drops all but one id. The first EID and first non-empty +// uid win. type SecureSignalEid = { source: string; @@ -20,19 +24,19 @@ type SecureSignal = { provider: string; id: string }; export function secureSignalsFromEids(eids: SecureSignalEid[], filter: SecureSignalsFilter = {}): SecureSignal[] { const allows = (list: string[] | undefined, value: string | undefined) => !list?.length || list.includes(value ?? ""); - const signals: SecureSignal[] = []; + const bySource = new Map(); eids.forEach((eid) => { + if (bySource.has(eid.source)) return; if (!allows(filter.sources, eid.source)) return; if (!allows(filter.inserters, eid.inserter)) return; if (!allows(filter.matchers, eid.matcher)) return; - eid.uids?.forEach((uid) => { - if (uid.id) { - signals.push({ provider: eid.source, id: uid.id }); - } - }); + const uid = eid.uids?.find((u) => u.id); + if (uid?.id) { + bySource.set(eid.source, { provider: eid.source, id: uid.id }); + } }); - return signals; + return [...bySource.values()]; } export type { SecureSignalEid, SecureSignalsFilter, SecureSignal }; From 62f795751b70c2fae8493bc935206ee02996711d Mon Sep 17 00:00:00 2001 From: Etienne Latendresse Date: Wed, 16 Sep 2026 15:48:16 -0400 Subject: [PATCH 3/5] secureSignals: derive SecureSignalEid from the ORTB EID and expose the helper to script-tag pages --- README.md | 20 +++++++++++++++++++- browser/sdk.ts | 3 ++- lib/core/secure-signals.ts | 11 +++++------ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 38be8e8d..58bc3222 100644 --- a/README.md +++ b/README.md @@ -918,7 +918,9 @@ The Optable Web SDK provides a method `installGPTSecureSignals` to pass user-def Please refer to the list of approved Secure Signal [providers](https://support.google.com/admanager/answer/14750072). Please refer to the provider's integration documentation for the exact provider name and value to pass as a signal. -To build the signals from cached EIDs instead of by hand, `secureSignalsFromEids(eids, filter?)` builds one `{ provider, id }` pair per EID source — GPT keys secure signals by provider, so the first non-empty uid per source wins — optionally filtered by `sources`, `inserters` or `matchers` (an empty or missing list means no constraint on that field): +To build the signals from cached EIDs instead of by hand, `secureSignalsFromEids(eids, filter?)` builds one `{ provider, id }` pair per EID source — GPT keys secure signals by provider, so the first non-empty uid per source wins — optionally filtered by `sources`, `inserters` or `matchers` (an empty or missing list means no constraint on that field). + +With the npm package: ```javascript import { secureSignalsFromEids } from "@optable/web-sdk/lib/dist/core/secure-signals"; @@ -930,6 +932,22 @@ if (signals.length) { } ``` +With the SDK loaded from a script tag, the helper is available under `optable.utils`: + +```html + + +``` + You can verify the signal was correctly passed to GAM by searching for its value cached in `localStorage` under the key `_GESPSK-`. ## Integrating Prebid diff --git a/browser/sdk.ts b/browser/sdk.ts index d56266e6..731f8b4f 100644 --- a/browser/sdk.ts +++ b/browser/sdk.ts @@ -1,5 +1,6 @@ import Commands from "./commands"; import { resolveMultiNodeTargeting } from "../lib/core/resolvers/resolveMultiTargeting"; +import { secureSignalsFromEids } from "../lib/core/secure-signals"; import OptableSDK, { InitConfig } from "../lib/sdk"; import OptablePrebidAnalytics from "../lib/addons/prebid/analytics"; @@ -28,7 +29,7 @@ window.optable = window.optable || {}; window.optable.SDK = OptableSDK; window.optable.OptablePrebidAnalytics = OptablePrebidAnalytics; window.optable.cmd = new Commands(window.optable.cmd || []); -window.optable.utils = { resolveMultiNodeTargeting }; +window.optable.utils = { resolveMultiNodeTargeting, secureSignalsFromEids }; if (window.optable.instance_config) { window.optable.instance = new OptableSDK(window.optable.instance_config); diff --git a/lib/core/secure-signals.ts b/lib/core/secure-signals.ts index 498c196f..241921b0 100644 --- a/lib/core/secure-signals.ts +++ b/lib/core/secure-signals.ts @@ -1,3 +1,5 @@ +import type { EID, UID } from "iab-openrtb/v26"; + // Converts cached EIDs into GPT secure-signal pairs for // installGPTSecureSignals, optionally filtered by source, inserter or // matcher. An empty or missing filter list means no constraint on that field. @@ -6,12 +8,9 @@ // a provider twice drops all but one id. The first EID and first non-empty // uid win. -type SecureSignalEid = { - source: string; - inserter?: string; - matcher?: string; - uids?: Array<{ id?: string }>; -}; +// Cached EIDs are parsed from storage, so ORTB's required fields may be +// absent at runtime. Derived from EID so new or renamed ORTB fields flow through. +type SecureSignalEid = Partial> & Pick & { uids?: Array> }; type SecureSignalsFilter = { sources?: string[]; From 1ac390db690692b55f402d24769d2266729d7acc Mon Sep 17 00:00:00 2001 From: Etienne Latendresse Date: Tue, 20 Oct 2026 13:18:03 -0400 Subject: [PATCH 4/5] secureSignals: read the cache through the instance in the README and type installGPTSecureSignals Co-Authored-By: Claude Opus 5 --- README.md | 4 ++-- lib/addons/gpt.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 58bc3222..76481474 100644 --- a/README.md +++ b/README.md @@ -925,10 +925,10 @@ With the npm package: ```javascript import { secureSignalsFromEids } from "@optable/web-sdk/lib/dist/core/secure-signals"; -const eids = JSON.parse(localStorage.getItem("OPTABLE_RESOLVED") || "null")?.ortb2?.user?.eids ?? []; +const eids = sdk.targetingFromCache()?.ortb2?.user?.eids ?? []; const signals = secureSignalsFromEids(eids, { sources: ["uidapi.com"] }); if (signals.length) { - optable.instance.installGPTSecureSignals(...signals); + sdk.installGPTSecureSignals(...signals); } ``` diff --git a/lib/addons/gpt.ts b/lib/addons/gpt.ts index f059b197..ba450e20 100644 --- a/lib/addons/gpt.ts +++ b/lib/addons/gpt.ts @@ -1,12 +1,13 @@ import type { ContextualTargetingKeyValuesOptions } from "../edge/contextual_segments"; import type { WitnessProperties } from "../edge/witness"; +import type { SecureSignal } from "../core/secure-signals"; import { debugLog } from "../core/log"; import OptableSDK from "../sdk"; declare module "../sdk" { export interface OptableSDK { installGPTEventListeners: () => void; - installGPTSecureSignals: () => void; + installGPTSecureSignals: (...signals: SecureSignal[]) => void; } } @@ -86,7 +87,7 @@ OptableSDK.prototype.installGPTEventListeners = function (eventSpec?: GptEventSp /* * Pass user-defined signals to GAM Secure Signals */ -OptableSDK.prototype.installGPTSecureSignals = function (...signals: Array<{ provider: string; id: string }>) { +OptableSDK.prototype.installGPTSecureSignals = function (...signals: SecureSignal[]) { window.googletag = window.googletag || { cmd: [] }; const gpt = window.googletag; From 3e5be5605bac7d370309f8c7f4bcfb5197fd3437 Mon Sep 17 00:00:00 2001 From: Etienne Latendresse Date: Wed, 21 Oct 2026 11:33:18 -0400 Subject: [PATCH 5/5] secureSignals: accept the cache EID type and pin the per-source fall-through Co-Authored-By: Claude Opus 5 --- README.md | 14 +++++--------- lib/core/secure-signals.test.ts | 22 ++++++++++++++++++++++ lib/core/secure-signals.ts | 6 +++--- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 76481474..90c8ae16 100644 --- a/README.md +++ b/README.md @@ -918,7 +918,9 @@ The Optable Web SDK provides a method `installGPTSecureSignals` to pass user-def Please refer to the list of approved Secure Signal [providers](https://support.google.com/admanager/answer/14750072). Please refer to the provider's integration documentation for the exact provider name and value to pass as a signal. -To build the signals from cached EIDs instead of by hand, `secureSignalsFromEids(eids, filter?)` builds one `{ provider, id }` pair per EID source — GPT keys secure signals by provider, so the first non-empty uid per source wins — optionally filtered by `sources`, `inserters` or `matchers` (an empty or missing list means no constraint on that field). +To build the signals from cached EIDs instead of by hand, `secureSignalsFromEids(eids, filter?)` builds one `{ provider, id }` pair per EID source — GPT keys secure signals by provider, so the first EID that passes the filter and carries a non-empty uid wins its source — optionally filtered by `sources`, `inserters` or `matchers` (an empty or missing list means no constraint on that field). + +The signals come from the last cached targeting response, so a first visit installs none until `targeting()` has resolved once. With the npm package: @@ -926,10 +928,7 @@ With the npm package: import { secureSignalsFromEids } from "@optable/web-sdk/lib/dist/core/secure-signals"; const eids = sdk.targetingFromCache()?.ortb2?.user?.eids ?? []; -const signals = secureSignalsFromEids(eids, { sources: ["uidapi.com"] }); -if (signals.length) { - sdk.installGPTSecureSignals(...signals); -} +sdk.installGPTSecureSignals(...secureSignalsFromEids(eids, { sources: ["uidapi.com"] })); ``` With the SDK loaded from a script tag, the helper is available under `optable.utils`: @@ -940,10 +939,7 @@ With the SDK loaded from a script tag, the helper is available under `optable.ut window.optable = window.optable || { cmd: [] }; optable.cmd.push(function () { const eids = optable.instance.targetingFromCache()?.ortb2?.user?.eids ?? []; - const signals = optable.utils.secureSignalsFromEids(eids, { sources: ["uidapi.com"] }); - if (signals.length) { - optable.instance.installGPTSecureSignals(...signals); - } + optable.instance.installGPTSecureSignals(...optable.utils.secureSignalsFromEids(eids, { sources: ["uidapi.com"] })); }); ``` diff --git a/lib/core/secure-signals.test.ts b/lib/core/secure-signals.test.ts index ff1e57ad..02908881 100644 --- a/lib/core/secure-signals.test.ts +++ b/lib/core/secure-signals.test.ts @@ -22,6 +22,24 @@ describe("secureSignalsFromEids", () => { expect(secureSignalsFromEids(dup)).toEqual([{ provider: "pair.optable.co", id: "pair-1" }]); }); + it("falls through to a later EID of the same source when the first is empty or filtered out", () => { + expect( + secureSignalsFromEids([ + { source: "s.com", uids: [{ id: "" }] }, + { source: "s.com", uids: [{ id: "second" }] }, + ]) + ).toEqual([{ provider: "s.com", id: "second" }]); + expect( + secureSignalsFromEids( + [ + { source: "s.com", inserter: "other", uids: [{ id: "first" }] }, + { source: "s.com", inserter: "optable", uids: [{ id: "second" }] }, + ], + { inserters: ["optable"] } + ) + ).toEqual([{ provider: "s.com", id: "second" }]); + }); + it("filters by source, inserter and matcher", () => { expect(secureSignalsFromEids(EIDS, { sources: ["id5-sync.com"] })).toEqual([ { provider: "id5-sync.com", id: "id5-a" }, @@ -30,6 +48,10 @@ describe("secureSignalsFromEids", () => { expect(secureSignalsFromEids(EIDS, { matchers: ["m2"] })).toEqual([{ provider: "id5-sync.com", id: "id5-a" }]); }); + it("requires every provided filter to match", () => { + expect(secureSignalsFromEids(EIDS, { sources: ["uidapi.com"], matchers: ["m2"] })).toEqual([]); + }); + it("treats an empty filter list as no constraint", () => { expect(secureSignalsFromEids(EIDS, { sources: [] })).toHaveLength(2); }); diff --git a/lib/core/secure-signals.ts b/lib/core/secure-signals.ts index 241921b0..725710d3 100644 --- a/lib/core/secure-signals.ts +++ b/lib/core/secure-signals.ts @@ -5,12 +5,12 @@ import type { EID, UID } from "iab-openrtb/v26"; // matcher. An empty or missing filter list means no constraint on that field. // // One signal per source: GPT keys secure signals by provider, so registering -// a provider twice drops all but one id. The first EID and first non-empty -// uid win. +// a provider twice drops all but one id. The first EID that passes the filter +// and carries a non-empty uid wins its source. // Cached EIDs are parsed from storage, so ORTB's required fields may be // absent at runtime. Derived from EID so new or renamed ORTB fields flow through. -type SecureSignalEid = Partial> & Pick & { uids?: Array> }; +type SecureSignalEid = Partial> & Pick & { uids?: Array, "id">> }; type SecureSignalsFilter = { sources?: string[];