← Back to Skills

Passport & Branding

interface VaultBranding { // Self-serve (Claimed tier) displayName: string // shown on all surfaces — REQUIRED on first write tagline?: string // "Colorado's…

Ruleprovision

About

interface VaultBranding { // Self-serve (Claimed tier) displayName: string // shown on all surfaces — REQUIRED on first write tagline?: string // "Colorado's largest commercial agency" logoUrl?: string // primary logo — SVG or PNG logoMarkUrl?: string // icon/mark for small surfaces (32x32) primaryColor: string // hex — buttons, accents (default: '#0F172A') secondaryColor?: string // hex websiteUrl?: string phone?: string email?: string

Skill Content

This is the raw markdown that gets installed as a Claude Code rule.

# Passport & Branding

## The Short Version

- The **Passport** is a JWT that carries identity, org branding, and permissions. Bio-ID assembles it at token mint time. Services read `req.user` — no extra calls needed.
- **Vault branding** is the single source of truth for how an org appears on every surface. It lives at `profile.branding` on vault entities.
- When `passport.includeBranding: true` is declared in catalog-info.yaml, Bio-ID fetches vault branding at mint time (fail-open, 3s timeout) and bundles it into the JWT `branding` claim.
- Consuming services read `req.user.branding` — no separate vault call needed for authenticated requests.

---

## Vault Branding Schema

```typescript
interface VaultBranding {
  // Self-serve (Claimed tier)
  displayName: string          // shown on all surfaces — REQUIRED on first write
  tagline?: string             // "Colorado's largest commercial agency"
  logoUrl?: string             // primary logo — SVG or PNG
  logoMarkUrl?: string         // icon/mark for small surfaces (32x32)
  primaryColor: string         // hex — buttons, accents (default: '#0F172A')
  secondaryColor?: string      // hex
  websiteUrl?: string
  phone?: string
  email?: string

  // White-Label Approved tier only
  customDomain?: string        // auth.acmeagency.com
  hideEcoBranding: boolean     // remove "Powered by InsurEco" — requires whiteLabelApproved
  emailFromName?: string       // magic link sender name
  customCss?: string           // injected into consent page, max 10KB

  // Platform-managed (only settable by platform org)
  verified: boolean
  whiteLabelApproved: boolean
}
```

### Two Tiers

| Tier | Who | What they can set |
|------|-----|-------------------|
| **Claimed** | Any entity after Bio-ID claim flow | displayName, tagline, logoUrl, logoMarkUrl, colors, website, phone, email |
| **White-Label Approved** | Platform approval after verification | customDomain, hideEcoBranding, emailFromName, customCss |

### Vault API

```
GET  /v1/branding/:orgSlug                → 0 gas, no auth (public)
GET  /v1/entities/:iecHash/branding       → 0 gas, no auth (public)
PATCH /v1/entities/:iecHash/branding      → 2 gas, auth required (owner/admin)
```

- Response always returns defaults for unset fields.
- First-time branding write requires `displayName`.
- `verified` and `whiteLabelApproved` can only be set by platform services — PATCH from a regular service returns 403 for those fields.
- `hideEcoBranding` is silently ignored unless `whiteLabelApproved: true`.

### Vault SDK

```typescript
import { VaultClient } from '@insureco/bio-vault'
const vault = VaultClient.fromEnv()

// Read by orgSlug (most common — you have it from the JWT)
const branding = await vault.getBranding('acme-agency')

// Read by iecHash (for public entity pages)
const branding = await vault.getBrandingByHash('0x1a2b3c...')

// Update (owner/admin only)
await vault.updateBranding('0x1a2b3c...', {
  tagline: 'Updated tagline',
  primaryColor: '#1D3557',
})
```

---

## Enriched Passport JWT

When an OAuth client has `passportConfig.includeBranding: true`, Bio-ID fetches vault branding at token mint time and includes it in the JWT.

### passportConfig on OAuthClient

```typescript
interface IPassportConfig {
  includeBranding: boolean  // fetch vault branding at mint time
  includeIecHash: boolean   // include entity iecHash in token
}
```

Services opt in via catalog-info.yaml:

```yaml
spec:
  auth:
    mode: sso
    passport:
      includeBranding: true    # sets passportConfig.includeBranding on the OAuth client
      includeIecHash: false    # sets passportConfig.includeIecHash (default: false)
```

On deploy, the builder reads `spec.auth.passport` and sets `passportConfig` on the OAuth client. Bio-ID then enriches the JWT at token mint time based on these flags.

> The shorthand `branding: true` is still accepted and maps to `includeBranding: true`. The explicit form is preferred for new services.

### What goes into the JWT branding claim

When `includeBranding: true`, the JWT contains a `branding` claim with this shape:

```json
{
  "branding": {
    "displayName": "Acme Insurance",
    "logoUrl": "https://...",
    "logoMarkUrl": "https://...",
    "primaryColor": "#0066CC",
    "secondaryColor": "#003366",
    "verified": true,
    "whiteLabelApproved": false
  }
}
```

```typescript
// TypeScript interface — subset of VaultBranding, only display-relevant fields
interface PassportBranding {
  displayName: string
  logoUrl?: string
  logoMarkUrl?: string
  primaryColor: string
  secondaryColor?: string
  verified: boolean
  whiteLabelApproved: boolean
}
```

### Fail-open behavior

Bio-ID fetches vault branding with a **3-second timeout**. If the vault is unreachable at mint time, Bio-ID issues the token without a `branding` claim — auth still works. Branding is best-effort. Services should handle `req.user.branding` being `undefined`.

### Propagation timing

Branding refreshes on every token mint and refresh. Since access tokens have a 1-hour TTL, branding changes propagate within 1 hour at most.

---

## The Passport Object

The full Passport shape (identity + branding + permissions):

```typescript
interface Passport {
  // Identity
  bioId: string
  email: string
  firstName: string
  lastName: string
  displayName: string
  avatar?: string
  userType: string          // 'user' | 'service' | 'admin'
  roles: string[]

  // Primary org
  orgSlug: string
  orgId: string             // ORG-xxx format
  orgName: string

  // All org memberships
  orgs: {
    orgSlug: string
    orgId: string
    orgName: string
    roles: string[]
    modules: string[]
  }[]

  modules: string[]

  // Branding — present when passport.includeBranding: true
  branding?: PassportBranding

  // Permissions — resolved from Koko at mint time
  permissions: { scopes: string[] }

  // Vault link
  iecHash?: string

  // Session metadata
  issuedAt: string
  connectedServices: string[]
}
```

---

## Consuming Branding in Your Service

### From the Passport (authenticated requests)

```typescript
// req.user IS the Passport — populated by Janus or your middleware
const { branding, permissions } = req.user

if (branding) {
  document.title = branding.displayName
  headerLogo.src = branding.logoUrl
  root.style.setProperty('--primary', branding.primaryColor)

  // Trust badge
  if (branding.verified) showVerifiedBadge()

  // White-label features
  if (branding.whiteLabelApproved) hideEcoBranding()
}

// Permission gating
if (permissions.scopes.includes('raterspot:rate')) showRatingPanel()
```

### From the vault directly (unauthenticated / background)

Only call vault when there is no Passport available:

```typescript
// Public entity profile page — no user session
const branding = await vault.getBranding('acme-agency')

// Background job — no request context
const branding = await vault.getBranding(orgSlug)
```

### Passport Socket (real-time updates)

```
wss://bio.insureco.io/passport?token=<access_token>
```

| Event | When | Payload |
|-------|------|---------|
| `identity` | On connect | `{ type, passport }` |
| `passport_updated` | Role/org/branding change | `{ type, passport }` |
| `revoked` | Logout or token invalidated | `{ type }` |

```typescript
const ws = new WebSocket(`${BIO_ID_URL.replace(/^http/, 'ws')}/passport?token=${token}`)
ws.onmessage = (e) => {
  const { type, passport } = JSON.parse(e.data)
  if (type === 'identity' || type === 'passport_updated') applyBranding(passport.branding)
  if (type === 'revoked') clearSession()
}
```

---

## Three Auth Modes

All three produce the same Passport. The mode determines the user experience.

| Mode | catalog-info.yaml | Bio-ID visible? |
|------|-------------------|-----------------|
| **Branded** | `auth: mode: sso` | Yes — "Sign in with Bio-ID" |
| **White-Label** | `auth: mode: sso` + `passport: includeBranding: true` | No — org's domain + branding |
| **Headless** | `auth: mode: sso` + `passport: includeBranding: true, headless: true` | No — server-to-server, fully invisible |

White-Label requires `whiteLabelApproved: true` on the vault entity for custom domain and hideEcoBranding features.

---

## Gotchas

- **Branding only bundled if declared** — without `passport: includeBranding: true` in catalog-info.yaml, the Passport carries identity only.
- **Handle missing branding** — vault may be unreachable at mint time. Always check `if (req.user.branding)` before accessing fields.
- **Don't call vault for authenticated requests** — read `req.user.branding` instead. Vault calls are for public/unauthenticated contexts only.
- **1-hour propagation** — branding changes take up to 1 hour to appear in tokens (access token TTL).
- **Platform-only fields** — `verified` and `whiteLabelApproved` return 403 when set by non-platform services.
- **hideEcoBranding requires approval** — setting it without `whiteLabelApproved: true` has no effect.
- **First write needs displayName** — PATCH without `displayName` on a new branding record fails validation.
- **customCss max 10KB** — Bio-ID rejects larger payloads.
- **Socket closes on invalid token** — don't retry without a fresh token.
- **Headless mode: server-to-server only** — never expose Bio-ID embed URLs to the browser.

Install

Copy the skill content and save it to:

~/.claude/rules/tawa-passport-branding.md
Download .md

Coming soon via CLI:

tawa chaac install tawa-passport-branding

Details

Format
Rule
Category
provision
Version
1.0.83432
Tokens
~2,321
Updated
2026-06-24
platformpassportbrandingvaultjwtidentitywhite-labeltheming