Development
spec/ the specification: SPEC.md, and modules/<name>.md for each module
conformance/ HTTP cases every implementation must pass, and their runner
docs/ the topic docs linked from the README
packages/
core/ the platform-neutral handler, consent, cookies and module interface
adapters/ deployable platform packages, one folder per platform; rules for all of them in AGENTS.md
modules/ modules maintained by this project, built into every adapter
all/ @sharedid-edge/modules: the built-in list the adapters import
vendor-modules/ modules contributed and maintained by vendors, opt-in at build time
e2e/ unmodified Prebid.js against the worker, in Chromium, Firefox, WebKit and real Safari
website/ sharedid.dev: this Markdown, published with VitePress
scripts/ verify-deployment, the type checks, the repo checks below, and the docs generatorThe adapters are listed under Pick your platform and the modules in the module index. Each package has its own README.md and AGENTS.md. Workspaces, tsc -b references and the TypeScript 7 check are derived from these folders, so a new package needs no list edits beyond node scripts/check-projects.mjs --write.
Checks
npm ci
npm run check # everything CI runs, except the Workers-runtime and browser suitesTypeScript is strict from the start, and CI checks types several ways:
npm run build: every project under the shared strict config (strict,noUncheckedIndexedAccess,exactOptionalPropertyTypes,isolatedDeclarationsand more), including tests.npm run typecheck:ts7: the same projects with TypeScript 7.npm run typecheck:cloudflareandnpm run types:check -w @sharedid-edge/cloudflare: the Worker against the Workers runtime types, and that those types matchwrangler.jsonc.npm run consumer-types: the packed@sharedid-edge/coretype-checked by a strict consumer, for each module resolution and TypeScript version.npm run attw: the published types resolve correctly.npm run lint:typescript-eslint's strict type-checked rules.npm run check:projectsandnpm run check:links: the roottsconfig.jsonreferences every workspace project, and every relative link and heading anchor in the Markdown files resolves.npm run check:docs: the generated sections of the docs are up to date.npm run check:duplication: no code copied between packages (jscpd); it lists each clone with its file and line pairs, and writes a report to.jscpd-report/. See Duplication for what to do when it fails.
Tests:
npm test: unit tests, including the Worker's tests inside workerd, the Workers runtime. Vendor modules' tests run separately, withnpm run test:vendor-modules.npm run conformance: the conformance suite against the Node.js server;npm run conformance:wranglerruns it against the Worker underwrangler dev,npm run conformance:lambdaagainst the bundled Lambda handler behind a local stand-in for CloudFront,npm run conformance:vercelagainst the bundled Vercel adapter behind a local stand-in for Vercel's proxy,npm run conformance:azureagainst the bundled Azure Functions app behind a local stand-in for Front Door and the Functions host,npm run conformance:gcpagainst the real Functions Framework serving the bundled Cloud Run function, behind a local stand-in for Google Cloud's load balancer, andnpm run conformance:akamaiagainst the bundled EdgeWorker in a V8 context of its own, behind a local stand-in for Akamai EdgeWorkers. Seeconformance/AGENTS.md.- In
e2e/:npm ci && npx playwright install && npm testruns unmodified Prebid.js against the worker in three browsers. On a Mac,npm run test:safariruns the core tests in real Safari throughsafaridriver, as CI'se2e-safarijob does on a macOS runner; it needs remote automation enabled and an/etc/hostsentry (see Real Safari).
None of this needs a Cloudflare, AWS, Vercel, Azure or Google Cloud account. Separately, the Live tests workflow deploys to this project's Cloudflare test zone after every merge to main and nightly, never from a pull request, and runs verify-deployment, conformance and the browser tests against it.
Generated docs
The lists that every new module or adapter would otherwise edit are generated, so pull requests don't collide on them. node scripts/docs.mjs --write rewrites the text between <!-- generated:<name>:start --> and <!-- generated:<name>:end --> markers, and npm run check:docs fails when it's out of date:
| Section | In | From |
|---|---|---|
platforms, modules | README.md | adapters' and modules' package.json |
module-index | docs/modules.md | modules' package.json |
settings | docs/configuration.md | SETTINGS in packages/core/src/config.ts |
spec-modules | spec/SPEC.md | the files in spec/modules/ |
packages | AGENTS.md | adapters' and modules' package.json |
Don't edit between the markers; change the source and run the generator.
Package metadata
Every package in packages/adapters/ and packages/modules/ (and packages/vendor-modules/) has a sharedidEdge block in its package.json:
// a module
"sharedidEdge": {
"kind": "module",
"name": "geo", // the name in SID_MODULES; the same as the folder
"title": "the visitor's location as device.geo",
"summary": "What it adds to the payload, in one line (Markdown).",
"settings": "`precision`: `precise` (default) or `coarse`", // a short list, or "none"
"routes": ["v1"], // the module's routes: "v1", and "sync" if it runs there
"forwards": ["api.example.com"], // vendor modules only, if it sends data to another service
"export": "exampleModule", // vendor modules only, required: the module's export, for <package>#<export>
"network": "none", // vendor modules only, required: "none", or the hosts it may reach
"buildCredentials": ["EXAMPLE_LICENSE_KEY"], // vendor modules only: env variables its build reads (never shipped)
"ipDerived": true // vendor modules (and ec) only, if its ID uses the IP or other request data
}
// an adapter
"sharedidEdge": {
"kind": "adapter",
"platform": "Cloudflare Workers",
"summary": "Where it runs, in one line (Markdown)."
}@sharedid-edge/modules (packages/modules/all), the built-in list, is { "kind": "module-list" }. A project module also needs its specification in spec/modules/<name>.md.
The website
sharedid.dev is this repository's Markdown, published as it is with VitePress: the root README, docs/, spec/, and every adapter's and module's README. Nothing is copied. website/.vitepress/config.mts lists the published files and their URLs, and builds the sidebar from the same package.json metadata as the generated tables, so a new adapter or module appears there without an edit: project modules under Core modules, those in packages/vendor-modules/ under Vendor modules. AGENTS.md files and the rest of the repository aren't published.
- Write links for GitHub, as relative links between files. The site turns a link to a published page into a site URL, and any other link (a source file, an AGENTS.md, a folder without a README) into a link to the file on GitHub.
- A dead link fails the build, and so does a link to a heading that isn't there (
website/check-anchors.mjs). Headings get GitHub's anchors, so#modules-on-_sidsyncworks on both. Thewebsitejob in CI builds the site on every pull request, without an account. - VitePress upgrades: the site pins a VitePress 2 prerelease to use a patched Vite. Update it deliberately and run
npm ci && npm run buildinwebsite/to check pages and heading links. Inline code stays literal, including{{…}}examples; Vue components such as the adoption chart live in the theme. - Locally:
cd website && npm ci && npm run devserves it with live reload (onlocalhost:5173).npm run build && npm run previewserves the production build. - The adoption chart on the home page (
website/.vitepress/theme/AdoptionChart.vue) shows SharedID's share of Prebid.js User ID module detections from OpenSincera'smapping_modules.adoption.data.tsfetches the numbers at build time with theSINCERA_API_KEYrepository secret, which only the deploy job has, and puts only the counts in the page. Pull requests and local builds usewebsite/adoption-snapshot.json, as does a deploy whose request fails. Update the snapshot now and then so previews stay close. - Deploying:
website.ymlbuilds and deploys it on every merge tomainand weekly (refreshing the chart's numbers), as a static-assets Worker (website/wrangler.jsonc) on thesharedid.dev/*route, with the same Cloudflare secrets as the live tests.
Adding a module
A module adds IDs or signals to /_sid/v1:
import { defineModule } from '@sharedid-edge/core';
export const continent = defineModule({
name: 'continent',
purposes: [1], // TCF purposes it needs publisher consent for when GDPR applies
run({ platform }) {
return platform.continent === undefined ? undefined : { ortb2: { device: { geo: { ext: { continent: platform.continent } } } } };
}
});Publish it as its own package like packages/modules/ip, add it to builtInModules in packages/modules/all (every adapter imports that list), and enable it with SID_MODULES=continent. A module that only maintains cookies can also run on /_sid/sync with routes: ['v1', 'sync']; there only its cookies are used. Modules make no network calls, and a module that throws or runs past its time limit is left out without affecting the rest of the response. See packages/modules/AGENTS.md. A module from a vendor goes in packages/vendor-modules/ instead, and isn't built in.
Its docs are its own: a README.md (with its response examples), spec/modules/<name>.md, and the sharedidEdge block. Then run node scripts/docs.mjs --write; don't edit the root README.
Adding an adapter
An adapter is a new folder in packages/adapters/, with its own package.json (and sharedidEdge block), tsconfig.json, README.md (including the deployment walkthrough), AGENTS.md and test/, and a conformance target. See packages/adapters/AGENTS.md. Run node scripts/check-projects.mjs --write and node scripts/docs.mjs --write: the platform table in the root README picks it up.
Roadmap
- Done: specification, conformance suite, the cookie-only worker on Cloudflare, a Node.js server, AWS Lambda, Vercel, Azure Functions and Google Cloud adapters, and the
ip,ec,geo,reflect,botanduamodules, with a shared traffic signal any module can report bots to. - Next: Prebid.js sending its consent strings (TCF, GPP) to the worker with the sync, so consent no longer depends on the CMP cookie.
- A Prebid.js RTD module that reads
/_sid/v1, plus a device type module (ua, which also reports declared crawlers to the traffic signal), IP intelligence modules, a Fastly adapter, and Lambda@Edge. - Vendor modules, a Rust implementation, and inline mode.