v2 — current

SMRT Integration

Site-aware Member Recognition Technology — v2. Three-tier header injection: anonymous preference signals (SMRT), identified recognition (PDaaS), and silent customer matching (PDaaS True). One signing key, one JWKS endpoint, one verification path.

Headers at a glance

Header Tier Injected when TTL
X-BoxOwl-Smrt 1 (SMRT) Domain on the global registered-org list and no active connection. 10 min
X-BoxOwl-Identity 2 (PDaaS) & 3 (PDaaS True) Domain on the user's connected-org list. Mutually exclusive with the SMRT header. 30 min

Verifying a JWT (both headers)

Both shapes are RS256-signed with the same key. Fetch the JWKS, cache by kid, and verify locally.

GET/.well-known/smrt-jwks.json

Returns the JSON Web Key Set. Public, cacheable. Refresh on a kid miss (rate-limit refresh so a malformed token can't DOS your upstream).

Expected issuer on all v2 JWTs: boxowl.me. Identity-tier JWTs additionally carry an org claim; receivers MUST verify org matches the configured org slug.

import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

const client = jwksClient({
  jwksUri: 'https://api.boxowl.me/.well-known/smrt-jwks.json',
  cache: true,
  rateLimit: true,
});

function verifySmrtJwt(token, opts = {}) {
  const decoded = jwt.decode(token, { complete: true });
  const kid = decoded.header.kid;
  return new Promise((resolve, reject) => {
    client.getSigningKey(kid, (err, key) => {
      if (err) return reject(err);
      jwt.verify(token, key.getPublicKey(), {
        algorithms: ['RS256'],
        issuer: 'boxowl.me',
      }, (err, claims) => {
        if (err) return reject(err);
        // For X-BoxOwl-Identity, also assert the org claim matches.
        if (opts.expectedOrg && claims.org !== opts.expectedOrg) {
          return reject(new Error('org claim mismatch'));
        }
        resolve(claims);
      });
    });
  });
}

Reference implementation: backend/src/main/java/com/boxowl/api/controller/SmrtJwksController.java (the mint side) and demo/api/src/main/java/com/boxowl/perch/security/SmrtJwksVerifier.java (the verification side, Java/Spring).

JWT shape — X-BoxOwl-Smrt

Mintable via POST /api/v1/smrt/session (user JWT auth). The extension calls this at login and every 10 minutes; the rotating sub is what bounds cross-site behavioral tracking to a 10-minute window.

Claim Type Example Description
substring (UUID)"3f5c2d8e-7b13-4a91-9c47-1ab23456cdef"Rotating session UID. Not the user's id, handle, or any stable identifier.
smrtobjectsee belowThe user's org-agnostic preference profile (SMRT-015).
issstring"boxowl.me"Issuer.
iatnumber1748390400Issued at (Unix timestamp).
expnumber1748391000Expiration (Unix timestamp, +10 min).
jtistring"e7b…c1d"Unique token id.

Embedded smrt claim

Every field is optional — empty values drop out entirely. Treat all keys as missing-by-default. All fields are intentionally low-entropy so the SMRT-tier surface stays anonymous; exact-value variants of the same signals are reserved for the PDaaS Identity JWT below.

KeyTypeExampleVocabulary
ageBandstring"25-34"13-17, 18-24, 25-34, 35-44, 45-54, 55-64, 65+
regionstring"CO"2–8 char ISO-style region code. Never precise location.
styleRootsstring[]["minimalist","scandinavian"]User-defined; common values: minimalist, scandinavian, mid-century, industrial, bohemian, modern, rustic, eclectic.
colorPalettestring"earth-tones"earth-tones, monochrome, jewel-tones, pastels, neon, neutrals, warm, cool.
interestsstring[]["outdoor","cooking"]User-defined; common values: outdoor, cooking, music, books, sports, travel, art, technology, fitness, fashion.
languagestring"en"Primary ISO 639-1 language code, derived from the user's Demographic record. Region tags (en-US) are stripped — region already covers that. Single value only.
fitstring"slim"Categorical clothing fit preference: slim, regular, relaxed, athletic. Omitted when the user has not set one.
sizeBucketsobject{"top":"M","bottom":"M","shoes":"US-9-10"}Bucketed clothing-size hints. top / bottom are XSXL letter buckets; shoes is a pair-range like US-9-10, EU-40-41. Raw exact sizes never appear in this claim — they ride only in the PDaaS Identity JWT, where the org is already pairwise-identified.

JWT shape — X-BoxOwl-Identity

Mintable via POST /api/v1/smrt/batch-mint (user JWT auth). The extension calls this on webNavigation.onCommitted for connected-org domains; the response is per-slug so a single call can warm multiple JWTs.

Claim Type Example Description
substring"a8b7-c2…orguid"Pairwise orgUid — stable per (user, org), different for every connected org. Persist this on your side as the foreign key.
namestring"Alice Smith"Display name (preferredName → firstName + lastName → handle resolution).
preferredNamestring"Alice"Addressable short form for Hi Alice. Sourced from Identity.preferredName, falling back to firstName then handle. Use this when greeting the user — don't split name yourself.
verifiedbooleantrueWhether the user's BoxOwl email is verified.
smrtobjectsee SMRT shape aboveSame preference profile block as the Tier 1 JWT — same fields, same bucketed values.
sizesobject{"shirt":"M","pant":"32","shoe":"9.5","shoeSizeUnit":"us","sizeSystem":"us"}Exact clothing sizes from UserPreferences. Keys: shirt, pant, dress, shoe, shoeWidth, shoeSizeUnit, sizeSystem, clothingGender. Empty fields are omitted. Use these for size-aware product filtering or pre-selection at checkout. (Per-org pairwise identification means exact sizes here add no cross-org correlation surface.)
defaultShippingSpeedstring"express"User's preferred shipping speed: standard, express, overnight, pickup. Omitted when the user has not set one (entity default is prefer_not_to_say). Use to pre-pick the matching radio/select on checkout.
orgstring"acme-coffee"Org slug. Receivers MUST verify this matches their configured org slug.
tierstring"pdaas" | "pdaas_true"Subscription tier. "pdaas_true" indicates a tags claim is also present.
tagsobject{"customerId":"C123","memberTier":"gold"}PDaaS True only. Org-stored key→value annotations from OrgUserTag. Use this to silently match the BoxOwl orgUid to your own customer record.
issstring"boxowl.me"Issuer.
iat / expnumberIssued / expires (30-min lifetime).

The handle claim is intentionally absent — the pairwise orgUid is the only identifier.

Mint endpoints

The extension is the only normal caller. Each endpoint requires a user JWT in Authorization: Bearer.

POST/api/v1/smrt/session

Mints the global SMRT JWT. No body. Returns { "token": "<jwt>" } with sub = fresh UUID, smrt = profile claims, 10-minute TTL.

POST/api/v1/smrt/batch-mint

Body: { "orgSlugs": ["acme-coffee", "shop-b"] } (max 32). Returns { "jwts": { "acme-coffee": "<jwt>", "shop-b": "<jwt>" } }. Slugs without an active connection are silently omitted (extension treats absence the same as a revoked connection).

Domain-list endpoints

The extension drives its two stores from these endpoints. Both are ETag-conditional — send If-None-Match with the cached ETag; on 304 keep your current payload.

GET/api/v1/smrt/domains/all

Returns { "domains": ["acme.com", "shop.acme.com", ...] } — the global registered-org domain list. Public (no auth). 6-hour Cache-Control. Drives X-BoxOwl-Smrt injection.

GET/api/v1/smrt/connections/domains

Returns { "orgs": [{ "orgSlug", "displayName", "domains", "orgUid", "tier" }, ...] } — the calling user's active connections with each org's allowlist. Requires user JWT. 5-minute private cache. Drives X-BoxOwl-Identity upgrade.

User-side: SMRT profile settings

Users configure their SMRT profile in the BoxOwl app. These endpoints power the Android Settings > Privacy > SMRT Profile screen and the equivalent in the web SPA.

GET/api/v1/me/smrt-profile

Returns the user's current profile. Empty defaults ({}) if no row has been written. User JWT auth.

PUT/api/v1/me/smrt-profile

Body matches the user-editable subset of the smrt claim: ageBand, region, styleRoots, colorPalette, interests. Blank values clear the field. Returns the post-save shape.

The derived signals language, fit, and sizeBuckets are not set here — they come from the user's existing Demographic / UserPreferences records. Edit those via the standard vault endpoints (PUT /api/v1/vault/demographics, PUT /api/v1/vault/preferences) and the next minted SMRT JWT will pick them up.

Server-side: receive + verify

Read the inbound header, verify via JWKS, then route on tier. The two header names are mutually exclusive on the wire (the extension's higher-priority rule strips X-BoxOwl-Smrt when X-BoxOwl-Identity is injected).

app.use(async (req, res, next) => {
  const identityToken = req.get('X-BoxOwl-Identity');
  if (identityToken) {
    try {
      const claims = await verifySmrtJwt(identityToken, { expectedOrg: 'acme-coffee' });
      req.boxowlUser = {
        orgUid: claims.sub,
        name: claims.name,
        preferredName: claims.preferredName,       // "Hi Alice"
        verified: claims.verified,
        tier: claims.tier,                         // "pdaas" | "pdaas_true"
        tags: claims.tags || null,                 // PDaaS True only
        smrt: claims.smrt,                         // see SMRT shape
        sizes: claims.sizes || null,               // exact sizes for filtering / pre-pick
        defaultShippingSpeed: claims.defaultShippingSpeed || null,
      };
      return next();
    } catch (e) { /* fall through to SMRT tier */ }
  }

  const smrtToken = req.get('X-BoxOwl-Smrt');
  if (smrtToken) {
    try {
      const claims = await verifySmrtJwt(smrtToken);
      req.boxowlVisitor = {
        sessionUid: claims.sub,                    // rotates every 10 min
        smrt: claims.smrt,                         // anonymous preference signals
        // claims.smrt also carries: language, fit, sizeBuckets
        // — bucketed signals safe to consume even at SMRT tier.
      };
    } catch (e) { /* invalid; treat as anonymous */ }
  }
  next();
});

MV3 first-request constraint

Manifest V3 does not allow async-blocking HTTP requests. The extension installs identity-tier rules on webNavigation.onCommitted, which fires after the initial HTML request is already in flight.

This is acceptable for org integrations that read identity on API calls rather than the document request itself. If your authentication path absolutely needs identity on the first HTML request, fall back to the standard PDaaS exchange / connection flow.

Privacy model