From dffc558e0f9758e4fc444ccae9b355ca352f1787 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 15 Dec 2025 14:58:19 -0700 Subject: [PATCH 1/6] add dockerfile and update readme instructions for hashing tool --- README.md | 1 + docker-compose.yml | 11 +- siteDetails.js | 8 + tools/hashing-tool/Dockerfile | 25 + tools/hashing-tool/README.md | 38 + tools/hashing-tool/entrypoint.sh | 15 + tools/hashing-tool/index.html | 37 +- tools/hashing-tool/uid2-sdk-3.3.0.js | 1520 --------------------- tools/reverse-proxy/README.md | 3 + tools/reverse-proxy/default.conf.template | 26 +- tools/reverse-proxy/docker-entrypoint.sh | 5 +- 11 files changed, 155 insertions(+), 1534 deletions(-) create mode 100644 tools/hashing-tool/Dockerfile create mode 100644 tools/hashing-tool/README.md create mode 100644 tools/hashing-tool/entrypoint.sh delete mode 100644 tools/hashing-tool/uid2-sdk-3.3.0.js diff --git a/README.md b/README.md index 34600a08..aec8e94e 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ See the list below for the name of all individual services. | `prebid-client-server` | Prebid Client Server | 3052 | http://localhost:3052 | | `prebid-client-side-deferred` | Prebid Client Side Deferred | 3053 | http://localhost:3053 | | `prebid-secure-signals-client-side` | Prebid Secure Signals | 3061 | http://localhost:3061 | +| `hashing-tool` | Hashing Tool | 3071 | http://localhost:3071 | --- diff --git a/docker-compose.yml b/docker-compose.yml index fd1525d6..b521d2b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -139,4 +139,13 @@ services: env_file: - .env - + # tools + hashing-tool: + build: + context: tools/hashing-tool + dockerfile: Dockerfile + ports: + - "3071:3071" + container_name: hashing-tool + env_file: + - .env diff --git a/siteDetails.js b/siteDetails.js index 720d2e0a..1d12870a 100644 --- a/siteDetails.js +++ b/siteDetails.js @@ -85,6 +85,14 @@ const sites = [ port: 3061, description: 'Prebid Secure Signals Client Side', }, + + // Tools + { + name: 'hashing-tool', + domain: 'hashing-tool.sample-dev.com', + port: 3071, + description: 'Hashing Tool', + }, ]; // Export for CommonJS (used by createCA.ts) diff --git a/tools/hashing-tool/Dockerfile b/tools/hashing-tool/Dockerfile new file mode 100644 index 00000000..d4448bc9 --- /dev/null +++ b/tools/hashing-tool/Dockerfile @@ -0,0 +1,25 @@ +FROM nginx:alpine + +# Install gettext for envsubst +RUN apk add --no-cache gettext + +# Copy static files +COPY index.html /usr/share/nginx/html/index.html +COPY app.css /usr/share/nginx/html/app.css + +# Copy entrypoint script +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Configure nginx to serve on port 3071 +RUN echo 'server { \ + listen 3071; \ + location / { \ + root /usr/share/nginx/html; \ + index index.html; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +EXPOSE 3071 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/tools/hashing-tool/README.md b/tools/hashing-tool/README.md new file mode 100644 index 00000000..a6fd58d3 --- /dev/null +++ b/tools/hashing-tool/README.md @@ -0,0 +1,38 @@ +# UID2/EUID Hashing Tool + +A tool to verify that your implementation is normalizing and hashing email addresses and phone numbers correctly for UID2 and EUID. + +> **Note:** The normalization and hashing logic is identical for both UID2 and EUID. + +## Running Locally + +### Using Docker Compose + +From the repository root: + +```bash +docker-compose up -d hashing-tool +``` + +Access at: http://localhost:3071 + +### Using the Reverse Proxy (HTTPS) + +```bash +docker-compose up -d +``` + +Access at: https://hashing-tool.sample-dev.com (requires hosts file and certificate setup — see [reverse-proxy README](../reverse-proxy/README.md)) + +## Usage + +1. Select **Email** or **Phone Number** +2. Enter the value to hash +3. Click **Enter** +4. View the normalized value, SHA-256 hash, and base64-encoded result + +## Documentation + +- [UID2 Normalization and Encoding](https://unifiedid.com/docs/getting-started/gs-normalization-encoding) +- [EUID Normalization and Encoding](https://euid.eu/docs/getting-started/gs-normalization-encoding) + diff --git a/tools/hashing-tool/entrypoint.sh b/tools/hashing-tool/entrypoint.sh new file mode 100644 index 00000000..718567e2 --- /dev/null +++ b/tools/hashing-tool/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# Set default values if not provided +export UID_JS_SDK_URL=${UID_JS_SDK_URL:-"https://cdn.integ.uidapi.com/uid2-sdk-4.0.1.js"} +export UID_JS_SDK_NAME=${UID_JS_SDK_NAME:-"__uid2"} +export IDENTITY_NAME=${IDENTITY_NAME:-"UID2"} +export DOCS_BASE_URL=${DOCS_BASE_URL:-"https://unifiedid.com/docs"} + +# Process index.html template with environment variables +envsubst < /usr/share/nginx/html/index.html > /usr/share/nginx/html/index.temp.html +mv /usr/share/nginx/html/index.temp.html /usr/share/nginx/html/index.html + +# Start nginx +exec nginx -g "daemon off;" + diff --git a/tools/hashing-tool/index.html b/tools/hashing-tool/index.html index e40ba706..953d2c67 100644 --- a/tools/hashing-tool/index.html +++ b/tools/hashing-tool/index.html @@ -2,22 +2,30 @@ - UID2 Hashing Tool + ${IDENTITY_NAME} Hashing Tool - - + -

UID2 Hashing Tool

+

${IDENTITY_NAME} Hashing Tool

Use this tool to verify that your own implementation is normalizing and encoding correctly. Choose Email or Phone Number, then type or paste the value and click Enter.

NOTE: Normalize phone numbers before using the tool. - For details and examples, see Normalization and Encoding. + For details and examples, see Normalization and Encoding.

diff --git a/tools/hashing-tool/uid2-sdk-3.3.0.js b/tools/hashing-tool/uid2-sdk-3.3.0.js deleted file mode 100644 index cd772691..00000000 --- a/tools/hashing-tool/uid2-sdk-3.3.0.js +++ /dev/null @@ -1,1520 +0,0 @@ -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ 531: -/***/ ((__unused_webpack_module, exports) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.isOptoutIdentity = exports.isValidIdentity = void 0; -function isValidIdentity(identity) { - return (typeof identity === 'object' && - identity !== null && - 'advertising_token' in identity && - 'identity_expires' in identity && - 'refresh_from' in identity && - 'refresh_token' in identity && - 'refresh_expires' in identity); -} -exports.isValidIdentity = isValidIdentity; -function isOptoutIdentity(identity) { - if (identity === null || typeof identity !== 'object') - return false; - const maybeIdentity = identity; - return maybeIdentity.status === 'optout'; -} -exports.isOptoutIdentity = isOptoutIdentity; - - -/***/ }), - -/***/ 367: -/***/ (function(__unused_webpack_module, exports, __webpack_require__) { - - -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ApiClient = void 0; -const sdkBase_1 = __webpack_require__(533); -const Identity_1 = __webpack_require__(531); -const cstgBox_1 = __webpack_require__(828); -const cstgCrypto_1 = __webpack_require__(135); -const clientSideIdentityOptions_1 = __webpack_require__(522); -const base64_1 = __webpack_require__(819); -function isValidRefreshResponse(response) { - if (isUnvalidatedRefreshResponse(response)) { - return (response.status === 'optout' || - response.status === 'expired_token' || - (response.status === 'success' && 'body' in response && (0, Identity_1.isValidIdentity)(response.body))); - } - return false; -} -function isUnvalidatedRefreshResponse(response) { - return typeof response === 'object' && response !== null && 'status' in response; -} -function isCstgApiSuccessResponse(response) { - if (response === null || typeof response !== 'object') { - return false; - } - const successResponse = response; - return successResponse.status === 'success' && (0, Identity_1.isValidIdentity)(successResponse.body); -} -function isCstgApiOptoutResponse(response) { - if (response === null || typeof response !== 'object') { - return false; - } - const optoutResponse = response; - return optoutResponse.status === 'optout'; -} -function isCstgApiClientErrorResponse(response) { - if (response === null || typeof response !== 'object') { - return false; - } - const errorResponse = response; - return errorResponse.status === 'client_error' && typeof errorResponse.message === 'string'; -} -function isCstgApiForbiddenResponse(response) { - if (response === null || typeof response !== 'object') { - return false; - } - const forbiddenResponse = response; - return (forbiddenResponse.status === 'invalid_http_origin' && - typeof forbiddenResponse.message === 'string'); -} -class ApiClient { - constructor(opts, defaultBaseUrl, productName) { - var _a; - this._requestsInFlight = []; - this._baseUrl = (_a = opts.baseUrl) !== null && _a !== void 0 ? _a : defaultBaseUrl; - this._productName = productName; - this._clientVersion = productName.toLowerCase() + '-sdk-' + sdkBase_1.SdkBase.VERSION; - } - hasActiveRequests() { - return this._requestsInFlight.length > 0; - } - ResponseToRefreshResult(response) { - if (isValidRefreshResponse(response)) { - if (response.status === 'success') - return { status: response.status, identity: response.body }; - return response; - } - else - return "Response didn't contain a valid status"; - } - abortActiveRequests() { - this._requestsInFlight.forEach((req) => { - req.abort(); - }); - this._requestsInFlight = []; - } - callRefreshApi(refreshDetails) { - const url = this._baseUrl + '/v2/token/refresh'; - const req = new XMLHttpRequest(); - this._requestsInFlight.push(req); - req.overrideMimeType('text/plain'); - req.open('POST', url, true); - req.setRequestHeader('X-UID2-Client-Version', this._clientVersion); // N.B. EUID and UID2 currently both use the same header - let resolvePromise; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let rejectPromise; - const promise = new Promise((resolve, reject) => { - resolvePromise = resolve; - rejectPromise = reject; - }); - req.onreadystatechange = () => { - if (req.readyState !== req.DONE) - return; - this._requestsInFlight = this._requestsInFlight.filter((r) => r !== req); - try { - if (!refreshDetails.refresh_response_key || req.status !== 200) { - const response = JSON.parse(req.responseText); - const result = this.ResponseToRefreshResult(response); - if (typeof result === 'string') - rejectPromise(result); - else - resolvePromise(result); - } - else { - const encodeResp = (0, base64_1.base64ToBytes)(req.responseText); - window.crypto.subtle - .importKey('raw', (0, base64_1.base64ToBytes)(refreshDetails.refresh_response_key), { name: 'AES-GCM' }, false, ['decrypt']) - .then((key) => { - //returns the symmetric key - window.crypto.subtle - .decrypt({ - name: 'AES-GCM', - iv: encodeResp.slice(0, 12), - tagLength: 128, //The tagLength you used to encrypt (if any) - }, key, encodeResp.slice(12)) - .then((decrypted) => { - const decryptedResponse = String.fromCharCode(...new Uint8Array(decrypted)); - const response = JSON.parse(decryptedResponse); - const result = this.ResponseToRefreshResult(response); - if (typeof result === 'string') - rejectPromise(result); - else - resolvePromise(result); - }, (reason) => rejectPromise(`Call to ${this._productName} API failed: ` + reason)); - }, (reason) => rejectPromise(`Call to ${this._productName} API failed: ` + reason)); - } - } - catch (err) { - rejectPromise(err); - } - }; - req.send(refreshDetails.refresh_token); - return promise; - } - callCstgApi(data, opts) { - return __awaiter(this, void 0, void 0, function* () { - const optoutPayload = this._productName == 'EUID' ? { optout_check: 1 } : {}; - const request = 'emailHash' in data - ? Object.assign({ email_hash: data.emailHash }, optoutPayload) : Object.assign({ phone_hash: data.phoneHash }, optoutPayload); - const box = yield cstgBox_1.CstgBox.build((0, clientSideIdentityOptions_1.stripPublicKeyPrefix)(opts.serverPublicKey)); - const encoder = new TextEncoder(); - const now = Date.now(); - const { iv, ciphertext } = yield box.encrypt(encoder.encode(JSON.stringify(request)), encoder.encode(JSON.stringify([now]))); - const exportedPublicKey = yield (0, cstgCrypto_1.exportPublicKey)(box.clientPublicKey); - const requestBody = { - payload: (0, base64_1.bytesToBase64)(new Uint8Array(ciphertext)), - iv: (0, base64_1.bytesToBase64)(new Uint8Array(iv)), - public_key: (0, base64_1.bytesToBase64)(new Uint8Array(exportedPublicKey)), - timestamp: now, - subscription_id: opts.subscriptionId, - }; - const url = this._baseUrl + '/v2/token/client-generate'; - const req = new XMLHttpRequest(); - this._requestsInFlight.push(req); - req.overrideMimeType('text/plain'); - req.open('POST', url, true); - let resolvePromise; - let rejectPromise; - const promise = new Promise((resolve, reject) => { - resolvePromise = resolve; - rejectPromise = reject; - }); - req.onreadystatechange = () => __awaiter(this, void 0, void 0, function* () { - if (req.readyState !== req.DONE) - return; - this._requestsInFlight = this._requestsInFlight.filter((r) => r !== req); - try { - if (req.status === 200) { - const encodedResp = (0, base64_1.base64ToBytes)(req.responseText); - const decrypted = yield box.decrypt(encodedResp.slice(0, 12), encodedResp.slice(12)); - const decryptedResponse = new TextDecoder().decode(decrypted); - const response = JSON.parse(decryptedResponse); - if (isCstgApiSuccessResponse(response)) { - resolvePromise({ - status: 'success', - identity: response.body, - }); - } - else if (isCstgApiOptoutResponse(response)) { - resolvePromise({ - status: 'optout', - }); - } - else { - // A 200 should always be a success response. - // Something has gone wrong. - rejectPromise(`API error: Response body was invalid for HTTP status 200: ${decryptedResponse}`); - } - } - else if (req.status === 400) { - const response = JSON.parse(req.responseText); - if (isCstgApiClientErrorResponse(response)) { - rejectPromise(`Client error: ${response.message}`); - } - else { - // A 400 should always be a client error. - // Something has gone wrong. - rejectPromise(`API error: Response body was invalid for HTTP status 400: ${req.responseText}`); - } - } - else if (req.status === 403) { - const response = JSON.parse(req.responseText); - if (isCstgApiForbiddenResponse(response)) { - rejectPromise(`Forbidden: ${response.message}`); - } - else { - // A 403 should always be a forbidden response. - // Something has gone wrong. - rejectPromise(`API error: Response body was invalid for HTTP status 403: ${req.responseText}`); - } - } - else { - rejectPromise(`API error: Unexpected HTTP status ${req.status}`); - } - } - catch (err) { - rejectPromise(err); - } - }); - req.send(JSON.stringify(requestBody)); - return yield promise; - }); - } -} -exports.ApiClient = ApiClient; - - -/***/ }), - -/***/ 230: -/***/ ((__unused_webpack_module, exports) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.CallbackManager = exports.EventType = void 0; -var EventType; -(function (EventType) { - EventType["InitCompleted"] = "InitCompleted"; - EventType["IdentityUpdated"] = "IdentityUpdated"; - EventType["SdkLoaded"] = "SdkLoaded"; - EventType["OptoutReceived"] = "OptoutReceived"; -})(EventType = exports.EventType || (exports.EventType = {})); -class CallbackManager { - constructor(sdk, productName, getIdentity, logger) { - this._sentInit = false; - this._productName = productName; - this._logger = logger; - this._getIdentity = getIdentity; - this._sdk = sdk; - this._sdk.callbacks.push = this.callbackPushInterceptor.bind(this); - } - callbackPushInterceptor(...args) { - var _a; - for (const c of args) { - if (CallbackManager._sentSdkLoaded[this._productName]) - this.safeRunCallback(c, EventType.SdkLoaded, {}); - if (this._sentInit) - this.safeRunCallback(c, EventType.InitCompleted, { - identity: (_a = this._getIdentity()) !== null && _a !== void 0 ? _a : null, - }); - } - return Array.prototype.push.apply(this._sdk.callbacks, args); - } - runCallbacks(event, payload) { - var _a; - if (event === EventType.InitCompleted) - this._sentInit = true; - if (event === EventType.SdkLoaded) - CallbackManager._sentSdkLoaded[this._productName] = true; - if (!this._sentInit && event !== EventType.SdkLoaded) - return; - const enrichedPayload = Object.assign(Object.assign({}, payload), { identity: (_a = this._getIdentity()) !== null && _a !== void 0 ? _a : null }); - for (const callback of this._sdk.callbacks) { - this.safeRunCallback(callback, event, enrichedPayload); - } - } - safeRunCallback(callback, event, payload) { - if (typeof callback === 'function') { - try { - callback(event, payload); - } - catch (exception) { - this._logger.warn('SDK callback threw an exception', exception); - } - } - else { - this._logger.warn("An SDK callback was supplied which isn't a function."); - } - } -} -exports.CallbackManager = CallbackManager; -CallbackManager._sentSdkLoaded = {}; - - -/***/ }), - -/***/ 522: -/***/ ((__unused_webpack_module, exports) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.isClientSideIdentityOptionsOrThrow = exports.stripPublicKeyPrefix = void 0; -const SERVER_PUBLIC_KEY_PREFIX_LENGTH = 9; -function stripPublicKeyPrefix(serverPublicKey) { - return serverPublicKey.substring(SERVER_PUBLIC_KEY_PREFIX_LENGTH); -} -exports.stripPublicKeyPrefix = stripPublicKeyPrefix; -function isClientSideIdentityOptionsOrThrow(maybeOpts) { - if (typeof maybeOpts !== 'object' || maybeOpts === null) { - throw new TypeError('opts must be an object'); - } - const opts = maybeOpts; - if (typeof opts.serverPublicKey !== 'string') { - throw new TypeError('opts.serverPublicKey must be a string'); - } - const serverPublicKeyPrefix = /^UID2-X-[A-Z]-.+/; - if (!serverPublicKeyPrefix.test(opts.serverPublicKey)) { - throw new TypeError(`opts.serverPublicKey must match the regular expression ${serverPublicKeyPrefix}`); - } - // We don't do any further validation of the public key, as we will find out - // later if it's valid by using importKey. - if (typeof opts.subscriptionId !== 'string') { - throw new TypeError('opts.subscriptionId must be a string'); - } - if (opts.subscriptionId.length === 0) { - throw new TypeError('opts.subscriptionId is empty'); - } - return true; -} -exports.isClientSideIdentityOptionsOrThrow = isClientSideIdentityOptionsOrThrow; - - -/***/ }), - -/***/ 852: -/***/ ((__unused_webpack_module, exports, __webpack_require__) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.CookieManager = exports.isLegacyCookie = void 0; -const Identity_1 = __webpack_require__(531); -function isLegacyCookie(cookie) { - if (typeof cookie !== 'object' || !cookie) - return false; - const partialCookie = cookie; - if ('advertising_token' in partialCookie && - 'refresh_token' in partialCookie && - partialCookie.advertising_token && - partialCookie.refresh_token) - return true; - return false; -} -exports.isLegacyCookie = isLegacyCookie; -function enrichIdentity(identity, now) { - return Object.assign({ refresh_from: now, refresh_expires: now + 7 * 86400 * 1000, identity_expires: now + 4 * 3600 * 1000 }, identity); -} -class CookieManager { - constructor(opts, cookieName) { - this._cookieName = cookieName; - this._opts = opts; - } - setCookie(identity) { - var _a; - const value = JSON.stringify(identity); - const expires = new Date(identity.refresh_expires); - const path = (_a = this._opts.cookiePath) !== null && _a !== void 0 ? _a : '/'; - let cookie = this._cookieName + - '=' + - encodeURIComponent(value) + - ' ;path=' + - path + - ';expires=' + - expires.toUTCString(); - if (typeof this._opts.cookieDomain !== 'undefined') { - cookie += ';domain=' + this._opts.cookieDomain; - } - document.cookie = cookie; - } - removeCookie() { - document.cookie = this._cookieName + '=;expires=Tue, 1 Jan 1980 23:59:59 GMT'; - } - getCookie() { - const docCookie = document.cookie; - if (docCookie) { - const payload = docCookie.split('; ').find((row) => row.startsWith(this._cookieName + '=')); - if (payload) { - return decodeURIComponent(payload.split('=')[1]); - } - } - } - migrateLegacyCookie(identity, now) { - const newCookie = enrichIdentity(identity, now); - this.setCookie(newCookie); - return newCookie; - } - loadIdentityFromCookie() { - const payload = this.getCookie(); - if (payload) { - const result = JSON.parse(payload); - if ((0, Identity_1.isValidIdentity)(result)) - return result; - if ((0, Identity_1.isOptoutIdentity)(result)) - return result; - if (isLegacyCookie(result)) { - return this.migrateLegacyCookie(result, Date.now()); - } - } - return null; - } -} -exports.CookieManager = CookieManager; - - -/***/ }), - -/***/ 828: -/***/ (function(__unused_webpack_module, exports, __webpack_require__) { - - -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.CstgBox = void 0; -const cstgCrypto_1 = __webpack_require__(135); -class CstgBox { - constructor(clientPublicKey, sharedKey) { - this._clientPublicKey = clientPublicKey; - this._sharedKey = sharedKey; - } - static build(serverPublicKey) { - return __awaiter(this, void 0, void 0, function* () { - const clientKeyPair = yield (0, cstgCrypto_1.generateKeyPair)(CstgBox._namedCurve); - const importedServerPublicKey = yield (0, cstgCrypto_1.importPublicKey)(serverPublicKey, this._namedCurve); - const sharedKey = yield (0, cstgCrypto_1.deriveKey)(importedServerPublicKey, clientKeyPair.privateKey); - return new CstgBox(clientKeyPair.publicKey, sharedKey); - }); - } - encrypt(plaintext, additionalData) { - return __awaiter(this, void 0, void 0, function* () { - const iv = window.crypto.getRandomValues(new Uint8Array(12)); - const ciphertext = yield (0, cstgCrypto_1.encrypt)(plaintext, this._sharedKey, iv, additionalData); - return { - iv: iv, - ciphertext: ciphertext, - }; - }); - } - decrypt(iv, ciphertext) { - return __awaiter(this, void 0, void 0, function* () { - return yield (0, cstgCrypto_1.decrypt)(ciphertext, this._sharedKey, iv); - }); - } - get clientPublicKey() { - return this._clientPublicKey; - } -} -exports.CstgBox = CstgBox; -CstgBox._namedCurve = 'P-256'; - - -/***/ }), - -/***/ 135: -/***/ ((__unused_webpack_module, exports, __webpack_require__) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.decrypt = exports.encrypt = exports.deriveKey = exports.exportPublicKey = exports.importPublicKey = exports.generateKeyPair = void 0; -const base64_1 = __webpack_require__(819); -function generateKeyPair(namedCurve) { - const params = { - name: 'ECDH', - namedCurve: namedCurve, - }; - return window.crypto.subtle.generateKey(params, false, ['deriveKey']); -} -exports.generateKeyPair = generateKeyPair; -function importPublicKey(publicKey, namedCurve) { - const params = { - name: 'ECDH', - namedCurve: namedCurve, - }; - return window.crypto.subtle.importKey('spki', (0, base64_1.base64ToBytes)(publicKey), params, false, []); -} -exports.importPublicKey = importPublicKey; -function exportPublicKey(publicKey) { - return window.crypto.subtle.exportKey('spki', publicKey); -} -exports.exportPublicKey = exportPublicKey; -function deriveKey(serverPublicKey, clientPrivateKey) { - return window.crypto.subtle.deriveKey({ - name: 'ECDH', - public: serverPublicKey, - }, clientPrivateKey, { - name: 'AES-GCM', - length: 256, - }, false, ['encrypt', 'decrypt']); -} -exports.deriveKey = deriveKey; -function encrypt(data, key, iv, additionalData) { - return window.crypto.subtle.encrypt({ - name: 'AES-GCM', - iv: iv, - additionalData: additionalData, - }, key, data); -} -exports.encrypt = encrypt; -function decrypt(data, key, iv) { - return window.crypto.subtle.decrypt({ - name: 'AES-GCM', - iv: iv, - }, key, data); -} -exports.decrypt = decrypt; - - -/***/ }), - -/***/ 838: -/***/ ((__unused_webpack_module, exports) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.normalizeEmail = exports.isNormalizedPhone = void 0; -function isNormalizedPhone(phone) { - return /^\+[0-9]{10,15}$/.test(phone); -} -exports.isNormalizedPhone = isNormalizedPhone; -const EMAIL_EXTENSION_SYMBOL = '+'; -const EMAIL_DOT = '.'; -const GMAIL_DOMAIN = 'gmail.com'; -function splitEmailIntoAddressAndDomain(email) { - const parts = email.split('@'); - if (!parts.length || parts.length !== 2 || parts.some((part) => part === '')) - return; - return { - address: parts[0], - domain: parts[1], - }; -} -function isGmail(domain) { - return domain === GMAIL_DOMAIN; -} -function dropExtension(address, extensionSymbol = EMAIL_EXTENSION_SYMBOL) { - return address.split(extensionSymbol)[0]; -} -function normalizeAddressPart(address, shouldRemoveDot, shouldDropExtension) { - let parsedAddress = address; - if (shouldRemoveDot) - parsedAddress = parsedAddress.replaceAll(EMAIL_DOT, ''); - if (shouldDropExtension) - parsedAddress = dropExtension(parsedAddress); - return parsedAddress; -} -function normalizeEmail(email) { - if (!email || !email.length) - return; - const parsedEmail = email.trim().toLowerCase(); - if (parsedEmail.indexOf(' ') > 0) - return; - const emailParts = splitEmailIntoAddressAndDomain(parsedEmail); - if (!emailParts) - return; - const { address, domain } = emailParts; - const emailIsGmail = isGmail(domain); - const parsedAddress = normalizeAddressPart(address, emailIsGmail, emailIsGmail); - return parsedAddress ? `${parsedAddress}@${domain}` : undefined; -} -exports.normalizeEmail = normalizeEmail; - - -/***/ }), - -/***/ 819: -/***/ ((__unused_webpack_module, exports) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.bytesToBase64 = exports.base64ToBytes = void 0; -function base64ToBytes(base64) { - const binString = atob(base64); - return Uint8Array.from(binString, (m) => m.codePointAt(0)); -} -exports.base64ToBytes = base64ToBytes; -function bytesToBase64(bytes) { - const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join(''); - return btoa(binString); -} -exports.bytesToBase64 = bytesToBase64; - - -/***/ }), - -/***/ 699: -/***/ (function(__unused_webpack_module, exports, __webpack_require__) { - - -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.hashIdentifier = exports.hashAndEncodeIdentifier = void 0; -const base64_1 = __webpack_require__(819); -function hashAndEncodeIdentifier(value) { - return __awaiter(this, void 0, void 0, function* () { - const hash = yield window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)); - return (0, base64_1.bytesToBase64)(new Uint8Array(hash)); - }); -} -exports.hashAndEncodeIdentifier = hashAndEncodeIdentifier; -function hashIdentifier(value) { - return __awaiter(this, void 0, void 0, function* () { - const hash = yield window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)); - // converting 32-byte SHA-256 to hex-encoded representation - return [...new Uint8Array(hash)].map((x) => x.toString(16).padStart(2, '0')).join(''); - }); -} -exports.hashIdentifier = hashIdentifier; - - -/***/ }), - -/***/ 479: -/***/ ((__unused_webpack_module, exports, __webpack_require__) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.SdkBase = exports.hashAndEncodeIdentifier = exports.isBase64Hash = exports.isNormalizedPhone = exports.isClientSideIdentityOptionsOrThrow = exports.EventType = void 0; -var callbackManager_1 = __webpack_require__(230); -Object.defineProperty(exports, "EventType", ({ enumerable: true, get: function () { return callbackManager_1.EventType; } })); -var clientSideIdentityOptions_1 = __webpack_require__(522); -Object.defineProperty(exports, "isClientSideIdentityOptionsOrThrow", ({ enumerable: true, get: function () { return clientSideIdentityOptions_1.isClientSideIdentityOptionsOrThrow; } })); -var diiNormalization_1 = __webpack_require__(838); -Object.defineProperty(exports, "isNormalizedPhone", ({ enumerable: true, get: function () { return diiNormalization_1.isNormalizedPhone; } })); -var hashedDii_1 = __webpack_require__(254); -Object.defineProperty(exports, "isBase64Hash", ({ enumerable: true, get: function () { return hashedDii_1.isBase64Hash; } })); -var hash_1 = __webpack_require__(699); -Object.defineProperty(exports, "hashAndEncodeIdentifier", ({ enumerable: true, get: function () { return hash_1.hashAndEncodeIdentifier; } })); -var sdkBase_1 = __webpack_require__(533); -Object.defineProperty(exports, "SdkBase", ({ enumerable: true, get: function () { return sdkBase_1.SdkBase; } })); - - -/***/ }), - -/***/ 254: -/***/ ((__unused_webpack_module, exports) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.isBase64Hash = void 0; -function isBase64Hash(value) { - if (!(value && value.length === 44)) { - return false; - } - try { - return btoa(atob(value)) === value; - } - catch (err) { - return false; - } -} -exports.isBase64Hash = isBase64Hash; - - -/***/ }), - -/***/ 755: -/***/ ((__unused_webpack_module, exports) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.notifyInitCallback = exports.IdentityStatus = void 0; -var IdentityStatus; -(function (IdentityStatus) { - IdentityStatus[IdentityStatus["ESTABLISHED"] = 0] = "ESTABLISHED"; - IdentityStatus[IdentityStatus["REFRESHED"] = 1] = "REFRESHED"; - IdentityStatus[IdentityStatus["EXPIRED"] = 100] = "EXPIRED"; - IdentityStatus[IdentityStatus["NO_IDENTITY"] = -1] = "NO_IDENTITY"; - IdentityStatus[IdentityStatus["INVALID"] = -2] = "INVALID"; - IdentityStatus[IdentityStatus["REFRESH_EXPIRED"] = -3] = "REFRESH_EXPIRED"; - IdentityStatus[IdentityStatus["OPTOUT"] = -4] = "OPTOUT"; -})(IdentityStatus = exports.IdentityStatus || (exports.IdentityStatus = {})); -function notifyInitCallback(options, status, statusText, advertisingToken, logger) { - if (options.callback) { - const payload = { - advertisingToken: advertisingToken, - advertising_token: advertisingToken, - status: status, - statusText: statusText, - }; - try { - options.callback(payload); - } - catch (exception) { - logger.warn('SDK init callback threw an exception', exception); - } - } -} -exports.notifyInitCallback = notifyInitCallback; - - -/***/ }), - -/***/ 669: -/***/ ((__unused_webpack_module, exports, __webpack_require__) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.LocalStorageManager = void 0; -const Identity_1 = __webpack_require__(531); -class LocalStorageManager { - constructor(storageKey) { - this._storageKey = storageKey; - } - setValue(identity) { - const value = JSON.stringify(identity); - localStorage.setItem(this._storageKey, value); - } - removeValue() { - localStorage.removeItem(this._storageKey); - } - getValue() { - return localStorage.getItem(this._storageKey); - } - loadIdentityFromLocalStorage() { - const payload = this.getValue(); - if (payload) { - const result = JSON.parse(payload); - if ((0, Identity_1.isValidIdentity)(result)) - return result; - if ((0, Identity_1.isOptoutIdentity)(result)) - return result; - } - return null; - } -} -exports.LocalStorageManager = LocalStorageManager; - - -/***/ }), - -/***/ 317: -/***/ ((__unused_webpack_module, exports, __webpack_require__) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.PromiseHandler = void 0; -const callbackManager_1 = __webpack_require__(230); -class PromiseHandler { - constructor(sdk) { - this._promises = []; - this._seenInitOrRejectAll = false; - sdk.callbacks.push(this._handleEvent.bind(this)); - } - _handleEvent(eventType, payload) { - if (eventType !== callbackManager_1.EventType.InitCompleted && eventType !== callbackManager_1.EventType.IdentityUpdated) - return; - if (eventType === callbackManager_1.EventType.InitCompleted) { - this._seenInitOrRejectAll = true; - } - if (!this._apiClient || !this._apiClient.hasActiveRequests()) { - this._promises.forEach((p) => { - if ('identity' in payload && payload.identity) { - p.resolve(payload.identity.advertising_token); - } - else { - p.reject(new Error(`No identity available.`)); - } - }); - this._promises = []; - } - } - rejectAllPromises(reason) { - this._seenInitOrRejectAll = true; - this._promises.forEach((p) => { - p.reject(reason); - }); - this._promises = []; - } - // n.b. If this has seen an SDK init and there is no active request or a reject-all call, it'll reply immediately with the provided token or rejection. - // Otherwise, it will ignore the provided token and resolve with the identity available when the init event arrives - createMaybeDeferredPromise(token) { - if (!this._seenInitOrRejectAll || (this._apiClient && this._apiClient.hasActiveRequests())) { - return new Promise((resolve, reject) => { - this._promises.push({ - resolve, - reject, - }); - }); - } - else { - if (token) - return Promise.resolve(token); - else - return Promise.reject(new Error('Identity not available')); - } - } - registerApiClient(apiClient) { - this._apiClient = apiClient; - } -} -exports.PromiseHandler = PromiseHandler; - - -/***/ }), - -/***/ 533: -/***/ (function(__unused_webpack_module, exports, __webpack_require__) { - - -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.SdkBase = void 0; -const package_json_1 = __webpack_require__(147); -const Identity_1 = __webpack_require__(531); -const initCallbacks_1 = __webpack_require__(755); -const sdkOptions_1 = __webpack_require__(512); -const logger_1 = __webpack_require__(980); -const apiClient_1 = __webpack_require__(367); -const callbackManager_1 = __webpack_require__(230); -const clientSideIdentityOptions_1 = __webpack_require__(522); -const diiNormalization_1 = __webpack_require__(838); -const hashedDii_1 = __webpack_require__(254); -const promiseHandler_1 = __webpack_require__(317); -const storageManager_1 = __webpack_require__(505); -const hash_1 = __webpack_require__(699); -function hasExpired(expiry, now = Date.now()) { - return expiry <= now; -} -class SdkBase { - // Sets up nearly everything, but does not run SdkLoaded callbacks - derived classes must run them. - constructor(existingCallbacks = undefined, product) { - // Push functions to this array to receive event notifications - this.callbacks = []; - this._opts = {}; - this._initComplete = false; - this._refreshTimerId = null; - this._product = product; - this._logger = (0, logger_1.MakeLogger)(console, product.name); - const exception = new Error(); - this._logger.log(`Constructing an SDK!`, exception.stack); - if (existingCallbacks) - this.callbacks = existingCallbacks; - this._tokenPromiseHandler = new promiseHandler_1.PromiseHandler(this); - this._callbackManager = new callbackManager_1.CallbackManager(this, this._product.name, () => this.getIdentity(), this._logger); - } - static get VERSION() { - return package_json_1.version; - } - static get DEFAULT_REFRESH_RETRY_PERIOD_MS() { - return 5000; - } - init(opts) { - this.initInternal(opts); - } - getAdvertisingToken() { - var _a, _b; - return (_b = (_a = this.getIdentity()) === null || _a === void 0 ? void 0 : _a.advertising_token) !== null && _b !== void 0 ? _b : undefined; - } - setIdentityFromEmail(email, opts) { - return __awaiter(this, void 0, void 0, function* () { - this._logger.log('Sending request', email); - this.throwIfInitNotComplete('Cannot set identity before calling init.'); - (0, clientSideIdentityOptions_1.isClientSideIdentityOptionsOrThrow)(opts); - const normalizedEmail = (0, diiNormalization_1.normalizeEmail)(email); - if (normalizedEmail === undefined) { - throw new Error('Invalid email address'); - } - const emailHash = yield (0, hash_1.hashAndEncodeIdentifier)(email); - yield this.callCstgAndSetIdentity({ emailHash: emailHash }, opts); - }); - } - setIdentityFromEmailHash(emailHash, opts) { - return __awaiter(this, void 0, void 0, function* () { - this.throwIfInitNotComplete('Cannot set identity before calling init.'); - (0, clientSideIdentityOptions_1.isClientSideIdentityOptionsOrThrow)(opts); - if (!(0, hashedDii_1.isBase64Hash)(emailHash)) { - throw new Error('Invalid hash'); - } - yield this.callCstgAndSetIdentity({ emailHash: emailHash }, opts); - }); - } - setIdentity(identity) { - if (this._apiClient) - this._apiClient.abortActiveRequests(); - const validatedIdentity = this.validateAndSetIdentity(identity); - if (validatedIdentity) { - if ((0, Identity_1.isOptoutIdentity)(validatedIdentity)) { - this._callbackManager.runCallbacks(callbackManager_1.EventType.OptoutReceived, {}); - } - else { - this.triggerRefreshOrSetTimer(validatedIdentity); - } - this._callbackManager.runCallbacks(callbackManager_1.EventType.IdentityUpdated, {}); - } - } - getIdentity() { - return this._identity && !this.temporarilyUnavailable() && !(0, Identity_1.isOptoutIdentity)(this._identity) - ? this._identity - : null; - } - // When the SDK has been initialized, this function should return the token - // from the most recent refresh request, if there is a request, wait for the - // new token. Otherwise, returns a promise which will be resolved after init. - getAdvertisingTokenAsync() { - const token = this.getAdvertisingToken(); - return this._tokenPromiseHandler.createMaybeDeferredPromise(token !== null && token !== void 0 ? token : null); - } - /** - * Deprecated - */ - isLoginRequired() { - return this.hasIdentity(); - } - hasIdentity() { - var _a; - if (!this._initComplete) - return undefined; - return !(this.isLoggedIn() || ((_a = this._apiClient) === null || _a === void 0 ? void 0 : _a.hasActiveRequests())); - } - hasOptedOut() { - if (!this._initComplete) - return undefined; - return (0, Identity_1.isOptoutIdentity)(this._identity); - } - disconnect() { - this.abort(`${this._product.name} SDK disconnected.`); - // Note: This silently fails to clear the cookie if init hasn't been called and a cookieDomain is used! - if (this._storageManager) - this._storageManager.removeValues(); - else - new storageManager_1.StorageManager({}, this._product.cookieName, this._product.localStorageKey).removeValues(); - this._identity = undefined; - this._callbackManager.runCallbacks(callbackManager_1.EventType.IdentityUpdated, { - identity: null, - }); - } - // Note: This doesn't invoke callbacks. It's a hard, silent reset. - abort(reason) { - this._initComplete = true; - this._tokenPromiseHandler.rejectAllPromises(reason !== null && reason !== void 0 ? reason : new Error(`${this._product.name} SDK aborted.`)); - if (this._refreshTimerId) { - clearTimeout(this._refreshTimerId); - this._refreshTimerId = null; - } - if (this._apiClient) - this._apiClient.abortActiveRequests(); - } - initInternal(opts) { - var _a; - if (this._initComplete) { - throw new TypeError('Calling init() more than once is not allowed'); - } - if (!(0, sdkOptions_1.isSDKOptionsOrThrow)(opts)) - throw new TypeError(`Options provided to ${this._product.name} init couldn't be validated.`); - this._opts = opts; - this._storageManager = new storageManager_1.StorageManager(Object.assign({}, opts), this._product.cookieName, this._product.localStorageKey); - this._apiClient = new apiClient_1.ApiClient(opts, this._product.defaultBaseUrl, this._product.name); - this._tokenPromiseHandler.registerApiClient(this._apiClient); - let identity; - if (this._opts.identity) { - identity = this._opts.identity; - } - else { - identity = this._storageManager.loadIdentityWithFallback(); - } - const validatedIdentity = this.validateAndSetIdentity(identity); - if (validatedIdentity && !(0, Identity_1.isOptoutIdentity)(validatedIdentity)) - this.triggerRefreshOrSetTimer(validatedIdentity); - this._initComplete = true; - (_a = this._callbackManager) === null || _a === void 0 ? void 0 : _a.runCallbacks(callbackManager_1.EventType.InitCompleted, {}); - if (this.hasOptedOut()) - this._callbackManager.runCallbacks(callbackManager_1.EventType.OptoutReceived, {}); - } - isLoggedIn() { - return this._identity && !hasExpired(this._identity.refresh_expires); - } - temporarilyUnavailable() { - var _a; - if (!this._identity && ((_a = this._apiClient) === null || _a === void 0 ? void 0 : _a.hasActiveRequests())) - return true; - if (this._identity && - hasExpired(this._identity.identity_expires) && - !hasExpired(this._identity.refresh_expires)) - return true; - return false; - } - getIdentityStatus(identity) { - if (!identity) { - return { - valid: false, - errorMessage: 'Identity not available', - status: initCallbacks_1.IdentityStatus.NO_IDENTITY, - identity: null, - }; - } - if ((0, Identity_1.isOptoutIdentity)(identity)) { - return { - valid: false, - errorMessage: 'User has opted out', - status: initCallbacks_1.IdentityStatus.OPTOUT, - identity: identity, - }; - } - if (!identity.advertising_token) { - return { - valid: false, - errorMessage: 'advertising_token is not available or is not valid', - status: initCallbacks_1.IdentityStatus.INVALID, - identity: null, - }; - } - if (!identity.refresh_token) { - return { - valid: false, - errorMessage: 'refresh_token is not available or is not valid', - status: initCallbacks_1.IdentityStatus.INVALID, - identity: null, - }; - } - if (hasExpired(identity.refresh_expires, Date.now())) { - return { - valid: false, - errorMessage: 'Identity expired, refresh expired', - status: initCallbacks_1.IdentityStatus.REFRESH_EXPIRED, - identity: null, - }; - } - if (hasExpired(identity.identity_expires, Date.now())) { - return { - valid: true, - errorMessage: 'Identity expired, refresh still valid', - status: initCallbacks_1.IdentityStatus.EXPIRED, - identity, - }; - } - if (typeof this._identity === 'undefined') - return { - valid: true, - identity, - status: initCallbacks_1.IdentityStatus.ESTABLISHED, - errorMessage: 'Identity established', - }; - return { - valid: true, - identity, - status: initCallbacks_1.IdentityStatus.REFRESHED, - errorMessage: 'Identity refreshed', - }; - } - validateAndSetIdentity(identity, status, statusText) { - var _a, _b; - if (!this._storageManager) - throw new Error('Cannot set identity before calling init.'); - const validity = this.getIdentityStatus(identity); - if (validity.valid && - validity.identity && - !(0, Identity_1.isOptoutIdentity)(this._identity) && - ((_a = validity.identity) === null || _a === void 0 ? void 0 : _a.advertising_token) === ((_b = this._identity) === null || _b === void 0 ? void 0 : _b.advertising_token)) - return validity.identity; - this._identity = validity.identity; - if (validity.valid && validity.identity) { - this._storageManager.setIdentity(validity.identity); - } - else if (validity.status === initCallbacks_1.IdentityStatus.OPTOUT || status === initCallbacks_1.IdentityStatus.OPTOUT) { - this._storageManager.setOptout(); - } - else { - this.abort(); - this._storageManager.removeValues(); - } - (0, initCallbacks_1.notifyInitCallback)(this._opts, status !== null && status !== void 0 ? status : validity.status, statusText !== null && statusText !== void 0 ? statusText : validity.errorMessage, this.getAdvertisingToken(), this._logger); - return validity.identity; - } - triggerRefreshOrSetTimer(validIdentity) { - if (hasExpired(validIdentity.refresh_from, Date.now())) { - this.refreshToken(validIdentity); - } - else { - this.setRefreshTimer(); - } - } - setRefreshTimer() { - var _a, _b; - const timeout = (_b = (_a = this._opts) === null || _a === void 0 ? void 0 : _a.refreshRetryPeriod) !== null && _b !== void 0 ? _b : SdkBase.DEFAULT_REFRESH_RETRY_PERIOD_MS; - if (this._refreshTimerId) { - clearTimeout(this._refreshTimerId); - } - this._refreshTimerId = setTimeout(() => { - var _a, _b; - if (this.isLoginRequired()) - return; - const validatedIdentity = this.validateAndSetIdentity((_b = (_a = this._storageManager) === null || _a === void 0 ? void 0 : _a.loadIdentity()) !== null && _b !== void 0 ? _b : null); - if (validatedIdentity && !(0, Identity_1.isOptoutIdentity)(validatedIdentity)) - this.triggerRefreshOrSetTimer(validatedIdentity); - this._refreshTimerId = null; - }, timeout); - } - refreshToken(identity) { - const apiClient = this._apiClient; - if (!apiClient) - throw new Error('Cannot refresh the token before calling init.'); - apiClient - .callRefreshApi(identity) - .then((response) => { - switch (response.status) { - case 'success': - this.validateAndSetIdentity(response.identity, initCallbacks_1.IdentityStatus.REFRESHED, 'Identity refreshed'); - this.setRefreshTimer(); - break; - case 'optout': - this.validateAndSetIdentity(null, initCallbacks_1.IdentityStatus.OPTOUT, 'User opted out'); - this._callbackManager.runCallbacks(callbackManager_1.EventType.OptoutReceived, {}); - break; - case 'expired_token': - this.validateAndSetIdentity(null, initCallbacks_1.IdentityStatus.REFRESH_EXPIRED, 'Refresh token expired'); - break; - } - }, (reason) => { - this._logger.warn(`Encountered an error refreshing the token`, reason); - this.validateAndSetIdentity(identity); - if (!hasExpired(identity.refresh_expires, Date.now())) - this.setRefreshTimer(); - }) - .then(() => { - this._callbackManager.runCallbacks(callbackManager_1.EventType.IdentityUpdated, {}); - }, (reason) => this._logger.warn(`Callbacks on identity event failed.`, reason)); - } - callCstgAndSetIdentity(request, opts) { - return __awaiter(this, void 0, void 0, function* () { - const cstgResult = yield this._apiClient.callCstgApi(request, opts); - if (cstgResult.status == 'success') { - this.setIdentity(cstgResult.identity); - } - else if (cstgResult.status === 'optout') { - this.validateAndSetIdentity(null, initCallbacks_1.IdentityStatus.OPTOUT); - this._callbackManager.runCallbacks(callbackManager_1.EventType.OptoutReceived, {}); - this._callbackManager.runCallbacks(callbackManager_1.EventType.IdentityUpdated, {}); - } - else { - const errorText = 'Unexpected status received from CSTG endpoint.'; - this._logger.warn(errorText); - throw new Error(errorText); - } - }); - } - throwIfInitNotComplete(message) { - if (!this._initComplete) { - throw new Error(message); - } - } -} -exports.SdkBase = SdkBase; -SdkBase.IdentityStatus = initCallbacks_1.IdentityStatus; -SdkBase.EventType = callbackManager_1.EventType; - - -/***/ }), - -/***/ 512: -/***/ ((__unused_webpack_module, exports) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.isSDKOptionsOrThrow = void 0; -function isSDKOptionsOrThrow(maybeOpts) { - if (typeof maybeOpts !== 'object' || maybeOpts === null) { - throw new TypeError('opts must be an object'); - } - const opts = maybeOpts; - if (opts.callback !== undefined && typeof opts.callback !== 'function') { - throw new TypeError('opts.callback, if provided, must be a function'); - } - if (typeof opts.refreshRetryPeriod !== 'undefined') { - if (typeof opts.refreshRetryPeriod !== 'number') - throw new TypeError('opts.refreshRetryPeriod must be a number'); - else if (opts.refreshRetryPeriod < 1000) - throw new RangeError('opts.refreshRetryPeriod must be >= 1000'); - } - return true; -} -exports.isSDKOptionsOrThrow = isSDKOptionsOrThrow; - - -/***/ }), - -/***/ 980: -/***/ ((__unused_webpack_module, exports) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.MakeLogger = void 0; -function nop(...data) { } -function MakeSafeLoggerFunction(fn) { - if (typeof fn === 'function') - return fn; - return nop; -} -function MakeAnnotatedLoggerFunction(fn, annotation) { - return (...data) => { - if (typeof data[0] === 'string') - fn(`[${annotation}] ${data[0]}`, ...data.slice(1)); - else - fn(`[${annotation}]`, ...data); - }; -} -function MakeLoggerFunction(fn, annotation) { - const safeFunction = MakeSafeLoggerFunction(fn); - if (annotation) - return MakeAnnotatedLoggerFunction(safeFunction, annotation); - else - return safeFunction; -} -function MakeLogger(logger, annotation) { - return { - debug: MakeLoggerFunction(logger.debug, annotation), - error: MakeLoggerFunction(logger.error, annotation), - info: MakeLoggerFunction(logger.info, annotation), - log: MakeLoggerFunction(logger.log, annotation), - trace: MakeLoggerFunction(logger.trace, annotation), - warn: MakeLoggerFunction(logger.warn, annotation), - }; -} -exports.MakeLogger = MakeLogger; - - -/***/ }), - -/***/ 505: -/***/ ((__unused_webpack_module, exports, __webpack_require__) => { - - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.StorageManager = void 0; -const cookieManager_1 = __webpack_require__(852); -const localStorageManager_1 = __webpack_require__(669); -class StorageManager { - constructor(opts, cookieName, localStorageKey) { - this._opts = opts; - this._cookieManager = new cookieManager_1.CookieManager(Object.assign({}, opts), cookieName); - this._localStorageManager = new localStorageManager_1.LocalStorageManager(localStorageKey); - } - loadIdentityWithFallback() { - const localStorageIdentity = this._localStorageManager.loadIdentityFromLocalStorage(); - const cookieIdentity = this._cookieManager.loadIdentityFromCookie(); - const shouldUseCookie = cookieIdentity && - (!localStorageIdentity || - cookieIdentity.identity_expires > localStorageIdentity.identity_expires); - return shouldUseCookie ? cookieIdentity : localStorageIdentity; - } - loadIdentity() { - return this._opts.useCookie - ? this._cookieManager.loadIdentityFromCookie() - : this._localStorageManager.loadIdentityFromLocalStorage(); - } - setIdentity(identity) { - this.setValue(identity); - } - setOptout() { - const expiry = Date.now() + 72 * 60 * 60 * 1000; // 3 days - need to pick something - const optout = { - refresh_expires: expiry, - identity_expires: expiry, - status: 'optout', - }; - this.setValue(optout); - } - setValue(value) { - if (this._opts.useCookie) { - this._cookieManager.setCookie(value); - return; - } - this._localStorageManager.setValue(value); - if (this._opts.useCookie === false && - this._localStorageManager.loadIdentityFromLocalStorage()) { - this._cookieManager.removeCookie(); - } - } - removeValues() { - this._cookieManager.removeCookie(); - this._localStorageManager.removeValue(); - } -} -exports.StorageManager = StorageManager; - - -/***/ }), - -/***/ 890: -/***/ (function(__unused_webpack_module, exports, __webpack_require__) { - - -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.sdkWindow = exports.__uid2InternalHandleScriptLoad = exports.UID2 = exports.UID2Helper = void 0; -const callbackManager_1 = __webpack_require__(230); -const clientSideIdentityOptions_1 = __webpack_require__(522); -const diiNormalization_1 = __webpack_require__(838); -const hashedDii_1 = __webpack_require__(254); -const hash_1 = __webpack_require__(699); -const sdkBase_1 = __webpack_require__(533); -__exportStar(__webpack_require__(479), exports); -class UID2Helper { - normalizeEmail(email) { - return (0, diiNormalization_1.normalizeEmail)(email); - } - hashIdentifier(normalizedEmail) { - return (0, hash_1.hashIdentifier)(normalizedEmail); - } - hashAndEncodeIdentifier(normalizedEmail) { - return __awaiter(this, void 0, void 0, function* () { - return yield (0, hash_1.hashAndEncodeIdentifier)(normalizedEmail); - }); - } - isNormalizedPhone(phone) { - return (0, diiNormalization_1.isNormalizedPhone)(phone); - } -} -exports.UID2Helper = UID2Helper; -class UID2 extends sdkBase_1.SdkBase { - constructor(existingCallbacks = undefined, callbackContainer = {}) { - super(existingCallbacks, UID2.Uid2Details); - const runCallbacks = () => { - this._callbackManager.runCallbacks(callbackManager_1.EventType.SdkLoaded, {}); - }; - if (window.__uid2 instanceof UID2) { - runCallbacks(); - } - else { - // Need to defer running callbacks until this is assigned to the window global - callbackContainer.callback = runCallbacks; - } - } - // Deprecated. Integrators should never access the cookie directly! - static get COOKIE_NAME() { - console.warn('Detected access to UID2.COOKIE_NAME. This is deprecated and will be removed in the future. Integrators should not access the cookie directly.'); - return UID2.cookieName; - } - static get Uid2Details() { - return { - name: 'UID2', - defaultBaseUrl: 'https://prod.uidapi.com', - localStorageKey: 'UID2-sdk-identity', - cookieName: UID2.cookieName, - }; - } - static setupGoogleTag() { - UID2.setupGoogleSecureSignals(); - } - static setupGoogleSecureSignals() { - if (window.__uid2SecureSignalProvider) - window.__uid2SecureSignalProvider.registerSecureSignalProvider(); - } - setIdentityFromPhone(phone, opts) { - return __awaiter(this, void 0, void 0, function* () { - this.throwIfInitNotComplete('Cannot set identity before calling init.'); - (0, clientSideIdentityOptions_1.isClientSideIdentityOptionsOrThrow)(opts); - if (!(0, diiNormalization_1.isNormalizedPhone)(phone)) { - throw new Error('Invalid phone number'); - } - const phoneHash = yield (0, hash_1.hashAndEncodeIdentifier)(phone); - yield this.callCstgAndSetIdentity({ phoneHash: phoneHash }, opts); - }); - } - setIdentityFromPhoneHash(phoneHash, opts) { - return __awaiter(this, void 0, void 0, function* () { - this.throwIfInitNotComplete('Cannot set identity before calling init.'); - (0, clientSideIdentityOptions_1.isClientSideIdentityOptionsOrThrow)(opts); - if (!(0, hashedDii_1.isBase64Hash)(phoneHash)) { - throw new Error('Invalid hash'); - } - yield this.callCstgAndSetIdentity({ phoneHash: phoneHash }, opts); - }); - } -} -exports.UID2 = UID2; -UID2.cookieName = '__uid_2'; -function __uid2InternalHandleScriptLoad() { - var _a; - const callbacks_uid2 = ((_a = window === null || window === void 0 ? void 0 : window.__uid2) === null || _a === void 0 ? void 0 : _a.callbacks) || []; - const callbackContainer = {}; - window.__uid2 = new UID2(callbacks_uid2, callbackContainer); - window.__uid2Helper = new UID2Helper(); - if (callbackContainer.callback) - callbackContainer.callback(); -} -exports.__uid2InternalHandleScriptLoad = __uid2InternalHandleScriptLoad; -__uid2InternalHandleScriptLoad(); -exports.sdkWindow = globalThis.window; - - -/***/ }), - -/***/ 147: -/***/ ((module) => { - -module.exports = JSON.parse('{"name":"@uid2/uid2-sdk","version":"3.3.0","description":"UID2 Client SDK","main":"lib/src/uid2Sdk.js","types":"lib/src/uid2Sdk.d.ts","files":["/lib"],"author":"The Trade Desk","license":"Apache 2.0","wallaby":{"delays":{"run":1000}},"scripts":{"lint":"eslint -c .eslintrc.js . ../static/js/uid2-sdk-2.0.0.js ../static/js/uid2-sdk-1.0.0.js","test":"jest","build":"webpack","build-with-sourcemaps":"webpack --mode=production --env prodSourceMaps=true","build-package":"tsc","watch":"webpack watch --mode=development","webpack-dev-server":"webpack-dev-server --config webpack-dev-server.config.js --hot --port 9091","uid2-examples":"webpack --mode=development --env outputToExamples=true","build:esp":"webpack --env espOnly=true"},"engines":{"node":">=18"},"jest":{"preset":"ts-jest","testEnvironment":"jsdom","setupFilesAfterEnv":["./setupJest.js"],"testPathIgnorePatterns":["/node_modules/","/dist/"]},"devDependencies":{"@jest/globals":"^29.2.2","@types/jest":"^29.2.0","@types/node":"^18.11.3","@typescript-eslint/eslint-plugin":"^5.40.1","@typescript-eslint/parser":"^5.40.1","eslint":"^8.25.0","eslint-config-airbnb-typescript":"^17.0.0","eslint-plugin-import":"^2.26.0","eslint-plugin-promise":"^6.1.1","eslint-plugin-simple-import-sort":"^8.0.0","eslint-plugin-testing-library":"^5.9.0","jest":"^29.2.1","jest-environment-jsdom":"^29.2.1","jsdom":"^20.0.1","ts-jest":"^29.0.3","ts-loader":"^9.4.1","typescript":"^4.8.4","webpack":"^5.74.0","webpack-cli":"^4.10.0","webpack-dev-server":"^4.15.1"}}'); - -/***/ }) - -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/************************************************************************/ -/******/ -/******/ // startup -/******/ // Load entry module and return exports -/******/ // This entry module is referenced by other modules so it can't be inlined -/******/ var __webpack_exports__ = __webpack_require__(890); -/******/ -/******/ })() -; \ No newline at end of file diff --git a/tools/reverse-proxy/README.md b/tools/reverse-proxy/README.md index 77e80535..6a5df13e 100644 --- a/tools/reverse-proxy/README.md +++ b/tools/reverse-proxy/README.md @@ -80,6 +80,7 @@ Add these entries: 127.0.0.1 prebid-client-server.sample-dev.com 127.0.0.1 prebid-deferred.sample-dev.com 127.0.0.1 prebid-secure-signals.sample-dev.com +127.0.0.1 hashing-tool.sample-dev.com ``` Flush DNS cache after saving: @@ -119,6 +120,7 @@ Go to **https://sample-dev.com** — this index page has clickable links to all | `https://prebid-client-server.sample-dev.com` | Prebid Client Server | 3052 | | `https://prebid-deferred.sample-dev.com` | Prebid Client Side Deferred | 3053 | | `https://prebid-secure-signals.sample-dev.com` | Prebid Secure Signals | 3061 | +| `https://hashing-tool.sample-dev.com` | Hashing Tool | 3071 | --- @@ -155,6 +157,7 @@ You can skip all certificate setup and access services directly via localhost: | `http://localhost:3051` | Prebid Client Side | | `http://localhost:3052` | Prebid Client Server | | `http://localhost:3053` | Prebid Client Side Deferred | +| `http://localhost:3071` | Hashing Tool | | `http://localhost:3031` | JavaScript SDK Client Side | | *(etc.)* | diff --git a/tools/reverse-proxy/default.conf.template b/tools/reverse-proxy/default.conf.template index 8d1831a1..b5e35d1c 100644 --- a/tools/reverse-proxy/default.conf.template +++ b/tools/reverse-proxy/default.conf.template @@ -20,7 +20,7 @@ server { location / { add_header Content-Type text/html; # Use protocol-relative URLs (//) so they work with both http and https - return 200 "UID2 Sample Pages

UID2 Sample Pages

Access services using the following subdomains:

js-client-side.${DOMAIN}js-client-server.${DOMAIN}js-react.${DOMAIN}server-side.${DOMAIN}secure-signals-client-server.${DOMAIN}secure-signals-client-side.${DOMAIN}secure-signals-server-side.${DOMAIN}secure-signals-react.${DOMAIN}prebid-client.${DOMAIN}prebid-client-server.${DOMAIN}prebid-deferred.${DOMAIN}prebid-secure-signals.${DOMAIN}
Note: For local development, add ${DOMAIN} and all subdomains to your hosts file (127.0.0.1) to use them. Example: 127.0.0.1 ${DOMAIN} js-client-side.${DOMAIN} ...
"; + return 200 "UID2 Sample Pages

UID2 Sample Pages

Access services using the following subdomains:

js-client-side.${DOMAIN}js-client-server.${DOMAIN}js-react.${DOMAIN}server-side.${DOMAIN}secure-signals-client-server.${DOMAIN}secure-signals-client-side.${DOMAIN}secure-signals-server-side.${DOMAIN}secure-signals-react.${DOMAIN}prebid-client.${DOMAIN}prebid-client-server.${DOMAIN}prebid-deferred.${DOMAIN}prebid-secure-signals.${DOMAIN}hashing-tool.${DOMAIN}
Note: For local development, add ${DOMAIN} and all subdomains to your hosts file (127.0.0.1) to use them. Example: 127.0.0.1 ${DOMAIN} js-client-side.${DOMAIN} ...
"; } } @@ -345,3 +345,27 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } } + +# Hashing Tool (port 3071) +server { + listen 80; + listen 443 ssl; + server_name hashing-tool.${DOMAIN} *.hashing-tool.${DOMAIN}; + + ssl_certificate /etc/nginx/certs/cert.crt; + ssl_certificate_key /etc/nginx/certs/cert.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + proxy_pass http://${HASHING_TOOL_BACKEND}:3071; + proxy_redirect off; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/tools/reverse-proxy/docker-entrypoint.sh b/tools/reverse-proxy/docker-entrypoint.sh index bb31cb0c..ecc1522a 100644 --- a/tools/reverse-proxy/docker-entrypoint.sh +++ b/tools/reverse-proxy/docker-entrypoint.sh @@ -38,6 +38,7 @@ if [ "${BACKEND_HOST+set}" = "set" ] && [ -z "$BACKEND_HOST" ]; then PREBID_CLIENT_SERVER_BACKEND=${PREBID_CLIENT_SERVER_BACKEND:-prebid-client-server} PREBID_CLIENT_SIDE_DEFERRED_BACKEND=${PREBID_CLIENT_SIDE_DEFERRED_BACKEND:-prebid-client-side-deferred} PREBID_SECURE_SIGNALS_CLIENT_SIDE_BACKEND=${PREBID_SECURE_SIGNALS_CLIENT_SIDE_BACKEND:-prebid-secure-signals-client-side} + HASHING_TOOL_BACKEND=${HASHING_TOOL_BACKEND:-hashing-tool} else # BACKEND_HOST is unset (defaults to localhost) or set to a value - use it for all services BACKEND_HOST_VALUE=${BACKEND_HOST:-localhost} @@ -53,6 +54,7 @@ else PREBID_CLIENT_SERVER_BACKEND=${PREBID_CLIENT_SERVER_BACKEND:-$BACKEND_HOST_VALUE} PREBID_CLIENT_SIDE_DEFERRED_BACKEND=${PREBID_CLIENT_SIDE_DEFERRED_BACKEND:-$BACKEND_HOST_VALUE} PREBID_SECURE_SIGNALS_CLIENT_SIDE_BACKEND=${PREBID_SECURE_SIGNALS_CLIENT_SIDE_BACKEND:-$BACKEND_HOST_VALUE} + HASHING_TOOL_BACKEND=${HASHING_TOOL_BACKEND:-$BACKEND_HOST_VALUE} fi # Export all variables for envsubst @@ -69,9 +71,10 @@ export PREBID_CLIENT_BACKEND export PREBID_CLIENT_SERVER_BACKEND export PREBID_CLIENT_SIDE_DEFERRED_BACKEND export PREBID_SECURE_SIGNALS_CLIENT_SIDE_BACKEND +export HASHING_TOOL_BACKEND # Substitute environment variables in the template -envsubst '${DOMAIN} ${JS_CLIENT_SIDE_BACKEND} ${JS_CLIENT_SERVER_BACKEND} ${JS_REACT_CLIENT_SIDE_BACKEND} ${SERVER_SIDE_BACKEND} ${SECURE_SIGNALS_CLIENT_SERVER_BACKEND} ${SECURE_SIGNALS_CLIENT_SIDE_BACKEND} ${SECURE_SIGNALS_SERVER_SIDE_BACKEND} ${SECURE_SIGNALS_REACT_CLIENT_SIDE_BACKEND} ${PREBID_CLIENT_BACKEND} ${PREBID_CLIENT_SERVER_BACKEND} ${PREBID_CLIENT_SIDE_DEFERRED_BACKEND} ${PREBID_SECURE_SIGNALS_CLIENT_SIDE_BACKEND}' < /etc/nginx/templates/default.conf.template > /etc/nginx/conf.d/default.conf +envsubst '${DOMAIN} ${JS_CLIENT_SIDE_BACKEND} ${JS_CLIENT_SERVER_BACKEND} ${JS_REACT_CLIENT_SIDE_BACKEND} ${SERVER_SIDE_BACKEND} ${SECURE_SIGNALS_CLIENT_SERVER_BACKEND} ${SECURE_SIGNALS_CLIENT_SIDE_BACKEND} ${SECURE_SIGNALS_SERVER_SIDE_BACKEND} ${SECURE_SIGNALS_REACT_CLIENT_SIDE_BACKEND} ${PREBID_CLIENT_BACKEND} ${PREBID_CLIENT_SERVER_BACKEND} ${PREBID_CLIENT_SIDE_DEFERRED_BACKEND} ${PREBID_SECURE_SIGNALS_CLIENT_SIDE_BACKEND} ${HASHING_TOOL_BACKEND}' < /etc/nginx/templates/default.conf.template > /etc/nginx/conf.d/default.conf # Test nginx configuration nginx -t From d392f3510f90bdb3fe69ba8500c80fd9d071aa2d Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 15 Dec 2025 15:25:22 -0700 Subject: [PATCH 2/6] update styling for hashing tool --- tools/hashing-tool/app.css | 476 ++++++++++++++++++++++++++++------ tools/hashing-tool/index.html | 232 ++++++++++++----- 2 files changed, 562 insertions(+), 146 deletions(-) diff --git a/tools/hashing-tool/app.css b/tools/hashing-tool/app.css index 3edd6fb4..3bd30669 100644 --- a/tools/hashing-tool/app.css +++ b/tools/hashing-tool/app.css @@ -1,26 +1,276 @@ +/* Color Variables */ +:root { + /* Brand Colors */ + --primary-orange: #FF6B35; + --primary-dark: #2D3748; + --accent-teal: #0D9488; + --accent-yellow: #FBBF24; + + /* Text Colors */ + --text-dark: #1A202C; + --text-gray: #718096; + + /* Background Colors */ + --bg-white: #FFFFFF; + --bg-light: #F7FAFC; + --sidebar-bg: #FFF7ED; + + /* Border Colors */ + --border-color: #E2E8F0; + + /* Button Colors */ + --button-navy: rgba(2, 10, 64, 1); + --button-navy-hover: rgba(2, 10, 64, 0.9); + + /* Link Colors */ + --link-color: #06B6D4; + --link-hover: #06B6D4; + + /* Alert Colors */ + --alert-red: #f44336; + --alert-red-bg: rgba(244, 67, 54, 0.1); + + /* Tooltip Colors */ + --tooltip-bg: #1F2937; + --tooltip-trigger: #3B82F6; + --tooltip-trigger-hover: #2563EB; + + /* Shadows */ + --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + body { - padding: 50px; - font: - 14px 'Lucida Grande', - Helvetica, - Arial, - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif; + background: var(--bg-light); + color: var(--text-dark); + line-height: 1.6; + padding: 2rem; +} + +/* Two Column Layout */ +.page-wrapper { + display: flex; + gap: 2rem; + max-width: 1400px; + margin: 0 auto; +} + +/* Main Content Area (75%) */ +.main-content { + flex: 3; + background: var(--bg-white); + border-radius: 12px; + padding: 2.5rem; + box-shadow: var(--shadow-md); +} + +/* Sidebar (25%) */ +.sidebar { + flex: 1; + background: var(--sidebar-bg); + border-radius: 12px; + padding: 2rem; + box-shadow: var(--shadow); + border-left: 4px solid var(--primary-orange); + position: sticky; + top: 2rem; + height: fit-content; + max-height: calc(100vh - 4rem); + overflow-y: auto; +} + +.sidebar h3 { + color: var(--primary-dark); + font-size: 1.1rem; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--primary-orange); +} + +.sidebar .section { + margin-bottom: 1.5rem; +} + +.sidebar .section h4 { + color: var(--accent-teal); + font-size: 0.9rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.sidebar ul { + margin-left: 1.25rem; + font-size: 0.875rem; + color: var(--text-gray); +} + +.sidebar li { + margin-bottom: 0.5rem; + line-height: 1.6; +} + +.sidebar .note { + background: rgba(255, 107, 53, 0.1); + padding: 0.75rem; + border-radius: 6px; + font-size: 0.85rem; + color: var(--text-dark); + margin-top: 1rem; +} + +.sidebar .note strong { + color: var(--primary-orange); +} + +/* Header */ +h1 { + font-size: 2rem; + font-weight: 800; + color: var(--primary-dark); + margin-bottom: 0.75rem; + line-height: 1.3; +} + +h2 { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-dark); + margin: 1.5rem 0 1rem 0; + padding-bottom: 0.5rem; + border-bottom: 3px solid var(--primary-orange); +} + +.intro { + font-size: 0.95rem; + color: var(--text-dark); + margin-bottom: 1.5rem; + line-height: 1.8; } a { - color: #00b7ff; + color: var(--link-color); + text-decoration: underline; + font-weight: 500; + transition: opacity 0.2s ease; } -.alert { - padding: 20px; - background-color: #f44336; +a:hover { + opacity: 0.8; +} + +/* Forms */ +.form { + margin-top: 2rem; +} + +.form.top-form { + margin-top: 0; + margin-bottom: 1.5rem; +} + +/* Input Type Toggle (Email/Phone) */ +.input-type-toggle { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.input-type-toggle input[type="radio"] { + width: 16px; + height: 16px; + accent-color: var(--button-navy); + margin: 0; +} + +.input-type-toggle label { + margin-right: 1.5rem; + font-weight: 500; + color: var(--text-dark); + cursor: pointer; +} + +.email_prompt { + display: flex; + gap: 0; + max-width: 600px; + box-shadow: var(--shadow); + border-radius: 8px; + overflow: hidden; +} + +#input_value { + flex: 1; + padding: 0.875rem 1.25rem; + border: 2px solid var(--border-color); + border-right: none; + font-size: 0.95rem; + color: var(--text-dark); + outline: none; + transition: border-color 0.2s ease; +} + +#input_value:focus { + border-color: var(--primary-orange); +} + +#input_value::placeholder { + color: var(--text-gray); +} + +/* Buttons */ +.button { + padding: 0.875rem 2rem; + background: var(--button-navy); color: white; - width: 400px; + border: none; + font-size: 0.95rem; + font-weight: 700; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + box-shadow: var(--shadow); +} + +.button:hover { + background: var(--button-navy-hover); + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(2, 10, 64, 0.3); +} + +.button:active { + transform: translateY(0); +} + +/* Clear button (full width like other sample sites) */ +#clear_form .button { + width: 100%; + max-width: 600px; + border-radius: 8px; +} + +/* Alert Messages */ +.alert { + padding: 1rem 1.25rem; + background-color: var(--alert-red-bg); + border-left: 4px solid var(--alert-red); + color: var(--alert-red); + border-radius: 6px; + margin: 1rem 0; + max-width: 600px; + display: none; } .closebtn { margin-left: 15px; - color: white; + color: var(--alert-red); font-weight: bold; float: right; font-size: 22px; @@ -30,81 +280,159 @@ a { } .closebtn:hover { - color: black; + opacity: 0.7; } -.button { - border-style: none; - cursor: pointer; - align-items: center; - height: 40px; - width: 401px; - text-align: center; - position: absolute; - letter-spacing: 0.28px; - box-sizing: border-box; - color: white; - font-family: 'Raleway', Helvetica, Arial, serif; - font-size: 14px; - font-style: normal; - font-weight: 700; - text-transform: none; - text-indent: 0; - text-shadow: none; - margin: 0; - padding: 1px 6px; - background-color: rgba(2, 10, 64); - border-image: initial; +/* Results Table */ +#results_table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; + font-size: 0.875rem; + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: visible; } -.form { - margin-top: 40px; - +#results_table tr { + border-bottom: 1px solid var(--border-color); } -.prompt { - align-items: center; - align-self: center; - background-color: white; - border: 1px solid rgba(2, 10, 64); - border-radius: 2px; - box-sizing: border-box; +#results_table tr:nth-child(even) { + background-color: var(--bg-light); +} + +#results_table tr:last-child { + border-bottom: none; +} + +#results_table td { + padding: 1rem; + vertical-align: top; +} + +#results_table .label { + font-weight: 600; + color: var(--text-dark); + white-space: nowrap; + padding-right: 2rem; + width: 15em; +} + +#results_table .value { + color: var(--text-gray); + font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; +} + +#results_table .value pre { + white-space: pre-wrap; + word-break: break-all; + margin: 0; +} + +/* Tooltip Styles */ +.tooltip-wrapper { display: inline-flex; - flex-direction: row; - flex-shrink: 0; - height: 40px; - justify-content: flex-start; - margin-right: 1px; - margin-bottom: 20px; - min-width: 399px; - padding: 0 16px; + align-items: center; + gap: 0.5rem; +} + +.tooltip { position: relative; - width: auto; + display: inline-flex; + align-items: center; + cursor: help; } -#input_value { - background-color: white; - border-style: none; +.tooltip-trigger { + width: 16px; + height: 16px; + border-radius: 50%; + background-color: var(--tooltip-trigger); + color: white; + border: none; + font-size: 0.7rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + cursor: help; + transition: all 0.2s ease; flex-shrink: 0; - height: auto; - letter-spacing: 0.12px; - line-height: 16px; - min-height: 16px; - position: relative; - text-align: left; - white-space: nowrap; - width: 351px; - color: rgba(2, 10, 64, 1); - font-family: 'Raleway', Helvetica, Arial, serif; - font-size: 12px; - font-style: normal; - font-weight: 500; - padding: 1px 2px; - outline: none; } -h1 { - padding-bottom: 20px; +.tooltip-trigger:hover { + background-color: var(--tooltip-trigger-hover); + transform: scale(1.05); +} + +.tooltip-content { + visibility: hidden; + opacity: 0; + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + background-color: var(--tooltip-bg); + color: white; + padding: 10px; + border-radius: 4px; + font-size: 0.75rem; + line-height: 1.4; + min-width: 250px; + max-width: 350px; + white-space: normal; + z-index: 10000; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + transition: opacity 0.2s ease, visibility 0.2s ease; + font-weight: 400; + pointer-events: none; +} + +.tooltip-content::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border-width: 6px; + border-style: solid; + border-color: var(--tooltip-bg) transparent transparent transparent; } +.tooltip:hover .tooltip-content { + visibility: visible; + opacity: 1; +} +@media (max-width: 1024px) { + .page-wrapper { + flex-direction: column; + } + + .sidebar { + position: static; + max-height: none; + } + + body { + padding: 1rem; + } + + .main-content { + padding: 1.5rem; + } + + .email_prompt { + flex-direction: column; + } + + #input_value { + border-right: 2px solid var(--border-color); + border-bottom: none; + } + + .button { + width: 100%; + } +} diff --git a/tools/hashing-tool/index.html b/tools/hashing-tool/index.html index 953d2c67..7c79726c 100644 --- a/tools/hashing-tool/index.html +++ b/tools/hashing-tool/index.html @@ -8,10 +8,9 @@ -

${IDENTITY_NAME} Hashing Tool

-

- Use this tool to verify that your own implementation is normalizing and - encoding correctly. Choose Email or Phone Number, then type or paste the - value and click Enter.

- NOTE: Normalize phone numbers before using the tool. - For details and examples, see Normalization and Encoding. -

- - - - -
- -
-
- -
+
+ +
+

${IDENTITY_NAME} Hashing Tool

+

+ Use this tool to verify that your own implementation is normalizing and + encoding correctly. Choose Email or Phone Number, then type or paste the + value and click Enter. For documentation, see + Normalization and Encoding. + [Source Code] +

-
- × - Email format is invalid. -
-
- × - Phone number format is invalid or is not normalized. -
-
-
-
-


-
-
-

Normalized Value:

-

+ +
+
+ + + + +
+ + + +
+ × + Email format is invalid. +
+
+ × + Phone number format is invalid or is not normalized. +
+
+ + + + +

Results

+ + + + + + + + + + + + + + +
+
+ Normalized Value: +
+ ? +
+ The email after normalization: lowercase, trimmed, and for Gmail addresses, periods removed and plus-addressing stripped. +
+
+
+
 
+
+ Hashed Value (SHA-256): +
+ ? +
+ The 64-character hex-encoded SHA-256 hash of the normalized value. This is an intermediate step—use the Base64-encoded value for API calls. +
+
+
+
 
+
+ Base64-encoded Value: +
+ ? +
+ The 44-character Base64-encoded representation of the SHA-256 hash bytes. This is the value to send to ${IDENTITY_NAME} API endpoints. +
+
+
+
 
- -

Hashed Value:

-

- -

Base64-encoded Value:

-

- -
-
- - -
- + + + From 34bc20436ab69cef6535c64acf986d4d19e0ad9b Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 15 Dec 2025 15:36:09 -0700 Subject: [PATCH 3/6] simplify hashed value label --- tools/hashing-tool/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/hashing-tool/index.html b/tools/hashing-tool/index.html index 7c79726c..3a70e6ab 100644 --- a/tools/hashing-tool/index.html +++ b/tools/hashing-tool/index.html @@ -181,7 +181,7 @@

Results

- Hashed Value (SHA-256): + Hashed Value:
?
From 66ec918fb23cafc248fff4835e73601913159b0c Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 15 Dec 2025 15:43:26 -0700 Subject: [PATCH 4/6] update sidebar header --- tools/hashing-tool/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/hashing-tool/index.html b/tools/hashing-tool/index.html index 3a70e6ab..419e2171 100644 --- a/tools/hashing-tool/index.html +++ b/tools/hashing-tool/index.html @@ -223,7 +223,7 @@

Privacy & Security

-

Normalization Matters

+

Normalization is Required

  • Ensures consistent ${IDENTITY_NAME}s across systems
  • Email: Lowercase, trim spaces, Gmail special handling
  • From 7558d8f04843a7ec2827c30d72bab351358078d5 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Tue, 16 Dec 2025 10:18:27 -0700 Subject: [PATCH 5/6] move input buttons to top for js-client-server, react, and server-side --- .../client-server/views/index.html | 48 ++++++++---------- .../react-client-side/src/ClientSideApp.tsx | 49 +++++++++--------- web-integrations/server-side/views/index.html | 50 +++++++++---------- 3 files changed, 71 insertions(+), 76 deletions(-) diff --git a/web-integrations/javascript-sdk/client-server/views/index.html b/web-integrations/javascript-sdk/client-server/views/index.html index 06ed4269..e6c30d88 100644 --- a/web-integrations/javascript-sdk/client-server/views/index.html +++ b/web-integrations/javascript-sdk/client-server/views/index.html @@ -74,6 +74,27 @@
    <%- include('intro.html'); -%> + + + + +

    <%- identityName %> Integration Status

    @@ -148,33 +169,6 @@

    <%- identityName %> Integration Status

    - - - - - -
    diff --git a/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx b/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx index 71681c9b..e71971a9 100644 --- a/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx +++ b/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx @@ -113,6 +113,31 @@ const ClientSideApp = () => { .

    + {/* Generate/Clear buttons at the top for easy access */} + {showLoginForm ? ( +
    +
    + setEmail(e.target.value)} + /> + +
    +
    + ) : ( +
    + +
    + )} +

    {IDENTITY_NAME} Integration Status

    @@ -198,30 +223,6 @@ const ClientSideApp = () => {
    - - {showLoginForm ? ( -
    -
    - setEmail(e.target.value)} - /> - -
    -
    - ) : ( -
    - -
    - )}
From 982c1356fa851af65ccbda169345725cebe0446d Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Tue, 16 Dec 2025 10:23:23 -0700 Subject: [PATCH 6/6] fix CSS styling and add source code link to React sample --- .../client-server/public/stylesheets/app.css | 22 ++++++++++++----- .../react-client-side/src/ClientSideApp.tsx | 2 +- .../react-client-side/src/styles/app.css | 10 ++++++++ .../server-side/public/stylesheets/app.css | 24 +++++++++++++------ 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/web-integrations/javascript-sdk/client-server/public/stylesheets/app.css b/web-integrations/javascript-sdk/client-server/public/stylesheets/app.css index 45f944fc..4daeb56e 100644 --- a/web-integrations/javascript-sdk/client-server/public/stylesheets/app.css +++ b/web-integrations/javascript-sdk/client-server/public/stylesheets/app.css @@ -200,7 +200,12 @@ a:hover { margin-top: 2rem; } -.form-inline { +.form.top-form { + margin-top: 0; + margin-bottom: 2rem; +} + +.email_prompt { display: flex; gap: 0; max-width: 600px; @@ -209,7 +214,7 @@ a:hover { overflow: hidden; } -.email-input { +#email { flex: 1; padding: 0.875rem 1.25rem; border: 2px solid var(--border-color); @@ -220,11 +225,11 @@ a:hover { transition: border-color 0.2s ease; } -.email-input:focus { +#email:focus { border-color: var(--primary-orange); } -.email-input::placeholder { +#email::placeholder { color: var(--text-gray); } @@ -260,6 +265,11 @@ a:hover { border-radius: 8px; } +/* Rounded corners for login button in form */ +#login_form .button { + border-radius: 0 8px 8px 0; +} + /* Tooltip Styles - Matching Self-Serve Portal */ .tooltip-wrapper { display: inline-flex; @@ -353,11 +363,11 @@ a:hover { padding: 1.5rem; } - .form-inline { + .email_prompt { flex-direction: column; } - .email-input { + #email { border-right: 2px solid var(--border-color); border-bottom: none; } diff --git a/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx b/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx index e71971a9..3461fcaa 100644 --- a/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx +++ b/web-integrations/javascript-sdk/react-client-side/src/ClientSideApp.tsx @@ -110,7 +110,7 @@ const ClientSideApp = () => { This example demonstrates how a content publisher can integrate {IDENTITY_NAME} using client-side token generation with React, where the SDK generates tokens directly in the browser using public credentials. For documentation, see the{' '} Client-Side Integration Guide for JavaScript - . + . [Source Code]

{/* Generate/Clear buttons at the top for easy access */} diff --git a/web-integrations/javascript-sdk/react-client-side/src/styles/app.css b/web-integrations/javascript-sdk/react-client-side/src/styles/app.css index 3ef2298b..d24c7d94 100644 --- a/web-integrations/javascript-sdk/react-client-side/src/styles/app.css +++ b/web-integrations/javascript-sdk/react-client-side/src/styles/app.css @@ -213,6 +213,11 @@ a:hover { margin-top: 2rem; } +.form.top-form { + margin-top: 0; + margin-bottom: 2rem; +} + .email_prompt { display: flex; gap: 0; @@ -272,6 +277,11 @@ a:hover { border-radius: 8px; } +/* Rounded corners for login button in form */ +#login_form .button { + border-radius: 0 8px 8px 0; +} + /* Success Message */ .message { background: linear-gradient(135deg, rgba(34, 197, 94, 0.1) 0%, rgba(34, 197, 94, 0.05) 100%); diff --git a/web-integrations/server-side/public/stylesheets/app.css b/web-integrations/server-side/public/stylesheets/app.css index 77cf84ff..a5b9d21c 100644 --- a/web-integrations/server-side/public/stylesheets/app.css +++ b/web-integrations/server-side/public/stylesheets/app.css @@ -207,7 +207,12 @@ a:hover { margin-top: 2rem; } -.form-inline { +.form.top-form { + margin-top: 0; + margin-bottom: 2rem; +} + +.email_prompt { display: flex; gap: 0; max-width: 600px; @@ -216,7 +221,7 @@ a:hover { overflow: hidden; } -.email-input { +#email { flex: 1; padding: 0.875rem 1.25rem; border: 2px solid var(--border-color); @@ -227,11 +232,11 @@ a:hover { transition: border-color 0.2s ease; } -.email-input:focus { +#email:focus { border-color: var(--primary-orange); } -.email-input::placeholder { +#email::placeholder { color: var(--text-gray); } @@ -267,6 +272,11 @@ a:hover { border-radius: 8px; } +/* Rounded corners for login button in form */ +#login_form .button { + border-radius: 0 8px 8px 0; +} + /* Tooltip Styles - Matching Self-Serve Portal */ .tooltip-wrapper { display: inline-flex; @@ -345,7 +355,7 @@ a:hover { @media (max-width: 1024px) { .page-wrapper { flex-direction: column; -} + } .sidebar { position: static; @@ -360,11 +370,11 @@ a:hover { padding: 1.5rem; } - .form-inline { + .email_prompt { flex-direction: column; } - .email-input { + #email { border-right: 2px solid var(--border-color); border-bottom: none; }