From 027c0487224a6415d9ec2244f519c161bd0ef990 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Thu, 6 Nov 2025 15:44:35 -0700 Subject: [PATCH 01/19] Create new subfolder for server side javascript sdk --- .../javascript-sdk/server-side/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 web-integrations/javascript-sdk/server-side/README.md diff --git a/web-integrations/javascript-sdk/server-side/README.md b/web-integrations/javascript-sdk/server-side/README.md new file mode 100644 index 0000000..8ddce39 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/README.md @@ -0,0 +1,16 @@ +# UID2 JavaScript SDK Server-Side Integration Example + +This example demonstrates a server-side Node.js implementation using the UID2 JavaScript SDK. + +## Overview + +Coming soon... + +## Prerequisites + +Coming soon... + +## Running the Example + +Coming soon... + From 9a877ccda2a40ee02f9f5ea13ecbb6fd924d81b5 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 10:29:09 -0700 Subject: [PATCH 02/19] create working example using js sdk on server --- docker-compose.yml | 10 + .../javascript-sdk/server-side/.gitignore | 17 + .../javascript-sdk/server-side/Dockerfile | 17 + .../javascript-sdk/server-side/README.md | 184 ++++++++- .../javascript-sdk/server-side/package.json | 31 ++ .../server-side/public/images/favicon.png | Bin 0 -> 1976 bytes .../server-side/public/stylesheets/app.css | 99 +++++ .../javascript-sdk/server-side/server.js | 356 ++++++++++++++++++ .../server-side/views/content.html | 7 + .../server-side/views/error.html | 9 + .../server-side/views/footer.html | 3 + .../server-side/views/header.html | 14 + .../server-side/views/identity.html | 5 + .../server-side/views/index.html | 9 + .../server-side/views/login.html | 6 + 15 files changed, 760 insertions(+), 7 deletions(-) create mode 100644 web-integrations/javascript-sdk/server-side/.gitignore create mode 100644 web-integrations/javascript-sdk/server-side/Dockerfile create mode 100644 web-integrations/javascript-sdk/server-side/package.json create mode 100644 web-integrations/javascript-sdk/server-side/public/images/favicon.png create mode 100644 web-integrations/javascript-sdk/server-side/public/stylesheets/app.css create mode 100644 web-integrations/javascript-sdk/server-side/server.js create mode 100644 web-integrations/javascript-sdk/server-side/views/content.html create mode 100644 web-integrations/javascript-sdk/server-side/views/error.html create mode 100644 web-integrations/javascript-sdk/server-side/views/footer.html create mode 100644 web-integrations/javascript-sdk/server-side/views/header.html create mode 100644 web-integrations/javascript-sdk/server-side/views/identity.html create mode 100644 web-integrations/javascript-sdk/server-side/views/index.html create mode 100644 web-integrations/javascript-sdk/server-side/views/login.html diff --git a/docker-compose.yml b/docker-compose.yml index 93cf174..3234531 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,16 @@ services: env_file: - .env + javascript-sdk-server-side: + build: + context: . + dockerfile: web-integrations/javascript-sdk/server-side/Dockerfile + ports: + - "3034:3034" + container_name: javascript-sdk-server-side + env_file: + - .env + # server-side integration (no SDK) server-side: build: diff --git a/web-integrations/javascript-sdk/server-side/.gitignore b/web-integrations/javascript-sdk/server-side/.gitignore new file mode 100644 index 0000000..708db9f --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/.gitignore @@ -0,0 +1,17 @@ +# Dependencies +node_modules/ +package-lock.json + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +.env.local +.env.*.local + +# OS files +.DS_Store + diff --git a/web-integrations/javascript-sdk/server-side/Dockerfile b/web-integrations/javascript-sdk/server-side/Dockerfile new file mode 100644 index 0000000..b0e8811 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20.11.0-alpine3.18 + +WORKDIR /usr/src/app + +# Copy package files first for better caching +COPY web-integrations/javascript-sdk/server-side/package*.json ./ +RUN npm install + +# Copy application files +COPY web-integrations/javascript-sdk/server-side/server.js ./ +COPY web-integrations/javascript-sdk/server-side/public ./public/ +COPY web-integrations/javascript-sdk/server-side/views ./views/ + +ENV PORT=3034 +EXPOSE 3034 +CMD ["npm", "start"] + diff --git a/web-integrations/javascript-sdk/server-side/README.md b/web-integrations/javascript-sdk/server-side/README.md index 8ddce39..698e3ee 100644 --- a/web-integrations/javascript-sdk/server-side/README.md +++ b/web-integrations/javascript-sdk/server-side/README.md @@ -1,16 +1,186 @@ -# UID2 JavaScript SDK Server-Side Integration Example +# Server-Side UID2 or EUID Integration Example using JavaScript SDK -This example demonstrates a server-side Node.js implementation using the UID2 JavaScript SDK. +This example demonstrates how a content publisher can use either the UID2 or EUID services and the JavaScript SDK on the server side to implement the server-side integration workflow. -## Overview +- For UID2: [UID2 services](https://unifiedid.com/docs/intro), [server-side UID2 integration workflow](https://unifiedid.com/docs/guides/integration-publisher-server-side) +- For EUID: [EUID services](https://euid.eu/docs/intro), [server-side EUID integration workflow](https://euid.eu/docs/guides/integration-publisher-server-side) -Coming soon... +This example can be configured for either UID2 or EUID — the behavior is determined by your environment variable configuration. You cannot use both simultaneously. + +## Key Difference from Other Examples + +This example proves that the UID2/EUID **JavaScript SDK works in Node.js** server environments. It uses the same `setIdentityFromEmail` method that runs in browsers, but executes it on the server. + +**Important:** This uses **public credentials** (Subscription ID + Server Public Key) which are the same credentials used for client-side integrations. This demonstrates that the client-side SDK is fully compatible with Node.js. + +### Comparison with Other Examples + +| Example | Environment | Credentials | SDK Usage | Notes | +|---------|-------------|-------------|-----------|-------| +| [server-side](../../server-side/) | Server only | API Key + Secret | No SDK | Manual encryption/decryption | +| [client-server](../client-server/) | Hybrid | API Key + Secret (server)
None (client) | Client SDK only | Server generates, client maintains | +| [client-side](../client-side/) | Client only | Public Key + Subscription ID | Client SDK | Fully client-side | +| **This example** | **Server only** | **Public Key + Subscription ID** | **Client SDK on server** | **Proves SDK works in Node.js** | + +> **Note:** While the server side of the example application is implemented in JavaScript using Node.js, it is not a requirement. You can use any technology of your choice and refer to the example application for illustration of the functionality that needs to be implemented. ## Prerequisites -Coming soon... +- Node.js 20.x or higher +- UID2/EUID API Key and Client Secret (for server-side integration) +- Access to UID2 or EUID integration environment + +## Build and Run the Example Application + +### Using Docker Compose (Recommended) + +From the repository root directory: + +```bash +# Start the service +docker compose up javascript-sdk-server-side +``` + +The application will be available at http://localhost:3034 + +To view logs or stop the service: + +```bash +# View logs (in another terminal) +docker compose logs javascript-sdk-server-side + +# Stop the service +docker compose stop javascript-sdk-server-side +``` + +### Using Docker Build + +```bash +# Build the image +docker build -f web-integrations/javascript-sdk/server-side/Dockerfile -t javascript-sdk-server-side . + +# Run the container +docker run -it --rm -p 3034:3034 --env-file .env javascript-sdk-server-side +``` + +### Using npm (Local Development) + +```bash +# Navigate to this directory +cd web-integrations/javascript-sdk/server-side + +# Install dependencies +npm install + +# Start the server (requires .env file in repository root) +npm start +``` + +## Configuration + +The following table lists the environment variables that you must specify to start the application. + +### Core Configuration + +| Variable | Description | Example Values | +|:---------|:------------|:---------------| +| `UID_SERVER_BASE_URL` | The base URL of the UID2/EUID service. For details, see [Environments](https://unifiedid.com/docs/getting-started/gs-environments) (UID2) or [Environments](https://euid.eu/docs/getting-started/gs-environments) (EUID). | UID2: `https://operator-integ.uidapi.com`
EUID: `https://integ.euid.eu/v2` | +| `UID_CSTG_SUBSCRIPTION_ID` | Your UID2/EUID subscription ID for Client-Side Token Generation. **These are public credentials.** | Your assigned subscription ID (e.g., `DMr7uHxqLU`) | +| `UID_CSTG_SERVER_PUBLIC_KEY` | Your UID2/EUID server public key for Client-Side Token Generation. **These are public credentials.** | Your assigned public key | +| `SESSION_KEY` | The key to encrypt session data stored in the application session cookie. This can be any arbitrary string. | Any secure random string | + +### Display/UI Configuration + +| Variable | Description | Example Values | +|:---------|:------------|:---------------| +| `IDENTITY_NAME` | Identity name for UI display | UID2: `UID2`
EUID: `EUID` | +| `DOCS_BASE_URL` | Documentation base URL | UID2: `https://unifiedid.com/docs`
EUID: `https://euid.eu/docs` | + +After you see output similar to the following, the example application is up and running: + +``` +Server-Side UID2/EUID Integration Example using JavaScript SDK +Listening at http://localhost:3034 + +Note: SDK integration is not yet complete. +TODO: Add UID2/EUID JavaScript SDK package and implement token generation/refresh. +``` + +If needed, to close the application, terminate the docker container or use the `Ctrl+C` keyboard shortcut. + +## Test the Example Application + +The example application illustrates the steps documented in the server-side integration guides: +- UID2: [Server-Side Integration Guide](https://unifiedid.com/docs/guides/integration-publisher-server-side) +- EUID: [Server-Side Integration Guide](https://euid.eu/docs/guides/integration-publisher-server-side) + +**Note:** For API endpoint documentation, see the UID2 or EUID docs based on your configuration. + +The application provides three main pages: index (main), example content 1, and example content 2. Access to these pages is possible only after the user completes the login process. If the user is not logged in, they will be redirected to the login page. + +Submitting the login form simulates logging in to a publisher's application in the real world. Normally the login would require checking the user's secure credentials (for example, a password), but for demonstration purposes this step is omitted, and the login process focuses on integration with the UID2/EUID services using the JavaScript SDK on the server. + +The following table outlines and annotates the steps you may take to test and explore the example application. + +| Step | Description | Comments | +|:----:|:------------|:---------| +| 1 | In your browser, navigate to the application main page at `http://localhost:3034`. | The displayed main (index) page of the example application provides a [login form](views/login.html) for the user to complete the UID2/EUID login process.
IMPORTANT: A real-life application must also display a form for the user to express their consent to targeted advertising. | +| 2 | Enter the user email address that you want to use for testing and click **Log In**. | This is a call to the `/login` endpoint ([server.js](server.js)). The login initiated on the server side uses the JavaScript SDK's `setIdentityFromEmail` method to generate a token and processes the received response. The SDK handles all encryption/decryption automatically, just as it does in the browser. | +| | The main page is updated to display links to the two pages with protected content and the established identity information. | The displayed identity information is the `body` property of the response from the SDK's `setIdentityFromEmail` call. If the response is successful, the returned identity is saved to a session cookie (a real-world application would use a different way to store session data) and the protected index page is rendered. | +| 3 | Click either of the two sample content pages. | When the user requests the index or content pages, the server reads the user session and extracts the current identity ([server.js](server.js)). The `advertising_token` on the identity can be used for targeted advertising. | +| 4 | Click the **Back to the main page** link. | Note that the identity contains several timestamps that determine when the advertising token becomes invalid (`identity_expires`) and when the server should attempt to refresh it (`refresh_from`). Every time a protected page is requested, the `verifyIdentity` function ([server.js](server.js)) uses the SDK to refresh the token as needed.
The user is automatically logged out in the following cases:
- If the identity expires without being refreshed and refresh attempt fails.
- If the refresh token expires.
- If the refresh attempt indicates that the user has opted out. | +| 5 | To exit the application, click **Log Out**. | This calls the `/logout` endpoint on the server ([server.js](server.js)), which clears the session and the first-party cookie and presents the user with the login form again.
NOTE: The page displays the **Log Out** button as long as the user is logged in. | + +## Implementation Status + +> **⚠️ Important:** This example is currently in initial development. The JavaScript SDK integration is not yet complete. + +### TODO + +- [ ] Identify the correct way to import/use the JavaScript SDK in Node.js (it's designed for browsers but should work with proper setup) +- [ ] Implement `setIdentityFromEmail` call in the `/login` endpoint using public credentials +- [ ] Handle SDK callbacks/promises properly in the Node.js environment +- [ ] Add error handling for SDK operations +- [ ] Test that the generated identity matches what the client-side example produces +- [ ] Update documentation with actual SDK API usage once working +- [ ] Add to docker-compose.yml in repository root + +## Development Notes + +This example demonstrates that the browser-based JavaScript SDK can run in Node.js. It uses the same public credentials and methods as the client-side example: + +**Client-Side (Browser):** +```javascript +// In browser +const sdk = window.__uid2; +await sdk.setIdentityFromEmail(email, { + subscriptionId: 'DMr7uHxqLU', + serverPublicKey: 'UID2-X-I-MFk...' +}); +``` + +**This Example (Node.js Server):** +```javascript +// Same code, but running on Node.js server +const sdk = getUid2Sdk(); // Need to figure out proper import +await sdk.setIdentityFromEmail(email, { + subscriptionId: process.env.UID_CSTG_SUBSCRIPTION_ID, + serverPublicKey: process.env.UID_CSTG_SERVER_PUBLIC_KEY +}); +``` + +The key challenge is figuring out how to properly import/initialize the SDK in a Node.js environment since it's primarily designed for browsers. + +## Contributing + +When implementing the SDK integration, ensure: -## Running the Example +1. The SDK is properly initialized with API key and client secret +2. Error handling matches the existing server-side example behavior +3. Session management remains unchanged +4. The user experience is identical to the manual crypto version +5. All environment variables are properly documented -Coming soon... +## License +This project is licensed under the Apache License 2.0 - see the LICENSE file for details. diff --git a/web-integrations/javascript-sdk/server-side/package.json b/web-integrations/javascript-sdk/server-side/package.json new file mode 100644 index 0000000..1dd4697 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/package.json @@ -0,0 +1,31 @@ +{ + "name": "uid2-javascript-sdk-server-side", + "version": "1.0.0", + "description": "Server-Side UID2/EUID Integration Example using JavaScript SDK", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "keywords": [ + "uid2", + "euid", + "identity", + "server-side" + ], + "author": "", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.6.0", + "cookie-session": "^2.0.0", + "dotenv": "^16.0.3", + "ejs": "^3.1.9", + "express": "^4.18.2", + "jsdom": "^23.0.0", + "nocache": "^4.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} + diff --git a/web-integrations/javascript-sdk/server-side/public/images/favicon.png b/web-integrations/javascript-sdk/server-side/public/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..48885e0436382ee528313e051d76eb21ed9568c3 GIT binary patch literal 1976 zcmY*a2UL^E8vS{IR4KZKj@g8!frJnQlMU5?z*0jIG)O2B0wD=WXkn#UQ5OkCS`bAk zq5=!cLqMuX6A%I}Ah;M}WCaT#D}seh)aQF|&iSU?@6NsR%{g->!_)oXc6m*C006c- zJK^x65iAbbZK9sZX0M3`sc<_tI{^6YqQb{uX>l^t3GW5~Yy$vDJqZAtA}IAQ0EjgO zfJGtzURR;ix0J4ui%iqlv6H1GQ2Zzx@NbrPc zx`+(`*aVDd8ckvagA<}DR3;|D3i5@45zWOg0s{U*VMSR%{M|gk2WSiucpuyxZVIuM z2ZO;_M%Xb79_RRNyJ%$viD0ql7z83dJ{}&Ag3}n`2qYSfMwpr*%*;$g3=?J|l@**| zLS;g~iu^|hM`DID$aEH&Mg@y?gF|SsEGq~^Z1lZ-JtvEN?7vP_=C`s$1tP>51QKqF z_^vJ5iWQ?64+fbeau(}bBe7qY|A&3^!6L-w|8tnHm3~1*rCQ5l5#O)PTHdwWFaQ9g z2b^(sKK^pI4-H@RaZssEelx7r58tf{zoLa3qc4Ae9X-asuGg#ZS;8@NbZ5q8oV1OS z?peoQ2Q=Nx$Id_PN-&ilpr&&q4XTjp=_(Eo8P9^?{0(;P%v|keTO#eWMty4Iix_ocCT_1S`Hmk+x?^se7+ue&+DT>nRlI@IF) z3duxppl!<=-6$OZ2Sdk6t;yFrXdvK@(boR0#Xlc@ApYc8`~xWPP1d=FD&6t6o0H_( z9;sz|wc(gu$voFr_Yws#HF;Lvd`$Q#K}o5nFn7(7y3P1 zT7usVYt5VF`a2V6A2V!ro4rAp?B59U@MyQD2>nh9->aX{NZKK(Tb?*ol<2Vu-~}hC zv2&JlykEMecv@Hoi;SeSJ3OO@|DsG4?SU>SX*CP%))wf`yN70p9;05;nyN!54oa(P zaXmUH=56gO1u?6ji?y+@bhVd~tRv;p#tf1)EsGqWddCYI_eMPJqxedFerH4gv08_x z&ZfCk7NhLTS4~3LaydvdJ*tE)|E1!sU`@`b>tQO&Cl9NCJAA00=9gvWcYR^M_A zYL*~@oPOV-biM+x_X7_-xsd70l6tYrwe2`0(u`+6w!rLs)=VE6@QMgbmdYP@AmAl; zY{($X-w8eX zh@Z}K17aJ{|I5C@RXpGTstw@{=B3@e(`wLG;yF^befqvi?c0n2tludqW&Z)B;5Pp1 z9w>om5K~j4u>T;U;t%FPBWH2Fop|kU_b7aEIN!3%+9V+Q*;;^MrH9wHoTo<=3voW3 zaip@;LpL({FcjC3*Bz9?KdT)~@X^nKzs6)_p|!d`#sxuAxD!q1G!4(fGz>jy7AW^+ zZKy{sX+qUBL3q&h9rcj4UMlNUN?a^_eu`qYdM_EayKU}Lhr1B0ek#UILUA}}LAX-2 z#=O&0&Z7!kO7nH{&TR}*j?NKpd8;h%2*P%(y;~Q!SCqv)Yb%Q~1P_2Pcy;oOQd2`Zk%L|-9#0{??xy4wUCHAG z2&yTB(d<U0%e(#|cfG8;Kl) zvMx)uPTT2rSa}|I*ShhA6j!un>17Kflx}R=ICCqC?_5z^Mo_zi zno!lv_1dRg+ajZn)~+_r=0Dh`jBvL1hUwQlI#NH2eJh;psm!gu*Vw$al_h()UG^T& zz4&*iRK7;OBmi-Q_O86-e_C*>XVfQfPFGijhoE9hDeA^1jZezmXY(pMHpufRn-Z;s zc;epYB~;#iPx*ZQwcZxnKK#O69jd2_rrb5#^!@_CsxRUg3nn8R#D$&Mkd}MmE%VT| zdWC%)wE4-#&fFcZaD+;$VOTXuW=2bHZm%0L0`Kw$<2qjo-nwNx_#t%f_(jTG1KvNsNDID6F5`$}gSM_#|`H-oWceqcmOd|LE-Cj;paJrvC%k09&pA literal 0 HcmV?d00001 diff --git a/web-integrations/javascript-sdk/server-side/public/stylesheets/app.css b/web-integrations/javascript-sdk/server-side/public/stylesheets/app.css new file mode 100644 index 0000000..58b2cf8 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/public/stylesheets/app.css @@ -0,0 +1,99 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} + +.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: 0px; + text-shadow: none; + margin: 0em; + padding: 1px 6px; + background-color: rgba(2,10,64,1.0); + border-image: initial +} + +.form { + margin-top: 40px; +} + +.email_prompt { + align-items: center; + align-self: center; + background-color: white; + border: 1px solid rgba(2,10,64,1.0); + border-radius: 2px; + box-sizing: border-box; + display: inline-flex; + flex-direction: row; + flex-shrink: 0; + height: 40px; + justify-content: flex-start; + margin-right: 1.0px; + margin-bottom: 20px; + min-width: 399px; + padding: 0 16.0px; + position: relative; + width: auto; +} + +#email { + background-color: white; + 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.0); + font-family: 'Raleway', Helvetica, Arial, serif; + font-size: 12px; + font-style: normal; + font-weight: 500; + padding: 1px 2px; + outline: none; +} + +h1 { + padding-bottom: 20px; +} + +#uid2_state .label { + white-space: nowrap; + padding-right: 20px; +} +#uid2_state tr { + margin-top: 10px; +} + +.message { + color: green; + padding: 20px; + margin-left: -22px; + font-size: 16px; + font-weight: 500; + border: 2px solid green; + border-radius: 5px; +} + diff --git a/web-integrations/javascript-sdk/server-side/server.js b/web-integrations/javascript-sdk/server-side/server.js new file mode 100644 index 0000000..7b6b495 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/server.js @@ -0,0 +1,356 @@ +"use strict"; + +// Load environment variables from .env file (for local development) +require('dotenv').config({ path: '../../../.env' }); + +const session = require('cookie-session'); +const ejs = require('ejs'); +const express = require('express'); +const nocache = require('nocache'); +const { JSDOM } = require('jsdom'); +const fs = require('fs'); +const path = require('path'); + +const app = express(); +const port = process.env.PORT || 3034; + +// UID2/EUID Configuration +// Note: Using UID_CLIENT_BASE_URL because we're using client-side SDK with CSTG credentials +// When running in Docker, use host.docker.internal instead of localhost +let uidBaseUrl = process.env.UID_CLIENT_BASE_URL; +if (uidBaseUrl && uidBaseUrl.includes('localhost')) { + uidBaseUrl = uidBaseUrl.replace('localhost', 'host.docker.internal'); + console.log(`Adjusted base URL for Docker: ${uidBaseUrl}`); +} +const subscriptionId = process.env.UID_CSTG_SUBSCRIPTION_ID; +const serverPublicKey = process.env.UID_CSTG_SERVER_PUBLIC_KEY; +const uidJsSdkUrl = process.env.UID_JS_SDK_URL || 'https://cdn.integ.uidapi.com/uid2-sdk-4.0.1.js'; +const uidJsSdkName = process.env.UID_JS_SDK_NAME || '__uid2'; + +// UI/Display configuration +const identityName = process.env.IDENTITY_NAME; +const docsBaseUrl = process.env.DOCS_BASE_URL; + +// Initialize UID2 JavaScript SDK in a simulated browser environment using jsdom +// This demonstrates that the client-side SDK works in Node.js with jsdom +let uid2Sdk = null; +let dom = null; + +async function initializeSDK() { + console.log('Initializing UID2/EUID SDK in Node.js using jsdom...'); + + const crypto = require('crypto'); + + // Create a virtual DOM environment + dom = new JSDOM('', { + url: 'http://localhost', + runScripts: 'dangerously', + resources: 'usable', + pretendToBeVisual: true, + }); + + // Set global variables for the SDK + global.window = dom.window; + global.document = dom.window.document; + global.navigator = dom.window.navigator; + global.localStorage = dom.window.localStorage; + + // Polyfill Web Crypto API for jsdom + // The SDK needs window.crypto.subtle for encryption + Object.defineProperty(dom.window, 'crypto', { + value: crypto.webcrypto, + writable: false, + configurable: true + }); + + // Polyfill TextEncoder and TextDecoder (required by the SDK) + const util = require('util'); + global.TextEncoder = util.TextEncoder; + global.TextDecoder = util.TextDecoder; + dom.window.TextEncoder = util.TextEncoder; + dom.window.TextDecoder = util.TextDecoder; + + // Load the UID2 SDK script + // First, try to load from local file, otherwise fetch from CDN + try { + let sdkCode; + const localSdkPath = path.join(__dirname, '../../../uid2-web-integrations/dist/uid2-sdk-4.0.1.js'); + + if (fs.existsSync(localSdkPath)) { + console.log('Loading SDK from local file:', localSdkPath); + sdkCode = fs.readFileSync(localSdkPath, 'utf8'); + } else { + console.log('Loading SDK from CDN:', uidJsSdkUrl); + const axios = require('axios'); + const response = await axios.get(uidJsSdkUrl); + sdkCode = response.data; + } + + // Execute the SDK code in the jsdom context + const scriptEl = dom.window.document.createElement('script'); + scriptEl.textContent = sdkCode; + dom.window.document.body.appendChild(scriptEl); + + // Wait a bit for the SDK to initialize + await new Promise(resolve => setTimeout(resolve, 100)); + + // Get reference to the SDK + uid2Sdk = dom.window[uidJsSdkName]; + + if (!uid2Sdk) { + throw new Error(`SDK not found at window.${uidJsSdkName}`); + } + + console.log('✓ SDK loaded successfully'); + console.log(`✓ SDK available at window.${uidJsSdkName}`); + + // Initialize the SDK + uid2Sdk.init({ baseUrl: uidBaseUrl }); + + return uid2Sdk; + } catch (error) { + console.error('Failed to initialize SDK:', error); + throw error; + } +} + +// Express middleware setup +app.use(session({ + keys: [process.env.SESSION_KEY || 'default-session-key-change-me'], + maxAge: 24 * 60 * 60 * 1000 // 24 hours +})); + +app.use(express.static('public')); +app.use(express.urlencoded({ extended: true })); + +app.engine('.html', ejs.__express); +app.set('view engine', 'html'); + +app.use(nocache()); + +/** + * Check if an identity is still valid and refreshable + */ +function isRefreshableIdentity(identity) { + if (!identity || typeof identity !== 'object') { + return false; + } + if (!identity.refresh_expires || Date.now() >= identity.refresh_expires) { + return false; + } + return !!identity.refresh_token; +} + +/** + * Refresh an identity token using the SDK + * The SDK will automatically refresh tokens when initialized with an existing identity + */ +async function refreshIdentity(identity) { + // TODO: Use JS SDK to refresh identity + // The SDK's init() with an existing identity will handle refresh automatically + // Example: + // const sdk = getUid2Sdk(); + // return new Promise((resolve) => { + // sdk.init({ + // baseUrl: uidBaseUrl, + // identity: identity + // }); + // sdk.callbacks.push((eventType, payload) => { + // if (eventType === 'IdentityUpdated') { + // resolve(payload.identity); + // } + // }); + // }); + + console.log('TODO: Implement SDK-based identity refresh'); + return identity; +} + +/** + * Verify and refresh identity if needed + */ +async function verifyIdentity(req) { + if (!isRefreshableIdentity(req.session.identity)) { + return false; + } + + // Check if identity needs refresh + if (Date.now() >= req.session.identity.refresh_from || Date.now() >= req.session.identity.identity_expires) { + req.session.identity = await refreshIdentity(req.session.identity); + } + + return !!req.session.identity; +} + +/** + * Middleware to protect routes that require authentication + */ +async function protect(req, res, next) { + if (await verifyIdentity(req)) { + next(); + } else { + req.session = null; + res.redirect('/login'); + } +} + +// Routes + +/** + * Main page - requires authentication + */ +app.get('/', protect, (req, res) => { + res.render('index', { + identity: req.session.identity, + identityName, + docsBaseUrl + }); +}); + +/** + * Sample content page 1 - requires authentication + */ +app.get('/content1', protect, (req, res) => { + res.render('content', { + identity: req.session.identity, + content: 'First Sample Content', + identityName, + docsBaseUrl + }); +}); + +/** + * Sample content page 2 - requires authentication + */ +app.get('/content2', protect, (req, res) => { + res.render('content', { + identity: req.session.identity, + content: 'Second Sample Content', + identityName, + docsBaseUrl + }); +}); + +/** + * Login page + */ +app.get('/login', async (req, res) => { + if (await verifyIdentity(req)) { + res.redirect('/'); + } else { + req.session = null; + res.render('login', { + identityName, + docsBaseUrl + }); + } +}); + +/** + * Handle login form submission + * Uses the JavaScript SDK's setIdentityFromEmail method on the server + */ +app.post('/login', async (req, res) => { + if (!uid2Sdk) { + return res.render('error', { + error: 'SDK not initialized. Server may still be starting up.', + response: null, + identityName, + docsBaseUrl + }); + } + + try { + console.log(`Generating token for email: ${req.body.email}`); + + // Use the SDK's setIdentityFromEmail method + // This is the same method used in browser environments + const identity = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Token generation timed out after 10 seconds')); + }, 10000); + + // Add callback to capture the identity + const callbackHandler = (eventType, payload) => { + console.log(`SDK Event: ${eventType}`, payload?.identity ? 'Identity received' : 'No identity'); + + if (eventType === 'InitCompleted' || eventType === 'IdentityUpdated') { + clearTimeout(timeout); + if (payload?.identity) { + // Remove this specific callback + const index = uid2Sdk.callbacks.indexOf(callbackHandler); + if (index > -1) { + uid2Sdk.callbacks.splice(index, 1); + } + resolve(payload.identity); + } + } + }; + + uid2Sdk.callbacks.push(callbackHandler); + + // Call setIdentityFromEmail + uid2Sdk.setIdentityFromEmail( + req.body.email, + { + subscriptionId: subscriptionId, + serverPublicKey: serverPublicKey + } + ).catch(err => { + clearTimeout(timeout); + reject(err); + }); + }); + + if (!identity) { + throw new Error('No identity returned from SDK'); + } + + console.log('✓ Token generated successfully'); + req.session.identity = identity; + res.redirect('/'); + + } catch (error) { + console.error('Token generation failed:', error); + res.render('error', { + error: error.message || error.toString(), + response: error.response || null, + identityName, + docsBaseUrl + }); + } +}); + +/** + * Logout endpoint + */ +app.get('/logout', (req, res) => { + req.session = null; + res.redirect('/login'); +}); + +// Start server and initialize SDK +initializeSDK().then(() => { + app.listen(port, () => { + console.log(''); + console.log('='.repeat(70)); + console.log(`Server-Side ${identityName || 'UID2/EUID'} Integration Example using JavaScript SDK`); + console.log('='.repeat(70)); + console.log(`✓ Server listening at http://localhost:${port}`); + console.log(''); + console.log('Configuration:'); + console.log(` Base URL: ${uidBaseUrl}`); + console.log(` Subscription ID: ${subscriptionId || 'NOT SET'}`); + console.log(` Public Key: ${serverPublicKey ? serverPublicKey.substring(0, 30) + '...' : 'NOT SET'}`); + console.log(` SDK Name: ${uidJsSdkName}`); + console.log(''); + console.log('✓ JavaScript SDK initialized and ready!'); + console.log(' The browser-based SDK is now running in Node.js via jsdom.'); + console.log('='.repeat(70)); + console.log(''); + }); +}).catch(error => { + console.error('Failed to start server:', error); + process.exit(1); +}); + diff --git a/web-integrations/javascript-sdk/server-side/views/content.html b/web-integrations/javascript-sdk/server-side/views/content.html new file mode 100644 index 0000000..f498237 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/views/content.html @@ -0,0 +1,7 @@ +<%- include('header.html'); -%> +

Protected Content — accessible only with a valid <%- identityName %> identity:

+
<%= content %>
+

Back to the main page

+<%- include('identity.html'); -%> +<%- include('footer.html'); -%> + diff --git a/web-integrations/javascript-sdk/server-side/views/error.html b/web-integrations/javascript-sdk/server-side/views/error.html new file mode 100644 index 0000000..f8b33c1 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/views/error.html @@ -0,0 +1,9 @@ +<%- include('header.html'); -%> +

Something went wrong:

+
<%= error %>
+

Response from the <%- identityName %> operator:

+
<%= response ? JSON.stringify(response.data) : '' %>
+

HTTP error:

+
<%= response ? (response.status + ' ' + response.statusText) : '' %>
+

Back to the main page

+<%- include('footer.html'); -%> diff --git a/web-integrations/javascript-sdk/server-side/views/footer.html b/web-integrations/javascript-sdk/server-side/views/footer.html new file mode 100644 index 0000000..c9efd30 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/views/footer.html @@ -0,0 +1,3 @@ + + + diff --git a/web-integrations/javascript-sdk/server-side/views/header.html b/web-integrations/javascript-sdk/server-side/views/header.html new file mode 100644 index 0000000..ec322de --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/views/header.html @@ -0,0 +1,14 @@ + + + + + Server-Side <%- identityName %> Integration Example using JavaScript SDK + + + + +

Server-Side <%- identityName %> Integration Example using JavaScript SDK

+

This example demonstrates how a content publisher can use <%- identityName %> services and the JavaScript SDK on the server to implement the + server-side <%- identityName %> integration workflow. This proves that the JavaScript SDK (designed for browsers) also works in a Node.js server environment. + [Source Code]

+ diff --git a/web-integrations/javascript-sdk/server-side/views/identity.html b/web-integrations/javascript-sdk/server-side/views/identity.html new file mode 100644 index 0000000..9fa8104 --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/views/identity.html @@ -0,0 +1,5 @@ +
<%= JSON.stringify(identity, null, 2) %>
+
+ +
+ diff --git a/web-integrations/javascript-sdk/server-side/views/index.html b/web-integrations/javascript-sdk/server-side/views/index.html new file mode 100644 index 0000000..2b2b47b --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/views/index.html @@ -0,0 +1,9 @@ +<%- include('header.html'); -%> +

Protected Content — accessible only with a valid <%- identityName %> identity:

+ +

Current <%- identityName %> Identity:

+<%- include('identity.html'); -%> +<%- include('footer.html'); -%> diff --git a/web-integrations/javascript-sdk/server-side/views/login.html b/web-integrations/javascript-sdk/server-side/views/login.html new file mode 100644 index 0000000..ab2d51b --- /dev/null +++ b/web-integrations/javascript-sdk/server-side/views/login.html @@ -0,0 +1,6 @@ +<%- include('header.html'); -%> +
+ +
+
+<%- include('footer.html'); -%> From b4e4fe082aa4262c03e22b00dd533e298a9c770e Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 12:10:09 -0700 Subject: [PATCH 03/19] simplify ui --- .../javascript-sdk/server-side/server.js | 50 +++--------------- .../server-side/views/content.html | 7 --- .../server-side/views/error.html | 51 +++++++++++++++---- .../server-side/views/footer.html | 3 -- .../server-side/views/header.html | 14 ----- .../server-side/views/identity.html | 5 -- .../server-side/views/index.html | 48 +++++++++++++---- .../server-side/views/login.html | 6 --- 8 files changed, 87 insertions(+), 97 deletions(-) delete mode 100644 web-integrations/javascript-sdk/server-side/views/content.html delete mode 100644 web-integrations/javascript-sdk/server-side/views/footer.html delete mode 100644 web-integrations/javascript-sdk/server-side/views/header.html delete mode 100644 web-integrations/javascript-sdk/server-side/views/identity.html delete mode 100644 web-integrations/javascript-sdk/server-side/views/login.html diff --git a/web-integrations/javascript-sdk/server-side/server.js b/web-integrations/javascript-sdk/server-side/server.js index 7b6b495..26d05c8 100644 --- a/web-integrations/javascript-sdk/server-side/server.js +++ b/web-integrations/javascript-sdk/server-side/server.js @@ -197,55 +197,16 @@ async function protect(req, res, next) { // Routes /** - * Main page - requires authentication + * Main page - shows login form or identity result */ -app.get('/', protect, (req, res) => { +app.get('/', (req, res) => { res.render('index', { - identity: req.session.identity, + identity: req.session.identity || null, identityName, docsBaseUrl }); }); -/** - * Sample content page 1 - requires authentication - */ -app.get('/content1', protect, (req, res) => { - res.render('content', { - identity: req.session.identity, - content: 'First Sample Content', - identityName, - docsBaseUrl - }); -}); - -/** - * Sample content page 2 - requires authentication - */ -app.get('/content2', protect, (req, res) => { - res.render('content', { - identity: req.session.identity, - content: 'Second Sample Content', - identityName, - docsBaseUrl - }); -}); - -/** - * Login page - */ -app.get('/login', async (req, res) => { - if (await verifyIdentity(req)) { - res.redirect('/'); - } else { - req.session = null; - res.render('login', { - identityName, - docsBaseUrl - }); - } -}); - /** * Handle login form submission * Uses the JavaScript SDK's setIdentityFromEmail method on the server @@ -307,6 +268,7 @@ app.post('/login', async (req, res) => { } console.log('✓ Token generated successfully'); + console.log('Identity:', JSON.stringify(identity, null, 2)); req.session.identity = identity; res.redirect('/'); @@ -322,11 +284,11 @@ app.post('/login', async (req, res) => { }); /** - * Logout endpoint + * Logout endpoint - clears session and returns to main page */ app.get('/logout', (req, res) => { req.session = null; - res.redirect('/login'); + res.redirect('/'); }); // Start server and initialize SDK diff --git a/web-integrations/javascript-sdk/server-side/views/content.html b/web-integrations/javascript-sdk/server-side/views/content.html deleted file mode 100644 index f498237..0000000 --- a/web-integrations/javascript-sdk/server-side/views/content.html +++ /dev/null @@ -1,7 +0,0 @@ -<%- include('header.html'); -%> -

Protected Content — accessible only with a valid <%- identityName %> identity:

-
<%= content %>
-

Back to the main page

-<%- include('identity.html'); -%> -<%- include('footer.html'); -%> - diff --git a/web-integrations/javascript-sdk/server-side/views/error.html b/web-integrations/javascript-sdk/server-side/views/error.html index f8b33c1..495d2d0 100644 --- a/web-integrations/javascript-sdk/server-side/views/error.html +++ b/web-integrations/javascript-sdk/server-side/views/error.html @@ -1,9 +1,42 @@ -<%- include('header.html'); -%> -

Something went wrong:

-
<%= error %>
-

Response from the <%- identityName %> operator:

-
<%= response ? JSON.stringify(response.data) : '' %>
-

HTTP error:

-
<%= response ? (response.status + ' ' + response.statusText) : '' %>
-

Back to the main page

-<%- include('footer.html'); -%> + + + + + Error - Server-Side <%- identityName %> Integration Example + + + + +

Server-Side <%- identityName %> Integration Example using JavaScript SDK

+ +

Error

+

Something went wrong while generating the <%- identityName %> token:

+ +
+

Error Message:

+
<%= error %>
+
+ + <% if (response && response.data) { %> +

Response from <%- identityName %> Operator:

+
<%= JSON.stringify(response.data, null, 2) %>
+ <% } %> + + <% if (response && response.status) { %> +

HTTP Status:

+
<%= response.status %> <%= response.statusText || '' %>
+ <% } %> + + + +

Common Issues:

+
    +
  • HTTP status 0: Check that your local UID2 operator and uid2-admin are running
  • +
  • Connection refused: Verify UID_CLIENT_BASE_URL is set correctly
  • +
  • Invalid credentials: Check UID_CSTG_SUBSCRIPTION_ID and UID_CSTG_SERVER_PUBLIC_KEY
  • +
+ + + diff --git a/web-integrations/javascript-sdk/server-side/views/footer.html b/web-integrations/javascript-sdk/server-side/views/footer.html deleted file mode 100644 index c9efd30..0000000 --- a/web-integrations/javascript-sdk/server-side/views/footer.html +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web-integrations/javascript-sdk/server-side/views/header.html b/web-integrations/javascript-sdk/server-side/views/header.html deleted file mode 100644 index ec322de..0000000 --- a/web-integrations/javascript-sdk/server-side/views/header.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Server-Side <%- identityName %> Integration Example using JavaScript SDK - - - - -

Server-Side <%- identityName %> Integration Example using JavaScript SDK

-

This example demonstrates how a content publisher can use <%- identityName %> services and the JavaScript SDK on the server to implement the - server-side <%- identityName %> integration workflow. This proves that the JavaScript SDK (designed for browsers) also works in a Node.js server environment. - [Source Code]

- diff --git a/web-integrations/javascript-sdk/server-side/views/identity.html b/web-integrations/javascript-sdk/server-side/views/identity.html deleted file mode 100644 index 9fa8104..0000000 --- a/web-integrations/javascript-sdk/server-side/views/identity.html +++ /dev/null @@ -1,5 +0,0 @@ -
<%= JSON.stringify(identity, null, 2) %>
-
- -
- diff --git a/web-integrations/javascript-sdk/server-side/views/index.html b/web-integrations/javascript-sdk/server-side/views/index.html index 2b2b47b..92d649d 100644 --- a/web-integrations/javascript-sdk/server-side/views/index.html +++ b/web-integrations/javascript-sdk/server-side/views/index.html @@ -1,9 +1,39 @@ -<%- include('header.html'); -%> -

Protected Content — accessible only with a valid <%- identityName %> identity:

- -

Current <%- identityName %> Identity:

-<%- include('identity.html'); -%> -<%- include('footer.html'); -%> + + + + + Server-Side <%- identityName %> Integration Example using JavaScript SDK + + + + +

Server-Side <%- identityName %> Integration Example using JavaScript SDK

+

+ This example demonstrates how a content publisher can use <%- identityName %> services and the JavaScript SDK on the server to implement the + server-side <%- identityName %> integration workflow. This proves that the JavaScript SDK (designed for browsers) also works in a Node.js server environment. + [Source Code] +

+ + <% if (identity) { %> + +

Current <%- identityName %> Identity:

+
<%= JSON.stringify(identity, null, 2) %>
+
+
+ +
+
+ <% } else { %> + +
+
+ +
+
+
+ <% } %> + + + diff --git a/web-integrations/javascript-sdk/server-side/views/login.html b/web-integrations/javascript-sdk/server-side/views/login.html deleted file mode 100644 index ab2d51b..0000000 --- a/web-integrations/javascript-sdk/server-side/views/login.html +++ /dev/null @@ -1,6 +0,0 @@ -<%- include('header.html'); -%> -
- -
-
-<%- include('footer.html'); -%> From 8701ecbae1022dad6564615f8f78f8da0b647c64 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 12:21:20 -0700 Subject: [PATCH 04/19] simplify error ui to match previous server side example --- .../javascript-sdk/server-side/server.js | 28 +++++++++---- .../server-side/views/error.html | 42 ++++++------------- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/web-integrations/javascript-sdk/server-side/server.js b/web-integrations/javascript-sdk/server-side/server.js index 26d05c8..8fba227 100644 --- a/web-integrations/javascript-sdk/server-side/server.js +++ b/web-integrations/javascript-sdk/server-side/server.js @@ -231,20 +231,30 @@ app.post('/login', async (req, res) => { reject(new Error('Token generation timed out after 10 seconds')); }, 10000); - // Add callback to capture the identity + // Add callback to capture the identity or optout const callbackHandler = (eventType, payload) => { console.log(`SDK Event: ${eventType}`, payload?.identity ? 'Identity received' : 'No identity'); - if (eventType === 'InitCompleted' || eventType === 'IdentityUpdated') { + // Handle successful identity generation + if ((eventType === 'InitCompleted' || eventType === 'IdentityUpdated') && payload?.identity) { clearTimeout(timeout); - if (payload?.identity) { - // Remove this specific callback - const index = uid2Sdk.callbacks.indexOf(callbackHandler); - if (index > -1) { - uid2Sdk.callbacks.splice(index, 1); - } - resolve(payload.identity); + // Remove this specific callback + const index = uid2Sdk.callbacks.indexOf(callbackHandler); + if (index > -1) { + uid2Sdk.callbacks.splice(index, 1); } + resolve(payload.identity); + } + + // Handle optout - user has opted out of UID2 + if (eventType === 'OptoutReceived') { + clearTimeout(timeout); + // Remove this specific callback + const index = uid2Sdk.callbacks.indexOf(callbackHandler); + if (index > -1) { + uid2Sdk.callbacks.splice(index, 1); + } + reject(new Error('User has opted out of UID2')); } }; diff --git a/web-integrations/javascript-sdk/server-side/views/error.html b/web-integrations/javascript-sdk/server-side/views/error.html index 495d2d0..d1e1f6f 100644 --- a/web-integrations/javascript-sdk/server-side/views/error.html +++ b/web-integrations/javascript-sdk/server-side/views/error.html @@ -2,41 +2,25 @@ - Error - Server-Side <%- identityName %> Integration Example + Server-Side <%- identityName %> Integration Example using JavaScript SDK

Server-Side <%- identityName %> Integration Example using JavaScript SDK

- -

Error

-

Something went wrong while generating the <%- identityName %> token:

- -
-

Error Message:

-
<%= error %>
-
+

+ This example demonstrates how a content publisher can use <%- identityName %> services and the JavaScript SDK on the server to implement the + server-side <%- identityName %> integration workflow. This proves that the JavaScript SDK (designed for browsers) also works in a Node.js server environment. + [Source Code] +

- <% if (response && response.data) { %> -

Response from <%- identityName %> Operator:

-
<%= JSON.stringify(response.data, null, 2) %>
- <% } %> - - <% if (response && response.status) { %> -

HTTP Status:

-
<%= response.status %> <%= response.statusText || '' %>
- <% } %> - - - -

Common Issues:

-
    -
  • HTTP status 0: Check that your local UID2 operator and uid2-admin are running
  • -
  • Connection refused: Verify UID_CLIENT_BASE_URL is set correctly
  • -
  • Invalid credentials: Check UID_CSTG_SUBSCRIPTION_ID and UID_CSTG_SERVER_PUBLIC_KEY
  • -
+

Something went wrong:

+
<%= error %>
+

Response from the <%- identityName %> operator:

+
<%= response ? JSON.stringify(response.data) : '' %>
+

HTTP error:

+
<%= response ? (response.status + ' ' + response.statusText) : '' %>
+

Back to the main page

From 0611f7d14a782c5b2564164cabe6be656d712bb0 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 12:33:50 -0700 Subject: [PATCH 05/19] simplify logging and functionality in server.js --- .../javascript-sdk/server-side/server.js | 65 ++++--------------- 1 file changed, 11 insertions(+), 54 deletions(-) diff --git a/web-integrations/javascript-sdk/server-side/server.js b/web-integrations/javascript-sdk/server-side/server.js index 8fba227..743d9f1 100644 --- a/web-integrations/javascript-sdk/server-side/server.js +++ b/web-integrations/javascript-sdk/server-side/server.js @@ -14,31 +14,22 @@ const path = require('path'); const app = express(); const port = process.env.PORT || 3034; -// UID2/EUID Configuration -// Note: Using UID_CLIENT_BASE_URL because we're using client-side SDK with CSTG credentials -// When running in Docker, use host.docker.internal instead of localhost -let uidBaseUrl = process.env.UID_CLIENT_BASE_URL; -if (uidBaseUrl && uidBaseUrl.includes('localhost')) { - uidBaseUrl = uidBaseUrl.replace('localhost', 'host.docker.internal'); - console.log(`Adjusted base URL for Docker: ${uidBaseUrl}`); -} +let uidBaseUrl = process.env.UID_SERVER_BASE_URL; const subscriptionId = process.env.UID_CSTG_SUBSCRIPTION_ID; const serverPublicKey = process.env.UID_CSTG_SERVER_PUBLIC_KEY; -const uidJsSdkUrl = process.env.UID_JS_SDK_URL || 'https://cdn.integ.uidapi.com/uid2-sdk-4.0.1.js'; -const uidJsSdkName = process.env.UID_JS_SDK_NAME || '__uid2'; +const uidJsSdkUrl = process.env.UID_JS_SDK_URL; +const uidJsSdkName = process.env.UID_JS_SDK_NAME; // UI/Display configuration const identityName = process.env.IDENTITY_NAME; const docsBaseUrl = process.env.DOCS_BASE_URL; -// Initialize UID2 JavaScript SDK in a simulated browser environment using jsdom +// Initialize UID JavaScript SDK in a simulated browser environment using jsdom // This demonstrates that the client-side SDK works in Node.js with jsdom let uid2Sdk = null; let dom = null; async function initializeSDK() { - console.log('Initializing UID2/EUID SDK in Node.js using jsdom...'); - const crypto = require('crypto'); // Create a virtual DOM environment @@ -70,40 +61,25 @@ async function initializeSDK() { dom.window.TextEncoder = util.TextEncoder; dom.window.TextDecoder = util.TextDecoder; - // Load the UID2 SDK script - // First, try to load from local file, otherwise fetch from CDN + // Load the UID2 SDK script from CDN try { - let sdkCode; - const localSdkPath = path.join(__dirname, '../../../uid2-web-integrations/dist/uid2-sdk-4.0.1.js'); - - if (fs.existsSync(localSdkPath)) { - console.log('Loading SDK from local file:', localSdkPath); - sdkCode = fs.readFileSync(localSdkPath, 'utf8'); - } else { - console.log('Loading SDK from CDN:', uidJsSdkUrl); - const axios = require('axios'); - const response = await axios.get(uidJsSdkUrl); - sdkCode = response.data; - } + const axios = require('axios'); + const response = await axios.get(uidJsSdkUrl); // Execute the SDK code in the jsdom context const scriptEl = dom.window.document.createElement('script'); - scriptEl.textContent = sdkCode; + scriptEl.textContent = response.data; dom.window.document.body.appendChild(scriptEl); - // Wait a bit for the SDK to initialize + // Wait for the SDK to initialize await new Promise(resolve => setTimeout(resolve, 100)); // Get reference to the SDK uid2Sdk = dom.window[uidJsSdkName]; - if (!uid2Sdk) { throw new Error(`SDK not found at window.${uidJsSdkName}`); } - console.log('✓ SDK loaded successfully'); - console.log(`✓ SDK available at window.${uidJsSdkName}`); - // Initialize the SDK uid2Sdk.init({ baseUrl: uidBaseUrl }); @@ -233,8 +209,6 @@ app.post('/login', async (req, res) => { // Add callback to capture the identity or optout const callbackHandler = (eventType, payload) => { - console.log(`SDK Event: ${eventType}`, payload?.identity ? 'Identity received' : 'No identity'); - // Handle successful identity generation if ((eventType === 'InitCompleted' || eventType === 'IdentityUpdated') && payload?.identity) { clearTimeout(timeout); @@ -254,7 +228,7 @@ app.post('/login', async (req, res) => { if (index > -1) { uid2Sdk.callbacks.splice(index, 1); } - reject(new Error('User has opted out of UID2')); + reject(new Error('Got unexpected token generate status: optout')); } }; @@ -277,8 +251,6 @@ app.post('/login', async (req, res) => { throw new Error('No identity returned from SDK'); } - console.log('✓ Token generated successfully'); - console.log('Identity:', JSON.stringify(identity, null, 2)); req.session.identity = identity; res.redirect('/'); @@ -304,22 +276,7 @@ app.get('/logout', (req, res) => { // Start server and initialize SDK initializeSDK().then(() => { app.listen(port, () => { - console.log(''); - console.log('='.repeat(70)); - console.log(`Server-Side ${identityName || 'UID2/EUID'} Integration Example using JavaScript SDK`); - console.log('='.repeat(70)); - console.log(`✓ Server listening at http://localhost:${port}`); - console.log(''); - console.log('Configuration:'); - console.log(` Base URL: ${uidBaseUrl}`); - console.log(` Subscription ID: ${subscriptionId || 'NOT SET'}`); - console.log(` Public Key: ${serverPublicKey ? serverPublicKey.substring(0, 30) + '...' : 'NOT SET'}`); - console.log(` SDK Name: ${uidJsSdkName}`); - console.log(''); - console.log('✓ JavaScript SDK initialized and ready!'); - console.log(' The browser-based SDK is now running in Node.js via jsdom.'); - console.log('='.repeat(70)); - console.log(''); + console.log(`Server listening at http://localhost:${port}`); }); }).catch(error => { console.error('Failed to start server:', error); From 6e330e0bfba9462ff1527c9dd1cc8538c4f5dadb Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 12:35:35 -0700 Subject: [PATCH 06/19] improve ui langauage and description of page --- web-integrations/javascript-sdk/server-side/views/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-integrations/javascript-sdk/server-side/views/index.html b/web-integrations/javascript-sdk/server-side/views/index.html index 92d649d..2104a9e 100644 --- a/web-integrations/javascript-sdk/server-side/views/index.html +++ b/web-integrations/javascript-sdk/server-side/views/index.html @@ -10,7 +10,7 @@

Server-Side <%- identityName %> Integration Example using JavaScript SDK

This example demonstrates how a content publisher can use <%- identityName %> services and the JavaScript SDK on the server to implement the - server-side <%- identityName %> integration workflow. This proves that the JavaScript SDK (designed for browsers) also works in a Node.js server environment. + server-side <%- identityName %> integration workflow. This showcases how the JavaScript SDK (designed for browsers) can also work in a Node.js server environment. [Source Code]

From 6b499a7f42fe486cd437ad0153fcb1f86489fcc4 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 12:57:05 -0700 Subject: [PATCH 07/19] update readme to be consistent with other sdk readmes --- .../javascript-sdk/server-side/README.md | 131 ++++-------------- 1 file changed, 24 insertions(+), 107 deletions(-) diff --git a/web-integrations/javascript-sdk/server-side/README.md b/web-integrations/javascript-sdk/server-side/README.md index 698e3ee..adedda1 100644 --- a/web-integrations/javascript-sdk/server-side/README.md +++ b/web-integrations/javascript-sdk/server-side/README.md @@ -1,34 +1,20 @@ # Server-Side UID2 or EUID Integration Example using JavaScript SDK -This example demonstrates how a content publisher can use either the UID2 or EUID services and the JavaScript SDK on the server side to implement the server-side integration workflow. +This example showcases how the UID2/EUID **JavaScript SDK works in Node.js** server environments. It uses the same `setIdentityFromEmail` method that runs in browsers, but executes it on the server. -- For UID2: [UID2 services](https://unifiedid.com/docs/intro), [server-side UID2 integration workflow](https://unifiedid.com/docs/guides/integration-publisher-server-side) -- For EUID: [EUID services](https://euid.eu/docs/intro), [server-side EUID integration workflow](https://euid.eu/docs/guides/integration-publisher-server-side) +For more information on the JavaScript SDK, refer to the [UID2 SDK for JavaScript](https://unifiedid.com/docs/sdks/sdk-ref-javascript) or [EUID SDK for JavaScript](https://euid.eu/docs/sdks/sdk-ref-javascript) documentation. -This example can be configured for either UID2 or EUID — the behavior is determined by your environment variable configuration. You cannot use both simultaneously. - -## Key Difference from Other Examples - -This example proves that the UID2/EUID **JavaScript SDK works in Node.js** server environments. It uses the same `setIdentityFromEmail` method that runs in browsers, but executes it on the server. - -**Important:** This uses **public credentials** (Subscription ID + Server Public Key) which are the same credentials used for client-side integrations. This demonstrates that the client-side SDK is fully compatible with Node.js. +> **Note:** This uses **public credentials** (Subscription ID + Server Public Key) which are the same credentials used for client-side integrations. -### Comparison with Other Examples - -| Example | Environment | Credentials | SDK Usage | Notes | -|---------|-------------|-------------|-----------|-------| -| [server-side](../../server-side/) | Server only | API Key + Secret | No SDK | Manual encryption/decryption | -| [client-server](../client-server/) | Hybrid | API Key + Secret (server)
None (client) | Client SDK only | Server generates, client maintains | -| [client-side](../client-side/) | Client only | Public Key + Subscription ID | Client SDK | Fully client-side | -| **This example** | **Server only** | **Public Key + Subscription ID** | **Client SDK on server** | **Proves SDK works in Node.js** | +This example can be configured for either UID2 or EUID — the behavior is determined by your environment variable configuration. You cannot use both simultaneously. -> **Note:** While the server side of the example application is implemented in JavaScript using Node.js, it is not a requirement. You can use any technology of your choice and refer to the example application for illustration of the functionality that needs to be implemented. +## Key Benefits -## Prerequisites +This example demonstrates the advantages of using the JavaScript SDK on the server: -- Node.js 20.x or higher -- UID2/EUID API Key and Client Secret (for server-side integration) -- Access to UID2 or EUID integration environment +- **Secure credential handling**: Public credentials (server public key and subscription ID) remain on the server and are not exposed to the browser +- **Simplified implementation**: The SDK handles the full token lifecycle including encryption, decryption, and refresh logic automatically +- **No manual cryptography**: Unlike traditional server-side integrations, there's no need to manually implement encryption/decryption processes ## Build and Run the Example Application @@ -63,20 +49,7 @@ docker build -f web-integrations/javascript-sdk/server-side/Dockerfile -t javasc docker run -it --rm -p 3034:3034 --env-file .env javascript-sdk-server-side ``` -### Using npm (Local Development) - -```bash -# Navigate to this directory -cd web-integrations/javascript-sdk/server-side - -# Install dependencies -npm install - -# Start the server (requires .env file in repository root) -npm start -``` - -## Configuration +## Environment Variables The following table lists the environment variables that you must specify to start the application. @@ -87,6 +60,8 @@ The following table lists the environment variables that you must specify to sta | `UID_SERVER_BASE_URL` | The base URL of the UID2/EUID service. For details, see [Environments](https://unifiedid.com/docs/getting-started/gs-environments) (UID2) or [Environments](https://euid.eu/docs/getting-started/gs-environments) (EUID). | UID2: `https://operator-integ.uidapi.com`
EUID: `https://integ.euid.eu/v2` | | `UID_CSTG_SUBSCRIPTION_ID` | Your UID2/EUID subscription ID for Client-Side Token Generation. **These are public credentials.** | Your assigned subscription ID (e.g., `DMr7uHxqLU`) | | `UID_CSTG_SERVER_PUBLIC_KEY` | Your UID2/EUID server public key for Client-Side Token Generation. **These are public credentials.** | Your assigned public key | +| `UID_JS_SDK_URL` | URL to the UID2/EUID JavaScript SDK | UID2: `https://cdn.integ.uidapi.com/uid2-sdk-4.0.1.js`
EUID: `https://cdn.integ.euid.eu/euid-sdk-4.0.1.js` | +| `UID_JS_SDK_NAME` | Global variable name for the SDK | UID2: `__uid2`
EUID: `__euid` | | `SESSION_KEY` | The key to encrypt session data stored in the application session cookie. This can be any arbitrary string. | Any secure random string | ### Display/UI Configuration @@ -99,88 +74,30 @@ The following table lists the environment variables that you must specify to sta After you see output similar to the following, the example application is up and running: ``` -Server-Side UID2/EUID Integration Example using JavaScript SDK -Listening at http://localhost:3034 - -Note: SDK integration is not yet complete. -TODO: Add UID2/EUID JavaScript SDK package and implement token generation/refresh. +Server listening at http://localhost:3034 ``` If needed, to close the application, terminate the docker container or use the `Ctrl+C` keyboard shortcut. ## Test the Example Application -The example application illustrates the steps documented in the server-side integration guides: -- UID2: [Server-Side Integration Guide](https://unifiedid.com/docs/guides/integration-publisher-server-side) -- EUID: [Server-Side Integration Guide](https://euid.eu/docs/guides/integration-publisher-server-side) - -**Note:** For API endpoint documentation, see the UID2 or EUID docs based on your configuration. - -The application provides three main pages: index (main), example content 1, and example content 2. Access to these pages is possible only after the user completes the login process. If the user is not logged in, they will be redirected to the login page. - -Submitting the login form simulates logging in to a publisher's application in the real world. Normally the login would require checking the user's secure credentials (for example, a password), but for demonstration purposes this step is omitted, and the login process focuses on integration with the UID2/EUID services using the JavaScript SDK on the server. - The following table outlines and annotates the steps you may take to test and explore the example application. | Step | Description | Comments | |:----:|:------------|:---------| -| 1 | In your browser, navigate to the application main page at `http://localhost:3034`. | The displayed main (index) page of the example application provides a [login form](views/login.html) for the user to complete the UID2/EUID login process.
IMPORTANT: A real-life application must also display a form for the user to express their consent to targeted advertising. | +| 1 | In your browser, navigate to the application main page at `http://localhost:3034`. | The displayed main (index) page provides a login form for the user to complete the UID2/EUID login process.
IMPORTANT: A real-life application must also display a form for the user to express their consent to targeted advertising. | | 2 | Enter the user email address that you want to use for testing and click **Log In**. | This is a call to the `/login` endpoint ([server.js](server.js)). The login initiated on the server side uses the JavaScript SDK's `setIdentityFromEmail` method to generate a token and processes the received response. The SDK handles all encryption/decryption automatically, just as it does in the browser. | -| | The main page is updated to display links to the two pages with protected content and the established identity information. | The displayed identity information is the `body` property of the response from the SDK's `setIdentityFromEmail` call. If the response is successful, the returned identity is saved to a session cookie (a real-world application would use a different way to store session data) and the protected index page is rendered. | -| 3 | Click either of the two sample content pages. | When the user requests the index or content pages, the server reads the user session and extracts the current identity ([server.js](server.js)). The `advertising_token` on the identity can be used for targeted advertising. | -| 4 | Click the **Back to the main page** link. | Note that the identity contains several timestamps that determine when the advertising token becomes invalid (`identity_expires`) and when the server should attempt to refresh it (`refresh_from`). Every time a protected page is requested, the `verifyIdentity` function ([server.js](server.js)) uses the SDK to refresh the token as needed.
The user is automatically logged out in the following cases:
- If the identity expires without being refreshed and refresh attempt fails.
- If the refresh token expires.
- If the refresh attempt indicates that the user has opted out. | -| 5 | To exit the application, click **Log Out**. | This calls the `/logout` endpoint on the server ([server.js](server.js)), which clears the session and the first-party cookie and presents the user with the login form again.
NOTE: The page displays the **Log Out** button as long as the user is logged in. | - -## Implementation Status - -> **⚠️ Important:** This example is currently in initial development. The JavaScript SDK integration is not yet complete. - -### TODO - -- [ ] Identify the correct way to import/use the JavaScript SDK in Node.js (it's designed for browsers but should work with proper setup) -- [ ] Implement `setIdentityFromEmail` call in the `/login` endpoint using public credentials -- [ ] Handle SDK callbacks/promises properly in the Node.js environment -- [ ] Add error handling for SDK operations -- [ ] Test that the generated identity matches what the client-side example produces -- [ ] Update documentation with actual SDK API usage once working -- [ ] Add to docker-compose.yml in repository root - -## Development Notes - -This example demonstrates that the browser-based JavaScript SDK can run in Node.js. It uses the same public credentials and methods as the client-side example: - -**Client-Side (Browser):** -```javascript -// In browser -const sdk = window.__uid2; -await sdk.setIdentityFromEmail(email, { - subscriptionId: 'DMr7uHxqLU', - serverPublicKey: 'UID2-X-I-MFk...' -}); -``` - -**This Example (Node.js Server):** -```javascript -// Same code, but running on Node.js server -const sdk = getUid2Sdk(); // Need to figure out proper import -await sdk.setIdentityFromEmail(email, { - subscriptionId: process.env.UID_CSTG_SUBSCRIPTION_ID, - serverPublicKey: process.env.UID_CSTG_SERVER_PUBLIC_KEY -}); -``` - -The key challenge is figuring out how to properly import/initialize the SDK in a Node.js environment since it's primarily designed for browsers. - -## Contributing +| | The main page is updated to display the established identity information. | The displayed identity information is the `body` property of the response from the SDK's `setIdentityFromEmail` call. If the response is successful, the returned identity is saved to a session cookie (a real-world application would use a different way to store session data) and the protected index page is rendered. | +| 3 | Review the displayed identity information. | The server reads the user session and extracts the current identity ([server.js](server.js)). The `advertising_token` on the identity can be used for targeted advertising. Note that the identity contains several timestamps that determine when the advertising token becomes invalid (`identity_expires`) and when the server should attempt to refresh it (`refresh_from`). The `verifyIdentity` function ([server.js](server.js)) uses the SDK to refresh the token as needed.
The user is automatically logged out in the following cases:
- If the identity expires without being refreshed and refresh attempt fails.
- If the refresh token expires.
- If the refresh attempt indicates that the user has opted out. | +| 4 | To exit the application, click **Log Out**. | This calls the `/logout` endpoint on the server ([server.js](server.js)), which clears the session and presents the user with the login form again.
NOTE: The page displays the **Log Out** button as long as the user is logged in. | -When implementing the SDK integration, ensure: +## How This Implementation Works -1. The SDK is properly initialized with API key and client secret -2. Error handling matches the existing server-side example behavior -3. Session management remains unchanged -4. The user experience is identical to the manual crypto version -5. All environment variables are properly documented +Unlike the browser where the SDK runs natively in the DOM, this example uses **jsdom** to simulate a browser environment within Node.js: -## License +1. **Creates a virtual DOM**: Uses jsdom to provide `window`, `document`, and `navigator` objects that the SDK expects +2. **Polyfills browser APIs**: Adds Node.js equivalents for Web Crypto API (`crypto.subtle`) and text encoding APIs (`TextEncoder`/`TextDecoder`) +3. **Loads the SDK**: Fetches and executes the browser-based SDK code within the simulated environment +4. **Runs SDK methods**: Calls `setIdentityFromEmail` just like in a browser, with the same public credentials -This project is licensed under the Apache License 2.0 - see the LICENSE file for details. +This demonstrates that the client-side SDK is can be compatible with server-side Node.js environments when given the proper browser-like context. From 986f3be1cfc21fdd6f808ed328f9397f133ac1d5 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 13:00:32 -0700 Subject: [PATCH 08/19] update defintion of env variable in readme --- web-integrations/javascript-sdk/server-side/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-integrations/javascript-sdk/server-side/README.md b/web-integrations/javascript-sdk/server-side/README.md index adedda1..a2047dd 100644 --- a/web-integrations/javascript-sdk/server-side/README.md +++ b/web-integrations/javascript-sdk/server-side/README.md @@ -62,7 +62,7 @@ The following table lists the environment variables that you must specify to sta | `UID_CSTG_SERVER_PUBLIC_KEY` | Your UID2/EUID server public key for Client-Side Token Generation. **These are public credentials.** | Your assigned public key | | `UID_JS_SDK_URL` | URL to the UID2/EUID JavaScript SDK | UID2: `https://cdn.integ.uidapi.com/uid2-sdk-4.0.1.js`
EUID: `https://cdn.integ.euid.eu/euid-sdk-4.0.1.js` | | `UID_JS_SDK_NAME` | Global variable name for the SDK | UID2: `__uid2`
EUID: `__euid` | -| `SESSION_KEY` | The key to encrypt session data stored in the application session cookie. This can be any arbitrary string. | Any secure random string | +| `SESSION_KEY` | Used by the cookie-session middleware to encrypt the session data stored in cookies. | Any secure random string | ### Display/UI Configuration From 2208be0d31a7e19a62624b47a23203b206260f08 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 13:35:11 -0700 Subject: [PATCH 09/19] fix optout bug by clearing session and clean up logs --- .../javascript-sdk/server-side/server.js | 93 ++----------------- 1 file changed, 8 insertions(+), 85 deletions(-) diff --git a/web-integrations/javascript-sdk/server-side/server.js b/web-integrations/javascript-sdk/server-side/server.js index 743d9f1..fb5bdf4 100644 --- a/web-integrations/javascript-sdk/server-side/server.js +++ b/web-integrations/javascript-sdk/server-side/server.js @@ -104,72 +104,6 @@ app.set('view engine', 'html'); app.use(nocache()); -/** - * Check if an identity is still valid and refreshable - */ -function isRefreshableIdentity(identity) { - if (!identity || typeof identity !== 'object') { - return false; - } - if (!identity.refresh_expires || Date.now() >= identity.refresh_expires) { - return false; - } - return !!identity.refresh_token; -} - -/** - * Refresh an identity token using the SDK - * The SDK will automatically refresh tokens when initialized with an existing identity - */ -async function refreshIdentity(identity) { - // TODO: Use JS SDK to refresh identity - // The SDK's init() with an existing identity will handle refresh automatically - // Example: - // const sdk = getUid2Sdk(); - // return new Promise((resolve) => { - // sdk.init({ - // baseUrl: uidBaseUrl, - // identity: identity - // }); - // sdk.callbacks.push((eventType, payload) => { - // if (eventType === 'IdentityUpdated') { - // resolve(payload.identity); - // } - // }); - // }); - - console.log('TODO: Implement SDK-based identity refresh'); - return identity; -} - -/** - * Verify and refresh identity if needed - */ -async function verifyIdentity(req) { - if (!isRefreshableIdentity(req.session.identity)) { - return false; - } - - // Check if identity needs refresh - if (Date.now() >= req.session.identity.refresh_from || Date.now() >= req.session.identity.identity_expires) { - req.session.identity = await refreshIdentity(req.session.identity); - } - - return !!req.session.identity; -} - -/** - * Middleware to protect routes that require authentication - */ -async function protect(req, res, next) { - if (await verifyIdentity(req)) { - next(); - } else { - req.session = null; - res.redirect('/login'); - } -} - // Routes /** @@ -198,43 +132,28 @@ app.post('/login', async (req, res) => { } try { - console.log(`Generating token for email: ${req.body.email}`); - - // Use the SDK's setIdentityFromEmail method - // This is the same method used in browser environments + // Call the SDK's setIdentityFromEmail method and wait for the result via callback const identity = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Token generation timed out after 10 seconds')); }, 10000); - // Add callback to capture the identity or optout const callbackHandler = (eventType, payload) => { - // Handle successful identity generation if ((eventType === 'InitCompleted' || eventType === 'IdentityUpdated') && payload?.identity) { clearTimeout(timeout); - // Remove this specific callback - const index = uid2Sdk.callbacks.indexOf(callbackHandler); - if (index > -1) { - uid2Sdk.callbacks.splice(index, 1); - } + uid2Sdk.callbacks.splice(uid2Sdk.callbacks.indexOf(callbackHandler), 1); resolve(payload.identity); } - // Handle optout - user has opted out of UID2 if (eventType === 'OptoutReceived') { clearTimeout(timeout); - // Remove this specific callback - const index = uid2Sdk.callbacks.indexOf(callbackHandler); - if (index > -1) { - uid2Sdk.callbacks.splice(index, 1); - } + uid2Sdk.callbacks.splice(uid2Sdk.callbacks.indexOf(callbackHandler), 1); reject(new Error('Got unexpected token generate status: optout')); } }; uid2Sdk.callbacks.push(callbackHandler); - // Call setIdentityFromEmail uid2Sdk.setIdentityFromEmail( req.body.email, { @@ -255,7 +174,8 @@ app.post('/login', async (req, res) => { res.redirect('/'); } catch (error) { - console.error('Token generation failed:', error); + console.error('Token generation failed:', error.message); + req.session = null; res.render('error', { error: error.message || error.toString(), response: error.response || null, @@ -269,6 +189,9 @@ app.post('/login', async (req, res) => { * Logout endpoint - clears session and returns to main page */ app.get('/logout', (req, res) => { + if (uid2Sdk && uid2Sdk.disconnect) { + uid2Sdk.disconnect(); + } req.session = null; res.redirect('/'); }); From 9f5f9bf2bcf285734a010c4759e9b928cae1beed Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 13:42:40 -0700 Subject: [PATCH 10/19] more comment clean up --- web-integrations/javascript-sdk/server-side/server.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/web-integrations/javascript-sdk/server-side/server.js b/web-integrations/javascript-sdk/server-side/server.js index fb5bdf4..7a0afc0 100644 --- a/web-integrations/javascript-sdk/server-side/server.js +++ b/web-integrations/javascript-sdk/server-side/server.js @@ -8,8 +8,9 @@ const ejs = require('ejs'); const express = require('express'); const nocache = require('nocache'); const { JSDOM } = require('jsdom'); -const fs = require('fs'); -const path = require('path'); +const axios = require('axios'); +const crypto = require('crypto'); +const util = require('util'); const app = express(); const port = process.env.PORT || 3034; @@ -30,8 +31,6 @@ let uid2Sdk = null; let dom = null; async function initializeSDK() { - const crypto = require('crypto'); - // Create a virtual DOM environment dom = new JSDOM('', { url: 'http://localhost', @@ -55,7 +54,6 @@ async function initializeSDK() { }); // Polyfill TextEncoder and TextDecoder (required by the SDK) - const util = require('util'); global.TextEncoder = util.TextEncoder; global.TextDecoder = util.TextDecoder; dom.window.TextEncoder = util.TextEncoder; @@ -63,7 +61,6 @@ async function initializeSDK() { // Load the UID2 SDK script from CDN try { - const axios = require('axios'); const response = await axios.get(uidJsSdkUrl); // Execute the SDK code in the jsdom context From 0ee4b54a1b02766030afe4df8e82a38231f05017 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 13:50:16 -0700 Subject: [PATCH 11/19] replace uid2 references with generic naming --- .../javascript-sdk/server-side/server.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/web-integrations/javascript-sdk/server-side/server.js b/web-integrations/javascript-sdk/server-side/server.js index 7a0afc0..fc4f5f8 100644 --- a/web-integrations/javascript-sdk/server-side/server.js +++ b/web-integrations/javascript-sdk/server-side/server.js @@ -27,7 +27,7 @@ const docsBaseUrl = process.env.DOCS_BASE_URL; // Initialize UID JavaScript SDK in a simulated browser environment using jsdom // This demonstrates that the client-side SDK works in Node.js with jsdom -let uid2Sdk = null; +let uidSdk = null; let dom = null; async function initializeSDK() { @@ -59,7 +59,7 @@ async function initializeSDK() { dom.window.TextEncoder = util.TextEncoder; dom.window.TextDecoder = util.TextDecoder; - // Load the UID2 SDK script from CDN + // Load the UID SDK script from CDN try { const response = await axios.get(uidJsSdkUrl); @@ -72,15 +72,15 @@ async function initializeSDK() { await new Promise(resolve => setTimeout(resolve, 100)); // Get reference to the SDK - uid2Sdk = dom.window[uidJsSdkName]; - if (!uid2Sdk) { + uidSdk = dom.window[uidJsSdkName]; + if (!uidSdk) { throw new Error(`SDK not found at window.${uidJsSdkName}`); } // Initialize the SDK - uid2Sdk.init({ baseUrl: uidBaseUrl }); + uidSdk.init({ baseUrl: uidBaseUrl }); - return uid2Sdk; + return uidSdk; } catch (error) { console.error('Failed to initialize SDK:', error); throw error; @@ -119,7 +119,7 @@ app.get('/', (req, res) => { * Uses the JavaScript SDK's setIdentityFromEmail method on the server */ app.post('/login', async (req, res) => { - if (!uid2Sdk) { + if (!uidSdk) { return res.render('error', { error: 'SDK not initialized. Server may still be starting up.', response: null, @@ -138,20 +138,20 @@ app.post('/login', async (req, res) => { const callbackHandler = (eventType, payload) => { if ((eventType === 'InitCompleted' || eventType === 'IdentityUpdated') && payload?.identity) { clearTimeout(timeout); - uid2Sdk.callbacks.splice(uid2Sdk.callbacks.indexOf(callbackHandler), 1); + uidSdk.callbacks.splice(uidSdk.callbacks.indexOf(callbackHandler), 1); resolve(payload.identity); } if (eventType === 'OptoutReceived') { clearTimeout(timeout); - uid2Sdk.callbacks.splice(uid2Sdk.callbacks.indexOf(callbackHandler), 1); + uidSdk.callbacks.splice(uidSdk.callbacks.indexOf(callbackHandler), 1); reject(new Error('Got unexpected token generate status: optout')); } }; - uid2Sdk.callbacks.push(callbackHandler); + uidSdk.callbacks.push(callbackHandler); - uid2Sdk.setIdentityFromEmail( + uidSdk.setIdentityFromEmail( req.body.email, { subscriptionId: subscriptionId, @@ -186,8 +186,8 @@ app.post('/login', async (req, res) => { * Logout endpoint - clears session and returns to main page */ app.get('/logout', (req, res) => { - if (uid2Sdk && uid2Sdk.disconnect) { - uid2Sdk.disconnect(); + if (uidSdk && uidSdk.disconnect) { + uidSdk.disconnect(); } req.session = null; res.redirect('/'); From 1fbaa6ff416796fbdc7935222688a3001a562c87 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Mon, 10 Nov 2025 13:57:21 -0700 Subject: [PATCH 12/19] simplify error page to reflect new sdk responses --- web-integrations/javascript-sdk/server-side/views/error.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web-integrations/javascript-sdk/server-side/views/error.html b/web-integrations/javascript-sdk/server-side/views/error.html index d1e1f6f..eb744be 100644 --- a/web-integrations/javascript-sdk/server-side/views/error.html +++ b/web-integrations/javascript-sdk/server-side/views/error.html @@ -16,10 +16,6 @@

Server-Side <%- identityName %> Integration Example using JavaScript SDK

Something went wrong:

<%= error %>
-

Response from the <%- identityName %> operator:

-
<%= response ? JSON.stringify(response.data) : '' %>
-

HTTP error:

-
<%= response ? (response.status + ' ' + response.statusText) : '' %>

Back to the main page

From 8c6e15440fb366cb50c74c31a873f97a97f08d61 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Tue, 11 Nov 2025 11:08:03 -0700 Subject: [PATCH 13/19] rename folder and reorganize readme --- .../.gitignore | 0 .../Dockerfile | 0 .../README.md | 26 +++++++++--------- .../package.json | 0 .../public/images/favicon.png | Bin .../public/stylesheets/app.css | 0 .../server.js | 0 .../views/error.html | 0 .../views/index.html | 0 9 files changed, 13 insertions(+), 13 deletions(-) rename web-integrations/javascript-sdk/{server-side => server-side-node}/.gitignore (100%) rename web-integrations/javascript-sdk/{server-side => server-side-node}/Dockerfile (100%) rename web-integrations/javascript-sdk/{server-side => server-side-node}/README.md (97%) rename web-integrations/javascript-sdk/{server-side => server-side-node}/package.json (100%) rename web-integrations/javascript-sdk/{server-side => server-side-node}/public/images/favicon.png (100%) rename web-integrations/javascript-sdk/{server-side => server-side-node}/public/stylesheets/app.css (100%) rename web-integrations/javascript-sdk/{server-side => server-side-node}/server.js (100%) rename web-integrations/javascript-sdk/{server-side => server-side-node}/views/error.html (100%) rename web-integrations/javascript-sdk/{server-side => server-side-node}/views/index.html (100%) diff --git a/web-integrations/javascript-sdk/server-side/.gitignore b/web-integrations/javascript-sdk/server-side-node/.gitignore similarity index 100% rename from web-integrations/javascript-sdk/server-side/.gitignore rename to web-integrations/javascript-sdk/server-side-node/.gitignore diff --git a/web-integrations/javascript-sdk/server-side/Dockerfile b/web-integrations/javascript-sdk/server-side-node/Dockerfile similarity index 100% rename from web-integrations/javascript-sdk/server-side/Dockerfile rename to web-integrations/javascript-sdk/server-side-node/Dockerfile diff --git a/web-integrations/javascript-sdk/server-side/README.md b/web-integrations/javascript-sdk/server-side-node/README.md similarity index 97% rename from web-integrations/javascript-sdk/server-side/README.md rename to web-integrations/javascript-sdk/server-side-node/README.md index a2047dd..b90fd08 100644 --- a/web-integrations/javascript-sdk/server-side/README.md +++ b/web-integrations/javascript-sdk/server-side-node/README.md @@ -8,13 +8,16 @@ For more information on the JavaScript SDK, refer to the [UID2 SDK for JavaScrip This example can be configured for either UID2 or EUID — the behavior is determined by your environment variable configuration. You cannot use both simultaneously. -## Key Benefits +## How This Implementation Works -This example demonstrates the advantages of using the JavaScript SDK on the server: +Unlike the browser where the SDK runs natively in the DOM, this example uses **jsdom** to simulate a browser environment within Node.js: -- **Secure credential handling**: Public credentials (server public key and subscription ID) remain on the server and are not exposed to the browser -- **Simplified implementation**: The SDK handles the full token lifecycle including encryption, decryption, and refresh logic automatically -- **No manual cryptography**: Unlike traditional server-side integrations, there's no need to manually implement encryption/decryption processes +1. **Creates a virtual DOM**: Uses jsdom to provide `window`, `document`, and `navigator` objects that the SDK expects +2. **Polyfills browser APIs**: Adds Node.js equivalents for Web Crypto API (`crypto.subtle`) and text encoding APIs (`TextEncoder`/`TextDecoder`) +3. **Loads the SDK**: Fetches and executes the browser-based SDK code within the simulated environment +4. **Runs SDK methods**: Calls `setIdentityFromEmail` just like in a browser, with the same public credentials + +This demonstrates that the client-side SDK can be compatible with server-side Node.js environments when given the proper browser-like context. ## Build and Run the Example Application @@ -91,13 +94,10 @@ The following table outlines and annotates the steps you may take to test and ex | 3 | Review the displayed identity information. | The server reads the user session and extracts the current identity ([server.js](server.js)). The `advertising_token` on the identity can be used for targeted advertising. Note that the identity contains several timestamps that determine when the advertising token becomes invalid (`identity_expires`) and when the server should attempt to refresh it (`refresh_from`). The `verifyIdentity` function ([server.js](server.js)) uses the SDK to refresh the token as needed.
The user is automatically logged out in the following cases:
- If the identity expires without being refreshed and refresh attempt fails.
- If the refresh token expires.
- If the refresh attempt indicates that the user has opted out. | | 4 | To exit the application, click **Log Out**. | This calls the `/logout` endpoint on the server ([server.js](server.js)), which clears the session and presents the user with the login form again.
NOTE: The page displays the **Log Out** button as long as the user is logged in. | -## How This Implementation Works - -Unlike the browser where the SDK runs natively in the DOM, this example uses **jsdom** to simulate a browser environment within Node.js: +## Key Benefits -1. **Creates a virtual DOM**: Uses jsdom to provide `window`, `document`, and `navigator` objects that the SDK expects -2. **Polyfills browser APIs**: Adds Node.js equivalents for Web Crypto API (`crypto.subtle`) and text encoding APIs (`TextEncoder`/`TextDecoder`) -3. **Loads the SDK**: Fetches and executes the browser-based SDK code within the simulated environment -4. **Runs SDK methods**: Calls `setIdentityFromEmail` just like in a browser, with the same public credentials +This example demonstrates the advantages of using the JavaScript SDK on the server: -This demonstrates that the client-side SDK is can be compatible with server-side Node.js environments when given the proper browser-like context. +- **Secure credential handling**: Public credentials (server public key and subscription ID) remain on the server and are not exposed to the browser +- **Simplified implementation**: The SDK handles the full token lifecycle including encryption, decryption, and refresh logic automatically +- **No manual cryptography**: Unlike traditional server-side integrations, there's no need to manually implement encryption/decryption processes diff --git a/web-integrations/javascript-sdk/server-side/package.json b/web-integrations/javascript-sdk/server-side-node/package.json similarity index 100% rename from web-integrations/javascript-sdk/server-side/package.json rename to web-integrations/javascript-sdk/server-side-node/package.json diff --git a/web-integrations/javascript-sdk/server-side/public/images/favicon.png b/web-integrations/javascript-sdk/server-side-node/public/images/favicon.png similarity index 100% rename from web-integrations/javascript-sdk/server-side/public/images/favicon.png rename to web-integrations/javascript-sdk/server-side-node/public/images/favicon.png diff --git a/web-integrations/javascript-sdk/server-side/public/stylesheets/app.css b/web-integrations/javascript-sdk/server-side-node/public/stylesheets/app.css similarity index 100% rename from web-integrations/javascript-sdk/server-side/public/stylesheets/app.css rename to web-integrations/javascript-sdk/server-side-node/public/stylesheets/app.css diff --git a/web-integrations/javascript-sdk/server-side/server.js b/web-integrations/javascript-sdk/server-side-node/server.js similarity index 100% rename from web-integrations/javascript-sdk/server-side/server.js rename to web-integrations/javascript-sdk/server-side-node/server.js diff --git a/web-integrations/javascript-sdk/server-side/views/error.html b/web-integrations/javascript-sdk/server-side-node/views/error.html similarity index 100% rename from web-integrations/javascript-sdk/server-side/views/error.html rename to web-integrations/javascript-sdk/server-side-node/views/error.html diff --git a/web-integrations/javascript-sdk/server-side/views/index.html b/web-integrations/javascript-sdk/server-side-node/views/index.html similarity index 100% rename from web-integrations/javascript-sdk/server-side/views/index.html rename to web-integrations/javascript-sdk/server-side-node/views/index.html From f1ca4b67e450a97229f419939c6dad5ceec9a62c Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Tue, 11 Nov 2025 11:09:57 -0700 Subject: [PATCH 14/19] more readme clean up --- web-integrations/javascript-sdk/server-side-node/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web-integrations/javascript-sdk/server-side-node/README.md b/web-integrations/javascript-sdk/server-side-node/README.md index b90fd08..48fb160 100644 --- a/web-integrations/javascript-sdk/server-side-node/README.md +++ b/web-integrations/javascript-sdk/server-side-node/README.md @@ -1,12 +1,10 @@ # Server-Side UID2 or EUID Integration Example using JavaScript SDK -This example showcases how the UID2/EUID **JavaScript SDK works in Node.js** server environments. It uses the same `setIdentityFromEmail` method that runs in browsers, but executes it on the server. +This example showcases how the UID2/EUID **JavaScript SDK works in Node.js** server environments. It uses the same `setIdentityFromEmail` method that runs in browsers, but executes it on the server. This uses **public credentials** (Subscription ID + Server Public Key) which are the same credentials used for client-side integrations. For more information on the JavaScript SDK, refer to the [UID2 SDK for JavaScript](https://unifiedid.com/docs/sdks/sdk-ref-javascript) or [EUID SDK for JavaScript](https://euid.eu/docs/sdks/sdk-ref-javascript) documentation. -> **Note:** This uses **public credentials** (Subscription ID + Server Public Key) which are the same credentials used for client-side integrations. - -This example can be configured for either UID2 or EUID — the behavior is determined by your environment variable configuration. You cannot use both simultaneously. +> **Note:** This example can be configured for either UID2 or EUID — the behavior is determined by your environment variable configuration. You cannot use both simultaneously. ## How This Implementation Works From 4b2fb162d39df0263c13a2444cd75ce3034c8606 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Tue, 11 Nov 2025 11:59:15 -0700 Subject: [PATCH 15/19] import npm package instead of script --- docker-compose.yml | 2 +- .../server-side-node/Dockerfile | 8 +-- .../javascript-sdk/server-side-node/README.md | 13 ++-- .../server-side-node/package.json | 6 +- .../javascript-sdk/server-side-node/server.js | 68 ++++++++++++------- 5 files changed, 59 insertions(+), 38 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3234531..fdf740f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,7 +23,7 @@ services: javascript-sdk-server-side: build: context: . - dockerfile: web-integrations/javascript-sdk/server-side/Dockerfile + dockerfile: web-integrations/javascript-sdk/server-side-node/Dockerfile ports: - "3034:3034" container_name: javascript-sdk-server-side diff --git a/web-integrations/javascript-sdk/server-side-node/Dockerfile b/web-integrations/javascript-sdk/server-side-node/Dockerfile index b0e8811..fcb0d80 100644 --- a/web-integrations/javascript-sdk/server-side-node/Dockerfile +++ b/web-integrations/javascript-sdk/server-side-node/Dockerfile @@ -3,13 +3,13 @@ FROM node:20.11.0-alpine3.18 WORKDIR /usr/src/app # Copy package files first for better caching -COPY web-integrations/javascript-sdk/server-side/package*.json ./ +COPY web-integrations/javascript-sdk/server-side-node/package*.json ./ RUN npm install # Copy application files -COPY web-integrations/javascript-sdk/server-side/server.js ./ -COPY web-integrations/javascript-sdk/server-side/public ./public/ -COPY web-integrations/javascript-sdk/server-side/views ./views/ +COPY web-integrations/javascript-sdk/server-side-node/server.js ./ +COPY web-integrations/javascript-sdk/server-side-node/public ./public/ +COPY web-integrations/javascript-sdk/server-side-node/views ./views/ ENV PORT=3034 EXPOSE 3034 diff --git a/web-integrations/javascript-sdk/server-side-node/README.md b/web-integrations/javascript-sdk/server-side-node/README.md index 48fb160..b4563ae 100644 --- a/web-integrations/javascript-sdk/server-side-node/README.md +++ b/web-integrations/javascript-sdk/server-side-node/README.md @@ -10,10 +10,11 @@ For more information on the JavaScript SDK, refer to the [UID2 SDK for JavaScrip Unlike the browser where the SDK runs natively in the DOM, this example uses **jsdom** to simulate a browser environment within Node.js: -1. **Creates a virtual DOM**: Uses jsdom to provide `window`, `document`, and `navigator` objects that the SDK expects -2. **Polyfills browser APIs**: Adds Node.js equivalents for Web Crypto API (`crypto.subtle`) and text encoding APIs (`TextEncoder`/`TextDecoder`) -3. **Loads the SDK**: Fetches and executes the browser-based SDK code within the simulated environment -4. **Runs SDK methods**: Calls `setIdentityFromEmail` just like in a browser, with the same public credentials +1. **Imports the SDK**: Uses npm packages `@uid2/uid2-sdk` or `@unified-id/euid-sdk` (selected dynamically based on `IDENTITY_NAME`) +2. **Creates a virtual DOM**: Uses jsdom to provide `window`, `document`, and `navigator` objects that the SDK expects +3. **Polyfills browser APIs**: Adds Node.js equivalents for Web Crypto API (`crypto.subtle`) and text encoding APIs (`TextEncoder`/`TextDecoder`) +4. **Instantiates the SDK**: Creates a new instance of `UID2` or `EUID` class +5. **Runs SDK methods**: Calls `setIdentityFromEmail` just like in a browser, with the same public credentials This demonstrates that the client-side SDK can be compatible with server-side Node.js environments when given the proper browser-like context. @@ -61,10 +62,10 @@ The following table lists the environment variables that you must specify to sta | `UID_SERVER_BASE_URL` | The base URL of the UID2/EUID service. For details, see [Environments](https://unifiedid.com/docs/getting-started/gs-environments) (UID2) or [Environments](https://euid.eu/docs/getting-started/gs-environments) (EUID). | UID2: `https://operator-integ.uidapi.com`
EUID: `https://integ.euid.eu/v2` | | `UID_CSTG_SUBSCRIPTION_ID` | Your UID2/EUID subscription ID for Client-Side Token Generation. **These are public credentials.** | Your assigned subscription ID (e.g., `DMr7uHxqLU`) | | `UID_CSTG_SERVER_PUBLIC_KEY` | Your UID2/EUID server public key for Client-Side Token Generation. **These are public credentials.** | Your assigned public key | -| `UID_JS_SDK_URL` | URL to the UID2/EUID JavaScript SDK | UID2: `https://cdn.integ.uidapi.com/uid2-sdk-4.0.1.js`
EUID: `https://cdn.integ.euid.eu/euid-sdk-4.0.1.js` | -| `UID_JS_SDK_NAME` | Global variable name for the SDK | UID2: `__uid2`
EUID: `__euid` | | `SESSION_KEY` | Used by the cookie-session middleware to encrypt the session data stored in cookies. | Any secure random string | +> **⚠️ Important**: Your CSTG subscription must be configured with `http://localhost:3034` as an allowed origin. Contact your UID2/EUID representative to add this origin to your subscription's allowed origins list. + ### Display/UI Configuration | Variable | Description | Example Values | diff --git a/web-integrations/javascript-sdk/server-side-node/package.json b/web-integrations/javascript-sdk/server-side-node/package.json index 1dd4697..fee0603 100644 --- a/web-integrations/javascript-sdk/server-side-node/package.json +++ b/web-integrations/javascript-sdk/server-side-node/package.json @@ -16,13 +16,15 @@ "author": "", "license": "Apache-2.0", "dependencies": { - "axios": "^1.6.0", + "@uid2/uid2-sdk": "^4.0.1", + "@unified-id/euid-sdk": "^4.0.1", "cookie-session": "^2.0.0", "dotenv": "^16.0.3", "ejs": "^3.1.9", "express": "^4.18.2", "jsdom": "^23.0.0", - "nocache": "^4.0.0" + "nocache": "^4.0.0", + "xhr2": "^0.2.1" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/web-integrations/javascript-sdk/server-side-node/server.js b/web-integrations/javascript-sdk/server-side-node/server.js index fc4f5f8..139c6a7 100644 --- a/web-integrations/javascript-sdk/server-side-node/server.js +++ b/web-integrations/javascript-sdk/server-side-node/server.js @@ -8,9 +8,9 @@ const ejs = require('ejs'); const express = require('express'); const nocache = require('nocache'); const { JSDOM } = require('jsdom'); -const axios = require('axios'); const crypto = require('crypto'); const util = require('util'); +const XMLHttpRequest = require('xhr2'); const app = express(); const port = process.env.PORT || 3034; @@ -18,66 +18,84 @@ const port = process.env.PORT || 3034; let uidBaseUrl = process.env.UID_SERVER_BASE_URL; const subscriptionId = process.env.UID_CSTG_SUBSCRIPTION_ID; const serverPublicKey = process.env.UID_CSTG_SERVER_PUBLIC_KEY; -const uidJsSdkUrl = process.env.UID_JS_SDK_URL; -const uidJsSdkName = process.env.UID_JS_SDK_NAME; // UI/Display configuration const identityName = process.env.IDENTITY_NAME; const docsBaseUrl = process.env.DOCS_BASE_URL; +// SDK will be loaded dynamically (it's an ES module) +let SdkClass = null; + // Initialize UID JavaScript SDK in a simulated browser environment using jsdom // This demonstrates that the client-side SDK works in Node.js with jsdom let uidSdk = null; let dom = null; async function initializeSDK() { - // Create a virtual DOM environment + // Create a virtual DOM environment for the SDK to run in + // NOTE: The origin 'http://localhost:3034' must be added to your CSTG subscription's + // allowed origins list by your UID2/EUID representative dom = new JSDOM('', { - url: 'http://localhost', + url: 'http://localhost:3034', runScripts: 'dangerously', resources: 'usable', pretendToBeVisual: true, }); - // Set global variables for the SDK + // Set global variables for the SDK (SDK expects browser globals) global.window = dom.window; global.document = dom.window.document; global.navigator = dom.window.navigator; global.localStorage = dom.window.localStorage; - // Polyfill Web Crypto API for jsdom - // The SDK needs window.crypto.subtle for encryption + // Polyfill Web Crypto API for jsdom (SDK uses crypto.subtle for encryption) Object.defineProperty(dom.window, 'crypto', { value: crypto.webcrypto, writable: false, configurable: true }); - // Polyfill TextEncoder and TextDecoder (required by the SDK) + // Polyfill TextEncoder and TextDecoder (required by SDK for string/byte conversion) global.TextEncoder = util.TextEncoder; global.TextDecoder = util.TextDecoder; dom.window.TextEncoder = util.TextEncoder; dom.window.TextDecoder = util.TextDecoder; - // Load the UID SDK script from CDN - try { - const response = await axios.get(uidJsSdkUrl); - - // Execute the SDK code in the jsdom context - const scriptEl = dom.window.document.createElement('script'); - scriptEl.textContent = response.data; - dom.window.document.body.appendChild(scriptEl); - - // Wait for the SDK to initialize - await new Promise(resolve => setTimeout(resolve, 100)); + // Polyfill XMLHttpRequest with Origin header support + // The SDK needs the Origin header to be set for CSTG validation + const OriginalXHR = XMLHttpRequest; + class XMLHttpRequestWithOrigin extends OriginalXHR { + constructor() { + super(); + this._origin = 'http://localhost:3034'; + } - // Get reference to the SDK - uidSdk = dom.window[uidJsSdkName]; - if (!uidSdk) { - throw new Error(`SDK not found at window.${uidJsSdkName}`); + open(method, url, async) { + const result = super.open(method, url, async); + // Set Origin header immediately after open (required for CSTG) + super.setRequestHeader('Origin', this._origin); + return result; } + } + + global.XMLHttpRequest = XMLHttpRequestWithOrigin; + dom.window.XMLHttpRequest = XMLHttpRequestWithOrigin; + + try { + // Dynamically import the SDK (it's an ES module) + const isEUID = identityName && identityName.toUpperCase() === 'EUID'; + if (isEUID) { + const { EUID } = await import('@unified-id/euid-sdk'); + SdkClass = EUID; + } else { + const { UID2 } = await import('@uid2/uid2-sdk'); + SdkClass = UID2; + } + + // Instantiate the SDK (UID2 or EUID based on config) + uidSdk = new SdkClass(); - // Initialize the SDK + // Initialize the SDK with base URL uidSdk.init({ baseUrl: uidBaseUrl }); return uidSdk; From 152fad628ab67b30c94e007eb691ced3fc6e4cc9 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Tue, 11 Nov 2025 13:12:04 -0700 Subject: [PATCH 16/19] clean up comments --- .../javascript-sdk/server-side-node/README.md | 9 ++- .../javascript-sdk/server-side-node/server.js | 66 ++++++++++--------- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/web-integrations/javascript-sdk/server-side-node/README.md b/web-integrations/javascript-sdk/server-side-node/README.md index b4563ae..43dc44e 100644 --- a/web-integrations/javascript-sdk/server-side-node/README.md +++ b/web-integrations/javascript-sdk/server-side-node/README.md @@ -64,7 +64,14 @@ The following table lists the environment variables that you must specify to sta | `UID_CSTG_SERVER_PUBLIC_KEY` | Your UID2/EUID server public key for Client-Side Token Generation. **These are public credentials.** | Your assigned public key | | `SESSION_KEY` | Used by the cookie-session middleware to encrypt the session data stored in cookies. | Any secure random string | -> **⚠️ Important**: Your CSTG subscription must be configured with `http://localhost:3034` as an allowed origin. Contact your UID2/EUID representative to add this origin to your subscription's allowed origins list. +### Optional Configuration + +| Variable | Description | Example Values | +|:---------|:------------|:---------------| +| `UID_CSTG_ORIGIN` | The public URL where this application is deployed. Must match your CSTG subscription's allowed origins. Defaults to `http://localhost:PORT`. | `https://your-domain.com` (production)
`http://localhost:3034` (default) | +| `PORT` | Port number for the server | `3034` (default) | + +> **⚠️ Important**: Your CSTG subscription must be configured with an allowed origin that matches where your application is deployed. For production deployments, set `UID_CSTG_ORIGIN` to your application's public URL (e.g., `https://your-domain.com`). For local development, it automatically defaults to `http://localhost:PORT`. ### Display/UI Configuration diff --git a/web-integrations/javascript-sdk/server-side-node/server.js b/web-integrations/javascript-sdk/server-side-node/server.js index 139c6a7..2a9367d 100644 --- a/web-integrations/javascript-sdk/server-side-node/server.js +++ b/web-integrations/javascript-sdk/server-side-node/server.js @@ -1,16 +1,14 @@ "use strict"; -// Load environment variables from .env file (for local development) +// Load environment variables require('dotenv').config({ path: '../../../.env' }); const session = require('cookie-session'); const ejs = require('ejs'); const express = require('express'); -const nocache = require('nocache'); -const { JSDOM } = require('jsdom'); const crypto = require('crypto'); -const util = require('util'); -const XMLHttpRequest = require('xhr2'); +const nocache = require('nocache'); + const app = express(); const port = process.env.PORT || 3034; @@ -18,37 +16,37 @@ const port = process.env.PORT || 3034; let uidBaseUrl = process.env.UID_SERVER_BASE_URL; const subscriptionId = process.env.UID_CSTG_SUBSCRIPTION_ID; const serverPublicKey = process.env.UID_CSTG_SERVER_PUBLIC_KEY; - -// UI/Display configuration const identityName = process.env.IDENTITY_NAME; const docsBaseUrl = process.env.DOCS_BASE_URL; -// SDK will be loaded dynamically (it's an ES module) -let SdkClass = null; -// Initialize UID JavaScript SDK in a simulated browser environment using jsdom -// This demonstrates that the client-side SDK works in Node.js with jsdom +// additional packages/variables needed to ensure compabitibility with the SDK +const { JSDOM } = require('jsdom'); // for simulating a browser environment +const util = require('util'); // for polyfilling TextEncoder and TextDecoder +const XMLHttpRequest = require('xhr2'); // for making HTTP requests +const clientOrigin = process.env.UID_CSTG_ORIGIN || `http://localhost:${port}`; // Client origin: the URL where this app is accessible + + +// Create a virtual DOM environment for the SDK to run in +let SdkClass = null; let uidSdk = null; let dom = null; async function initializeSDK() { - // Create a virtual DOM environment for the SDK to run in - // NOTE: The origin 'http://localhost:3034' must be added to your CSTG subscription's - // allowed origins list by your UID2/EUID representative dom = new JSDOM('', { - url: 'http://localhost:3034', + url: clientOrigin, runScripts: 'dangerously', resources: 'usable', pretendToBeVisual: true, }); - // Set global variables for the SDK (SDK expects browser globals) + // Polyfills for Browser APIs, SDK uses them extensively (e.g., for token storage or making network requests global.window = dom.window; global.document = dom.window.document; global.navigator = dom.window.navigator; global.localStorage = dom.window.localStorage; - // Polyfill Web Crypto API for jsdom (SDK uses crypto.subtle for encryption) + // Polyfill Web Crypto API for jsdom (SDK uses crypto.subtle for AES-GCM encryption/decryption) Object.defineProperty(dom.window, 'crypto', { value: crypto.webcrypto, writable: false, @@ -62,27 +60,41 @@ async function initializeSDK() { dom.window.TextDecoder = util.TextDecoder; // Polyfill XMLHttpRequest with Origin header support - // The SDK needs the Origin header to be set for CSTG validation const OriginalXHR = XMLHttpRequest; class XMLHttpRequestWithOrigin extends OriginalXHR { constructor() { super(); - this._origin = 'http://localhost:3034'; + this._origin = clientOrigin; + this._customHeaders = {}; } open(method, url, async) { const result = super.open(method, url, async); - // Set Origin header immediately after open (required for CSTG) - super.setRequestHeader('Origin', this._origin); return result; } + + setRequestHeader(header, value) { + // Allow 'Origin' header that xhr2 normally blocks + this._customHeaders[header] = value; + if (header.toLowerCase() !== 'origin') { + return super.setRequestHeader(header, value); + } + } + + send(body) { + if (!this._headers) { + this._headers = {}; + } + this._headers.origin = this._origin; + + return super.send(body); + } } global.XMLHttpRequest = XMLHttpRequestWithOrigin; dom.window.XMLHttpRequest = XMLHttpRequestWithOrigin; try { - // Dynamically import the SDK (it's an ES module) const isEUID = identityName && identityName.toUpperCase() === 'EUID'; if (isEUID) { const { EUID } = await import('@unified-id/euid-sdk'); @@ -92,10 +104,8 @@ async function initializeSDK() { SdkClass = UID2; } - // Instantiate the SDK (UID2 or EUID based on config) + // Instantiate the SDK (UID2 or EUID based on config) with base URL uidSdk = new SdkClass(); - - // Initialize the SDK with base URL uidSdk.init({ baseUrl: uidBaseUrl }); return uidSdk; @@ -121,9 +131,6 @@ app.use(nocache()); // Routes -/** - * Main page - shows login form or identity result - */ app.get('/', (req, res) => { res.render('index', { identity: req.session.identity || null, @@ -200,9 +207,6 @@ app.post('/login', async (req, res) => { } }); -/** - * Logout endpoint - clears session and returns to main page - */ app.get('/logout', (req, res) => { if (uidSdk && uidSdk.disconnect) { uidSdk.disconnect(); From 0b5ec062c391a98e003e80b47ffe6d90f3d75fd7 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Tue, 11 Nov 2025 13:24:26 -0700 Subject: [PATCH 17/19] add new env variable to sample files --- .env.sample.euid | 1 + .env.sample.uid2 | 1 + .../javascript-sdk/server-side-node/README.md | 10 +--------- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.env.sample.euid b/.env.sample.euid index 421bd81..49e5275 100644 --- a/.env.sample.euid +++ b/.env.sample.euid @@ -19,6 +19,7 @@ SESSION_KEY="your-session-key-here" # Client-Side Token Generation (CSTG) - Provided by your EUID integration representative UID_CSTG_SERVER_PUBLIC_KEY="your-euid-server-public-key" UID_CSTG_SUBSCRIPTION_ID="your-euid-subscription-id" +UID_CSTG_ORIGIN="your-app-url.com" # The public URL where this application is deployed (e.g., https://your-domain.com or http://localhost:3034 for local dev) # React Client-Side Examples - Provided by your EUID integration representative # Note: These are the same values as the variables above, prefixed with REACT_APP_ diff --git a/.env.sample.uid2 b/.env.sample.uid2 index f57c04b..350e602 100644 --- a/.env.sample.uid2 +++ b/.env.sample.uid2 @@ -19,6 +19,7 @@ SESSION_KEY="your-session-key-here" # Client-Side Token Generation (CSTG) UID_CSTG_SERVER_PUBLIC_KEY="UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEo+jcPlk8GWn3iG0R5Il2cbFQI9hR3TvHxaBUKHl5Vh+ugr+9uLMiXihka8To07ETFGghEifY96Hrpe5RnYko7Q==" UID_CSTG_SUBSCRIPTION_ID="DMr7uHxqLU" +UID_CSTG_ORIGIN="your-app-url.com" # The public URL where this application is deployed (e.g., https://your-domain.com or http://localhost:3034 for local dev) # React Client-Side Examples # Note: These are the same values as the variables above, prefixed with REACT_APP_ diff --git a/web-integrations/javascript-sdk/server-side-node/README.md b/web-integrations/javascript-sdk/server-side-node/README.md index 43dc44e..0b93c87 100644 --- a/web-integrations/javascript-sdk/server-side-node/README.md +++ b/web-integrations/javascript-sdk/server-side-node/README.md @@ -62,17 +62,9 @@ The following table lists the environment variables that you must specify to sta | `UID_SERVER_BASE_URL` | The base URL of the UID2/EUID service. For details, see [Environments](https://unifiedid.com/docs/getting-started/gs-environments) (UID2) or [Environments](https://euid.eu/docs/getting-started/gs-environments) (EUID). | UID2: `https://operator-integ.uidapi.com`
EUID: `https://integ.euid.eu/v2` | | `UID_CSTG_SUBSCRIPTION_ID` | Your UID2/EUID subscription ID for Client-Side Token Generation. **These are public credentials.** | Your assigned subscription ID (e.g., `DMr7uHxqLU`) | | `UID_CSTG_SERVER_PUBLIC_KEY` | Your UID2/EUID server public key for Client-Side Token Generation. **These are public credentials.** | Your assigned public key | +| `UID_CSTG_ORIGIN` | The public URL where this application is deployed. Must match your CSTG subscription's allowed origins. | `https://your-domain.com` (production)
`http://localhost:3034` (local dev default) | | `SESSION_KEY` | Used by the cookie-session middleware to encrypt the session data stored in cookies. | Any secure random string | -### Optional Configuration - -| Variable | Description | Example Values | -|:---------|:------------|:---------------| -| `UID_CSTG_ORIGIN` | The public URL where this application is deployed. Must match your CSTG subscription's allowed origins. Defaults to `http://localhost:PORT`. | `https://your-domain.com` (production)
`http://localhost:3034` (default) | -| `PORT` | Port number for the server | `3034` (default) | - -> **⚠️ Important**: Your CSTG subscription must be configured with an allowed origin that matches where your application is deployed. For production deployments, set `UID_CSTG_ORIGIN` to your application's public URL (e.g., `https://your-domain.com`). For local development, it automatically defaults to `http://localhost:PORT`. - ### Display/UI Configuration | Variable | Description | Example Values | From 189b3e5990319670ef4597baae7773ae94f6b565 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Tue, 11 Nov 2025 13:28:28 -0700 Subject: [PATCH 18/19] add link to npm packages in readme --- web-integrations/javascript-sdk/server-side-node/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-integrations/javascript-sdk/server-side-node/README.md b/web-integrations/javascript-sdk/server-side-node/README.md index 0b93c87..ab639d9 100644 --- a/web-integrations/javascript-sdk/server-side-node/README.md +++ b/web-integrations/javascript-sdk/server-side-node/README.md @@ -10,7 +10,7 @@ For more information on the JavaScript SDK, refer to the [UID2 SDK for JavaScrip Unlike the browser where the SDK runs natively in the DOM, this example uses **jsdom** to simulate a browser environment within Node.js: -1. **Imports the SDK**: Uses npm packages `@uid2/uid2-sdk` or `@unified-id/euid-sdk` (selected dynamically based on `IDENTITY_NAME`) +1. **Imports the SDK**: Uses npm packages [`@uid2/uid2-sdk`](https://www.npmjs.com/package/@uid2/uid2-sdk) or [`@unified-id/euid-sdk`](https://www.npmjs.com/package/@unified-id/euid-sdk) (selected dynamically based on the `IDENTITY_NAME` environment variable) 2. **Creates a virtual DOM**: Uses jsdom to provide `window`, `document`, and `navigator` objects that the SDK expects 3. **Polyfills browser APIs**: Adds Node.js equivalents for Web Crypto API (`crypto.subtle`) and text encoding APIs (`TextEncoder`/`TextDecoder`) 4. **Instantiates the SDK**: Creates a new instance of `UID2` or `EUID` class From dc22b3424ddcc4ba1a10e74a29ec585994185d34 Mon Sep 17 00:00:00 2001 From: Eiman Eltigani Date: Tue, 11 Nov 2025 13:54:52 -0700 Subject: [PATCH 19/19] update title --- web-integrations/javascript-sdk/server-side-node/README.md | 2 +- .../javascript-sdk/server-side-node/views/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web-integrations/javascript-sdk/server-side-node/README.md b/web-integrations/javascript-sdk/server-side-node/README.md index ab639d9..f2615dc 100644 --- a/web-integrations/javascript-sdk/server-side-node/README.md +++ b/web-integrations/javascript-sdk/server-side-node/README.md @@ -1,4 +1,4 @@ -# Server-Side UID2 or EUID Integration Example using JavaScript SDK +# Server-Side UID2 or EUID Integration Example using JavaScript SDK via Node.js This example showcases how the UID2/EUID **JavaScript SDK works in Node.js** server environments. It uses the same `setIdentityFromEmail` method that runs in browsers, but executes it on the server. This uses **public credentials** (Subscription ID + Server Public Key) which are the same credentials used for client-side integrations. diff --git a/web-integrations/javascript-sdk/server-side-node/views/index.html b/web-integrations/javascript-sdk/server-side-node/views/index.html index 2104a9e..ff40360 100644 --- a/web-integrations/javascript-sdk/server-side-node/views/index.html +++ b/web-integrations/javascript-sdk/server-side-node/views/index.html @@ -2,7 +2,7 @@ - Server-Side <%- identityName %> Integration Example using JavaScript SDK + Server-Side <%- identityName %> Integration Example using JavaScript SDK via Node.js