{"slug":"architecture/v2-identity-ingress","title":"IntegrateWise Continuity Bridge v2.0","section":"architecture","sectionLabel":"Architecture","status":"unclassified","type":"document","path":"architecture/v2-identity-ingress.mdx","bytes":70520,"description":null,"updated":null,"source":"integratewise/Integratewise-docs-wiki","content":"\n> **Status: SUPERSEDED IDENTITY/INGRESS DEEP-DIVE.** The old product/layer model\n> and any conflicting identity provider rules are historical. Clerk identity,\n> Gateway tenant resolution and current ingress contracts are frozen in\n> [`IMPLEMENTATION_CONTRACT_v1.1.md`](./IMPLEMENTATION_CONTRACT_v1.1.md).\n\n# Domain Specification: Identity & Ingress Layers\n\n# Status: Stage 1/2 Deep-Dive — Production Reference\n\n# Date: 2026-07-02\n\n# Scope: Authentication, Organizations, Users, API Keys, JWT, RBAC, Tenant Context,\n\n# Sessions, Gateway, Rate Limiting, Request Validation, Routing,\n\n# Transport (HTTP/WebSocket/SSE/MCP/Webhooks), Discovery\n\n---\n\n## 1. Domain Charter\n\nThis document defines the **Identity & Ingress Layers** (Layers 0–1 of the 6-layer architecture). These layers are the **only** surfaces that external consumers ever touch. Every request — from a human clicking in the workbench, an AI assistant streaming via MCP, a webhook payload from Salesforce, or a backend SDK call — must pass through the Gateway, be authenticated, scoped to a tenant, validated, rate-limited, and routed before any internal service ever sees it.\n\n**The Invariant:**\n\n> No internal service accepts direct external traffic. The Gateway (`integratewise-gateway`) is the sole north-plane entry point. Every hop carries `tenant_id`. Cross-tenant access is a 403 (fail-loud, not 404).\n\n---\n\n## 2. Architecture Position\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                         EXTERNAL CONSUMERS                                   │\n│  ┌─────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐     │\n│  │ Human   │  │ AI Assistant│  │ External    │  │ Provider Webhooks   │     │\n│  │ (Web UI)│  │ (Claude,    │  │ Agent / SDK │  │ (Salesforce,        │     │\n│  │         │  │  ChatGPT)   │  │             │  │  HubSpot, Stripe)   │     │\n│  └────┬────┘  └──────┬──────┘  └──────┬──────┘  └──────────┬──────────┘     │\n│       │              │                │                   │                 │\n│       │ HTTP(S)      │ SSE/MCP        │ HTTP/SSE          │ HTTP POST       │\n│       │ + Bearer JWT │ + Bearer JWT   │ + API Key         │ + Signature     │\n│       ▼              ▼                ▼                   ▼                 │\n├───────┴──────────────┴────────────────┴───────────────────┴─────────────────┤\n│                         IDENTITY & INGRESS LAYERS                           │\n│  ┌─────────────────────────────────────────────────────────────────────┐    │\n│  │  GATEWAY (integratewise-gateway) — Single Cloudflare Worker         │    │\n│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐   │    │\n│  │  │  AuthN  │ │ Rate    │ │ Tenant  │ │ Request │ │   Router    │   │    │\n│  │  │  (JWT/  │ │ Limit   │ │ Resolver│ │ Validate│ │  (Binding   │   │    │\n│  │  │  APIKey)│ │         │ │         │ │         │ │   Map)      │   │    │\n│  │  └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └──────┬──────┘   │    │\n│  │       └───────────┴───────────┴───────────┴─────────────┘          │    │\n│  │                              │                                      │    │\n│  │                              ▼                                      │    │\n│  │  ┌─────────────────────────────────────────────────────────────┐   │    │\n│  │  │           DISCOVERY ASSEMBLER (inside Gateway)              │   │    │\n│  │  │  GET /api/v1/discovery → capability registry snapshot       │   │    │\n│  │  └─────────────────────────────────────────────────────────────┘   │    │\n│  └─────────────────────────────────────────────────────────────────────┘    │\n│                              │                                              │\n│                              ▼ Service Bindings (26 bindings)                │\n├─────────────────────────────────────────────────────────────────────────────┤\n│                         CAPABILITY LAYER (Layer 2)                          │\n│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐   │\n│  │ tenants │ │connector│ │normalizer│ │  act    │ │ govern  │ │  think  │   │\n│  │ (RBAC)  │ │  -sync  │ │         │ │         │ │         │ │         │   │\n│  └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘   │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n**Integration Points with Adjacent Layers:**\n\n- **Downstream → Capability Layer:** Service bindings to all 26 services. Gateway never calls internal services over HTTP; it uses Cloudflare Service Bindings for zero-latency RPC.\n- **Upstream → Consumers:** HTTP(S) for REST, SSE for MCP streams, WebSocket for real-time workbench, standard HTTP POST for webhooks.\n- **Sideways → Governance Layer:** Every authenticated request is logged to `spine_audit_log` via the `pipeline` service binding (fire-and-forget audit enqueue).\n\n---\n\n## 3. Authentication Subsystem (AuthN)\n\n### 3.1 Supported Credential Types\n\nThe Gateway accepts **exactly two** credential types. All others (Clerk, Stack Auth, Descope, Supabase Auth direct) are rejected at the edge.\n\n| Credential Type | Header Format                      | Use Case                                          | Issuer                                   |\n| --------------- | ---------------------------------- | ------------------------------------------------- | ---------------------------------------- |\n| **Gateway JWT** | `Authorization: Bearer <jwt>`      | Human users, AI assistants, frontend sessions     | `tenants` service (login/oauth exchange) |\n| **API Key**     | `Authorization: Bearer iwak_<key>` | Backend SDKs, automation scripts, external agents | `admin` service (provisioned per tenant) |\n\n**Rejected patterns (403):**\n\n- Missing `Authorization` header on protected routes\n- `Basic` auth\n- Provider-issued tokens (Salesforce session, GitHub token) — these belong in the South Plane\n- Expired JWT (returns 401 with `WWW-Authenticate: Bearer error=\"invalid_token\"`)\n- Revoked API key (checked against KV blacklist)\n\n### 3.2 JWT Specification\n\n```typescript\n// packages/auth-types/src/jwt.ts\n\n/**\n * Gateway-issued JWT claims.\n * Signed with RS256 using a 2048-bit RSA keypair.\n * Private key lives in Cloudflare Secrets (never in repo).\n * Public key is cached in KV for edge verification.\n */\nexport interface GatewayJwtPayload {\n  /** JWT ID — unique per token, used for revocation tracking */\n  jti: string;\n\n  /** Issuer — always \"gateway\" */\n  iss: \"gateway\";\n\n  /** Subject — user UUID */\n  sub: string;\n\n  /** Audience — tenant slug or \"integratewise\" for platform admin */\n  aud: string;\n\n  /** Issued at (epoch seconds) */\n  iat: number;\n\n  /** Expiration (epoch seconds) — default 24h, refresh tokens 30d */\n  exp: number;\n\n  /** Tenant context — THE isolation boundary */\n  tenant_id: string;\n\n  /** Organization ID within tenant (for multi-org tenants) */\n  org_id: string;\n\n  /** User role — resolved from RBAC at issue time */\n  role: \"owner\" | \"admin\" | \"member\" | \"viewer\" | \"api\";\n\n  /** Capability scope — what this token can do */\n  scope: (\"read\" | \"write\" | \"admin\" | \"webhook\" | \"mcp\")[];\n\n  /** Session ID — links to KV session store */\n  sid: string;\n\n  /** Correlation ID — propagated across all service bindings */\n  cid: string;\n\n  /** Plan tier — used for rate-limit bucket selection */\n  tier: \"free\" | \"pro\" | \"enterprise\" | \"customer_zero\";\n}\n\n/**\n * JWT verification result. Never returns partial success.\n * Fail-loud: any validation failure → 403 with explicit reason.\n */\nexport interface JwtVerificationResult {\n  valid: boolean;\n  payload?: GatewayJwtPayload;\n  error?:\n    | \"missing\"\n    | \"malformed\"\n    | \"expired\"\n    | \"invalid_signature\"\n    | \"invalid_issuer\"\n    | \"invalid_audience\"\n    | \"revoked\"\n    | \"tenant_suspended\";\n}\n\n/**\n * API Key metadata stored in D1 (hashed, not plaintext).\n */\nexport interface ApiKeyRecord {\n  id: string;\n  tenant_id: string;\n  org_id: string;\n  name: string; // human-readable label\n  key_hash: string; // Argon2id hash of the key\n  scope: string[];\n  created_at: string;\n  expires_at: string | null;\n  last_used_at: string | null;\n  revoked_at: string | null;\n  created_by: string; // user UUID\n  rate_limit_bucket: \"default\" | \"elevated\" | \"unlimited\";\n}\n```\n\n### 3.3 JWT Lifecycle Flow\n\n```\n┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐\n│   User      │────▶│  Identity   │────▶│   Gateway   │────▶│   Client    │\n│  Login      │     │  Provider   │     │  JWT Issue  │     │  Storage    │\n│  (OAuth2)   │     │ (Google/    │     │             │     │             │\n│             │     │  GitHub)    │     │             │     │             │\n└─────────────┘     └──────┬──────┘     └─────────────┘     └─────────────┘\n                           │\n                           ▼\n                    ┌─────────────┐\n                    │  tenants    │\n                    │  Service    │\n                    │  (OAuth     │\n                    │  callback)  │\n                    └──────┬──────┘\n                           │\n                           ▼\n                    ┌─────────────┐\n                    │  D1: users  │\n                    │  D1: orgs   │\n                    │  D1: tenants│\n                    └─────────────┘\n```\n\n**Flow Detail:**\n\n1. **OAuth Initiation:** User clicks \"Sign in with Google\" → frontend → `GET /api/v1/auth/:provider/start` → Gateway → `tenants` service binding.\n2. **Provider Callback:** OAuth provider redirects to `POST /api/v1/auth/:provider/callback` → `tenants` service resolves or creates user/org/tenant.\n3. **RBAC Resolution:** `tenants` queries `D1 rbac_roles` + `rbac_permissions` for the user's org role.\n4. **JWT Minting:** `tenants` calls Gateway's internal `issueJwt()` (service binding, not HTTP) with claims payload.\n5. **Session Creation:** Gateway writes `KV:sessions:<sid>` with session metadata (IP, UA, tenant_id, exp).\n6. **Response:** JWT + refresh token returned to client. Refresh token is a separate opaque token stored in `KV:refresh:<jti>`.\n\n---\n\n## 4. Organization & Tenant Context\n\n### 4.1 Tenant Isolation Model\n\nTenant is the **hard isolation boundary**. Org is a soft partition within a tenant. Every database query, every cache key, every queue message, every log entry carries `tenant_id`.\n\n```\nTenant (tenant_id = \"acme_corp\")\n├── Org: \"Engineering\" (org_id = \"eng_01\")\n│   ├── Users: alice@acme.com (owner), bob@acme.com (member)\n│   └── Resources: scoped to org\n├── Org: \"Sales\" (org_id = \"sales_01\")\n│   ├── Users: carol@acme.com (admin), dave@acme.com (viewer)\n│   └── Resources: scoped to org\n└── Platform-level: tenant owner can see all orgs\n\nTenant (tenant_id = \"startup_xyz\")\n├── Org: \"Default\" (single-org tenant)\n└── ...\n```\n\n### 4.2 Tenant Resolution at the Edge\n\n```typescript\n// services/gateway/src/tenant-resolver.ts\n\n/**\n * Tenant resolution is STRATIFIED — never trust client input.\n * Priority (highest wins):\n *   1. JWT claim `tenant_id` (for Bearer JWT)\n *   2. API key lookup → D1 `api_keys` table → `tenant_id`\n *   3. NOTHING ELSE — no x-tenant-id header, no query param, no URL path\n *\n * If credential type is JWT and `aud` !== `tenant_id`, return 403.\n * If API key tenant is suspended, return 403 with `error: \"tenant_suspended\"`.\n */\nexport async function resolveTenant(request: Request, env: Env): Promise<TenantContext> {\n  const authHeader = request.headers.get(\"Authorization\");\n\n  if (!authHeader?.startsWith(\"Bearer \")) {\n    throw new AuthError(\"missing\", 401);\n  }\n\n  const token = authHeader.slice(7);\n\n  // Branch 1: API Key (starts with \"iwak_\")\n  if (token.startsWith(\"iwak_\")) {\n    return await resolveApiKey(token, env);\n  }\n\n  // Branch 2: JWT\n  return await resolveJwt(token, env);\n}\n\nexport interface TenantContext {\n  tenant_id: string;\n  org_id: string;\n  user_id: string | null; // null for API keys\n  role: string;\n  scope: string[];\n  tier: string;\n  session_id: string | null;\n  correlation_id: string; // x-correlation-id or generated\n  credential_type: \"jwt\" | \"api_key\";\n}\n\n/**\n * SECURITY: After resolution, inject TenantContext into\n * request.cf.integratewise (Cloudflare request context).\n * Every downstream service binding receives this via the binding's\n * `request` object. No header injection allowed.\n */\n```\n\n### 4.3 Cross-Tenant Safety Gates (P0)\n\n| #   | Vulnerability                          | Gate                                                      | Failure Mode                       |\n| --- | -------------------------------------- | --------------------------------------------------------- | ---------------------------------- |\n| 1   | `x-tenant-id` header spoofing          | Strip `x-tenant-id` from all inbound requests before auth | Header removed, ignored if present |\n| 2   | JWT `tenant_id` ≠ `aud` claim          | Verify `payload.tenant_id === payload.aud`                | 403 `invalid_tenant_claim`         |\n| 3   | API key from suspended tenant          | Check `tenants.status = 'active'` at resolution time      | 403 `tenant_suspended`             |\n| 4   | Session cross-tenant reuse             | `KV:sessions:<sid>` stores `tenant_id`; verify match      | 403 `session_tenant_mismatch`      |\n| 5   | Service binding without tenant context | Enforce `TenantContext` present in all internal RPCs      | 500 `missing_tenant_context`       |\n\n---\n\n## 5. RBAC (Role-Based Access Control)\n\n### 5.1 Role Hierarchy\n\n```\n                    ┌─────────────┐\n                    │   owner     │  ← Can do everything, including delete tenant\n                    └──────┬──────┘\n                           │\n              ┌────────────┼────────────┐\n              ▼            ▼            ▼\n        ┌─────────┐  ┌─────────┐  ┌─────────┐\n        │  admin  │  │  member │  │  viewer │\n        │(manage  │  │(execute│  │(read    │\n        │ users)  │  │ actions)│  │ only)   │\n        └────┬────┘  └────┬────┘  └─────────┘\n             │            │\n             ▼            ▼\n        ┌─────────┐  ┌─────────┐\n        │ api_key │  │ webhook │\n        │(scoped  │  │(inbound │\n        │ access) │  │ only)   │\n        └─────────┘  └─────────┘\n```\n\n### 5.2 Permission Model\n\n```typescript\n// packages/auth-types/src/rbac.ts\n\n/**\n * Permission format: `<resource>:<action>`\n * Examples:\n *   - \"spine:read\"          → Read any entity in Spine\n *   - \"spine:write\"         → Create/update entities\n *   - \"connector:create\"    → Add a new connector\n *   - \"connector:delete\"    → Remove a connector\n *   - \"twin:propose\"        → Trigger Twin reasoning\n *   - \"govern:approve\"      → Approve proposals (owner/admin only)\n *   - \"admin:*\"             → Full admin access\n *   - \"mcp:invoke\"          → Execute MCP tools\n *   - \"webhook:receive\"     → Receive inbound webhooks\n */\nexport type Permission = `${string}:${string}`;\n\nexport interface RoleDefinition {\n  id: string;\n  tenant_id: string;\n  name: string; // \"owner\", \"admin\", \"member\", \"viewer\", \"api\"\n  permissions: Permission[];\n  inherits_from: string | null; // role inheritance chain\n  created_at: string;\n}\n\n/**\n * Permission check. Evaluated at TWO boundaries:\n *   1. Gateway: coarse-grained (does this role have \"spine:read\"?)\n *   2. Service: fine-grained (does this user own this specific record?)\n *\n * Gateway never does row-level checks — it only validates the token\n * and passes TenantContext. Services do fine-grained authorization.\n */\n  // Direct match\n  if (role.permissions.includes(required)) return true;\n  // Wildcard match (\"admin:*\" matches \"admin:users\")\n  const [resource] = required.split(\":\");\n  if (role.permissions.includes(`${resource}:*`)) return true;\n  // Global wildcard\n  if (role.permissions.includes(\"*:*\")) return true;\n  // Recursive inheritance check\n  if (role.inherits_from) {\n    // Resolved via D1 lookup (cached in KV)\n    return checkInherited(role.inherits_from, required);\n  }\n  return false;\n}\n```\n\n### 5.3 RBAC Enforcement Points\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│                         ENFORCEMENT POINTS                               │\n├─────────────────────────────────────────────────────────────────────────┤\n│  Layer 0 — Gateway (Coarse)                                              │\n│  ├── Check role exists in `rbac_roles`                                   │\n│  ├── Verify required permission for route                                │\n│  │   Route map: `POST /api/v1/spine/*` → requires \"spine:read|write\"     │\n│  └── Reject early — 403 before any service binding call                  │\n│                                                                          │\n│  Layer 1 — Service (Fine-grained)                                        │\n│  ├── `WHERE tenant_id = ? AND org_id = ?` on every query                 │\n│  ├── Record-level ownership checks (e.g., \"did this user create this?\") │\n│  └── Audit log: `rbac_decision` table with allow/deny + reason           │\n│                                                                          │\n│  Layer 2 — Data (Hard boundary)                                          │\n│  ├── D1 row-level security (future: Postgres RLS when swappable)         │\n│  └── KV key prefixing: `kv:<tenant_id>:<key>`                           │\n└─────────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## 6. Session Management\n\n### 6.1 Session Store (KV-Backed)\n\nSessions are ephemeral state stored in Cloudflare KV with TTL. They are NOT the source of truth — the JWT is. Sessions enable revocation, multi-device tracking, and security events.\n\n```typescript\n// services/gateway/src/sessions.ts\n\nexport interface SessionRecord {\n  sid: string;\n  tenant_id: string;\n  org_id: string;\n  user_id: string;\n  jti: string; // links to JWT\n  issued_at: number;\n  expires_at: number;\n  ip_hash: string; // SHA-256 of IP (privacy-preserving)\n  user_agent_hash: string; // SHA-256 of UA string\n  device_fingerprint: string;\n  last_active_at: number;\n  revoked_at: number | null;\n  revoke_reason: string | null;\n}\n\n/**\n * Session lifecycle:\n *   CREATE → ACTIVE → (REFRESH) → (REVOKE) → EXPIRED\n *\n * KV keys:\n *   sessions:<sid>        → SessionRecord (TTL = JWT exp)\n *   sessions_by_jti:<jti> → sid (for revocation lookup)\n *   sessions_by_user:<user_id> → JSON[sid[]] (for \"log out all devices\")\n */\n\nexport async function createSession(\n  env: Env,\n  payload: GatewayJwtPayload,\n  request: Request\n): Promise<SessionRecord> {\n  const sid = crypto.randomUUID();\n  const now = Math.floor(Date.now() / 1000);\n\n  const session: SessionRecord = {\n    sid,\n    tenant_id: payload.tenant_id,\n    org_id: payload.org_id,\n    user_id: payload.sub,\n    jti: payload.jti,\n    issued_at: now,\n    expires_at: payload.exp,\n    ip_hash: await hashIp(request.headers.get(\"CF-Connecting-IP\") || \"\"),\n    user_agent_hash: await hashString(request.headers.get(\"User-Agent\") || \"\"),\n    device_fingerprint: await computeFingerprint(request),\n    last_active_at: now,\n    revoked_at: null,\n    revoke_reason: null,\n  };\n\n  // Write with TTL matching JWT expiration\n  const ttlSeconds = payload.exp - now;\n  await env.KV.put(`sessions:${sid}`, JSON.stringify(session), {\n    expirationTtl: ttlSeconds,\n  });\n  await env.KV.put(`sessions_by_jti:${payload.jti}`, sid, {\n    expirationTtl: ttlSeconds,\n  });\n\n  // Append to user's session list\n  const userSessions = await env.KV.get(`sessions_by_user:${payload.sub}`);\n  const sessions: string[] = userSessions ? JSON.parse(userSessions) : [];\n  sessions.push(sid);\n  await env.KV.put(`sessions_by_user:${payload.sub}`, JSON.stringify(sessions), {\n    expirationTtl: 30 * 24 * 60 * 60, // 30 days\n  });\n\n  return session;\n}\n```\n\n### 6.2 Session Revocation\n\n```typescript\n/**\n * Revocation flows:\n *   1. User \"Log out\" → revoke single session\n *   2. User \"Log out all devices\" → revoke all sessions for user\n *   3. Admin \"Suspend user\" → revoke all sessions + blacklist JWT jti\n *   4. Security event (suspicious IP) → automatic revocation\n *\n * Blacklist: KV `jwt_blacklist:<jti>` = \"revoked\" (TTL = remaining JWT lifetime)\n * Gateway checks blacklist on EVERY JWT verification.\n */\nexport async function revokeSession(env: Env, sid: string, reason: string): Promise<void> {\n  const sessionData = await env.KV.get(`sessions:${sid}`);\n  if (!sessionData) return;\n\n  const session: SessionRecord = JSON.parse(sessionData);\n  session.revoked_at = Math.floor(Date.now() / 1000);\n  session.revoke_reason = reason;\n\n  await env.KV.put(`sessions:${sid}`, JSON.stringify(session));\n\n  // Blacklist the JWT\n  const remainingTtl = session.expires_at - session.revoked_at;\n  if (remainingTtl > 0) {\n    await env.KV.put(`jwt_blacklist:${session.jti}`, \"revoked\", {\n      expirationTtl: remainingTtl,\n    });\n  }\n}\n```\n\n---\n\n## 7. Gateway — The Single Entry Point\n\n### 7.1 Gateway Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                     GATEWAY REQUEST LIFECYCLE                                │\n│                                                                              │\n│  ┌─────────┐   ┌─────────┐   ┌─────────┐   ┌─────────┐   ┌─────────────┐   │\n│  │  WAF    │──▶│  TLS    │──▶│  CORS   │──▶│  AuthN  │──▶│ Rate Limit  │   │\n│  │ (CF)    │   │ (CF)    │   │ (GW)    │   │  (GW)   │   │   (GW)      │   │\n│  └─────────┘   └─────────┘   └─────────┘   └─────────┘   └─────────────┘   │\n│       │            │            │            │                  │           │\n│       ▼            ▼            ▼            ▼                  ▼           │\n│    Blocked     Blocked      Blocked      401/403           429              │\n│    (WAF)       (TLS)        (CORS)       (Auth)         (Rate limit)        │\n│                                                                              │\n│  ┌─────────────┐   ┌─────────────┐   ┌─────────────────────────────────┐   │\n│  │   Tenant    │──▶│   Request   │──▶│         ROUTER                  │   │\n│  │  Resolver   │   │  Validation │   │  ┌─────────┐  ┌─────────────┐  │   │\n│  │             │   │  (Zod)      │   │  │ Route   │  │  Service    │  │   │\n│  └─────────────┘   └─────────────┘   │  │  Match  │──│  Binding    │──┼───┼──▶\n│                                      │  │         │  │    Call     │  │   │\n│                                      │  └─────────┘  └─────────────┘  │   │\n│                                      └─────────────────────────────────┘   │\n│                                                                              │\n│  ┌─────────────────────────────────────────────────────────────────────┐    │\n│  │  AUDIT ENQUEUE (fire-and-forget via PIPELINE service binding)       │    │\n│  │  → spine_audit_log entry with tenant_id, user_id, route, latency    │    │\n│  └─────────────────────────────────────────────────────────────────────┘    │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n### 7.2 Gateway Route Map\n\n```typescript\n// services/gateway/src/routes.ts\n\n/**\n * All routes are prefixed with `/api/v1`.\n * The Gateway matches routes and dispatches to service bindings.\n * NO route proxies to an external URL — all go to internal Workers.\n */\n  // ─── Auth & Identity ───\n  { method: \"GET\", path: \"/auth/:provider/start\", binding: null, handler: \"oauthRedirect\" },\n  { method: \"POST\", path: \"/auth/:provider/callback\", binding: \"TENANTS\", action: \"oauthCallback\" },\n  { method: \"POST\", path: \"/auth/refresh\", binding: null, handler: \"refreshJwt\" },\n  { method: \"POST\", path: \"/auth/logout\", binding: null, handler: \"revokeSession\" },\n  { method: \"GET\", path: \"/auth/me\", binding: \"TENANTS\", action: \"getCurrentUser\" },\n\n  // ─── Discovery (served by Gateway itself) ───\n  { method: \"GET\", path: \"/discovery\", binding: null, handler: \"discovery\" },\n\n  // ─── Tenant & Organization ───\n  { method: \"GET\", path: \"/tenant\", binding: \"TENANTS\", action: \"getTenant\" },\n  {\n    method: \"PATCH\",\n    path: \"/tenant\",\n    binding: \"TENANTS\",\n    action: \"updateTenant\",\n    permission: \"admin:tenant\",\n  },\n  { method: \"GET\", path: \"/tenant/orgs\", binding: \"TENANTS\", action: \"listOrgs\" },\n  {\n    method: \"POST\",\n    path: \"/tenant/orgs\",\n    binding: \"TENANTS\",\n    action: \"createOrg\",\n    permission: \"admin:org\",\n  },\n  { method: \"GET\", path: \"/tenant/users\", binding: \"TENANTS\", action: \"listUsers\" },\n  {\n    method: \"POST\",\n    path: \"/tenant/invite\",\n    binding: \"TENANTS\",\n    action: \"inviteUser\",\n    permission: \"admin:users\",\n  },\n\n  // ─── RBAC ───\n  { method: \"GET\", path: \"/rbac/roles\", binding: \"TENANTS\", action: \"listRoles\" },\n  {\n    method: \"POST\",\n    path: \"/rbac/roles\",\n    binding: \"TENANTS\",\n    action: \"createRole\",\n    permission: \"admin:rbac\",\n  },\n  { method: \"GET\", path: \"/rbac/permissions\", binding: \"TENANTS\", action: \"listPermissions\" },\n\n  // ─── API Keys ───\n  { method: \"GET\", path: \"/keys\", binding: \"TENANTS\", action: \"listApiKeys\" },\n  {\n    method: \"POST\",\n    path: \"/keys\",\n    binding: \"TENANTS\",\n    action: \"createApiKey\",\n    permission: \"admin:keys\",\n  },\n  {\n    method: \"DELETE\",\n    path: \"/keys/:id\",\n    binding: \"TENANTS\",\n    action: \"revokeApiKey\",\n    permission: \"admin:keys\",\n  },\n\n  // ─── Spine / Entities ───\n  {\n    method: \"GET\",\n    path: \"/spine/:type\",\n    binding: \"SPINE_V2\",\n    action: \"queryEntities\",\n    permission: \"spine:read\",\n  },\n  {\n    method: \"GET\",\n    path: \"/spine/:type/:id\",\n    binding: \"SPINE_V2\",\n    action: \"getEntity\",\n    permission: \"spine:read\",\n  },\n  {\n    method: \"POST\",\n    path: \"/spine/:type\",\n    binding: \"PIPELINE\",\n    action: \"createEntity\",\n    permission: \"spine:write\",\n  },\n  {\n    method: \"PATCH\",\n    path: \"/spine/:type/:id\",\n    binding: \"PIPELINE\",\n    action: \"updateEntity\",\n    permission: \"spine:write\",\n  },\n\n  // ─── Capabilities ───\n  {\n    method: \"POST\",\n    path: \"/capabilities/:name\",\n    binding: \"ACT\",\n    action: \"executeCapability\",\n    permission: \"mcp:invoke\",\n  },\n  { method: \"GET\", path: \"/capabilities\", binding: \"AGENT_REGISTRY\", action: \"listCapabilities\" },\n\n  // ─── Twin / AI ───\n  {\n    method: \"POST\",\n    path: \"/twin/propose\",\n    binding: \"TWIN_ORCHESTRATOR\",\n    action: \"propose\",\n    permission: \"twin:propose\",\n  },\n  { method: \"GET\", path: \"/twin/status\", binding: \"TWIN_ORCHESTRATOR\", action: \"getStatus\" },\n\n  // ─── Governance ───\n  { method: \"GET\", path: \"/govern/proposals\", binding: \"GOVERN\", action: \"listProposals\" },\n  {\n    method: \"POST\",\n    path: \"/govern/proposals/:id/approve\",\n    binding: \"GOVERN\",\n    action: \"approveProposal\",\n    permission: \"govern:approve\",\n  },\n  {\n    method: \"POST\",\n    path: \"/govern/proposals/:id/reject\",\n    binding: \"GOVERN\",\n    action: \"rejectProposal\",\n    permission: \"govern:approve\",\n  },\n\n  // ─── Knowledge ───\n  { method: \"GET\", path: \"/knowledge/search\", binding: \"KNOWLEDGE\", action: \"search\" },\n  {\n    method: \"POST\",\n    path: \"/knowledge/documents\",\n    binding: \"KNOWLEDGE\",\n    action: \"ingestDocument\",\n    permission: \"knowledge:write\",\n  },\n\n  // ─── Intelligence ───\n  { method: \"GET\", path: \"/intelligence/signals\", binding: \"INTELLIGENCE\", action: \"getSignals\" },\n  { method: \"GET\", path: \"/intelligence/insights\", binding: \"INTELLIGENCE\", action: \"getInsights\" },\n\n  // ─── Connectors ───\n  {\n    method: \"POST\",\n    path: \"/connectors\",\n    binding: \"CONNECTOR\",\n    action: \"createConnector\",\n    permission: \"connector:create\",\n  },\n  { method: \"GET\", path: \"/connectors\", binding: \"CONNECTOR\", action: \"listConnectors\" },\n  {\n    method: \"POST\",\n    path: \"/connectors/:id/sync\",\n    binding: \"CONNECTOR_SYNC\",\n    action: \"triggerSync\",\n    permission: \"connector:sync\",\n  },\n\n  // ─── Admin ───\n  {\n    method: \"GET\",\n    path: \"/admin/tenants\",\n    binding: \"ADMIN\",\n    action: \"listTenants\",\n    permission: \"admin:*\",\n  },\n  {\n    method: \"GET\",\n    path: \"/admin/audit\",\n    binding: \"ADMIN\",\n    action: \"getAuditLog\",\n    permission: \"admin:audit\",\n  },\n\n  // ─── MCP (SSE transport) ───\n  { method: \"POST\", path: \"/mcp/sessions\", binding: null, handler: \"mcpCreateSession\" },\n  { method: \"POST\", path: \"/mcp/tools\", binding: \"MCP_CONNECTOR\", action: \"listTools\" },\n  {\n    method: \"POST\",\n    path: \"/mcp/invoke\",\n    binding: \"MCP_CONNECTOR\",\n    action: \"invokeTool\",\n    permission: \"mcp:invoke\",\n  },\n  { method: \"GET\", path: \"/mcp/health\", binding: \"MCP_CONNECTOR\", action: \"health\" },\n  { method: \"DELETE\", path: \"/mcp/sessions/:id\", binding: null, handler: \"mcpDeleteSession\" },\n\n  // ─── Webhooks (inbound from providers) ───\n  {\n    method: \"POST\",\n    path: \"/webhooks/:provider\",\n    binding: \"WEBHOOK_INGRESS\",\n    action: \"receiveWebhook\",\n    auth: \"webhook_signature\",\n  },\n\n  // ─── Health ───\n  { method: \"GET\", path: \"/health\", binding: null, handler: \"healthCheck\" },\n  { method: \"GET\", path: \"/health/deep\", binding: null, handler: \"deepHealthCheck\" },\n];\n\nexport interface RouteDefinition {\n  method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n  path: string;\n  binding: string | null; // null = handled by Gateway itself\n  action?: string; // method name on the binding target\n  handler?: string; // local Gateway handler function name\n  permission?: string; // required permission (coarse check)\n  auth?: \"jwt\" | \"api_key\" | \"webhook_signature\" | \"none\";\n}\n```\n\n---\n\n## 8. Rate Limiting\n\n### 8.1 Rate Limit Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                         RATE LIMIT TIERS                                     │\n├─────────────────────────────────────────────────────────────────────────────┤\n│  Tier           │ Request/min │ Burst │ Concurrent │ MCP Streams │ Webhooks │\n│  ───────────────┼─────────────┼───────┼────────────┼─────────────┼──────────│\n│  free           │ 60          │ 10    │ 5          │ 1           │ 10/hr    │\n│  pro            │ 600         │ 100   │ 25         │ 3           │ 100/hr   │\n│  enterprise     │ 6,000       │ 500   │ 100        │ 10          │ 1,000/hr │\n│  customer_zero  │ unlimited   │ unlimited │ unlimited │ unlimited │ unlimited│\n├─────────────────────────────────────────────────────────────────────────────┤\n│  Rate limit is enforced at TWO levels:                                      │\n│    1. Gateway edge — per-tenant token bucket (CF Rate Limiting API)         │\n│    2. Service — per-tenant queue depth (backpressure)                       │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n### 8.2 Token Bucket Implementation\n\n```typescript\n// services/gateway/src/rate-limit.ts\n\n/**\n * Token bucket stored in KV for edge rate limiting.\n * Key: `ratelimit:<tenant_id>:<bucket_type>`\n * Bucket types: \"api\", \"mcp\", \"webhook\", \"sync\"\n */\nexport interface RateLimitBucket {\n  tokens: number; // current available tokens\n  last_refill: number; // epoch ms of last refill\n  window_start: number; // epoch ms of current window start\n}\n\nexport async function checkRateLimit(\n  env: Env,\n  tenantId: string,\n  tier: string,\n  bucketType: \"api\" | \"mcp\" | \"webhook\" | \"sync\"\n): Promise<RateLimitResult> {\n  const config = RATE_LIMIT_CONFIG[tier][bucketType];\n  const key = `ratelimit:${tenantId}:${bucketType}`;\n\n  const bucketData = await env.KV.get(key);\n  const now = Date.now();\n\n  let bucket: RateLimitBucket = bucketData\n    ? JSON.parse(bucketData)\n    : { tokens: config.burst, last_refill: now, window_start: now };\n\n  // Refill tokens based on elapsed time\n  const elapsedMs = now - bucket.last_refill;\n  const tokensToAdd = (elapsedMs / 1000) * (config.requestsPerMinute / 60);\n  bucket.tokens = Math.min(config.burst, bucket.tokens + tokensToAdd);\n  bucket.last_refill = now;\n\n  // Window rollover\n  if (now - bucket.window_start > config.windowMs) {\n    bucket.window_start = now;\n    bucket.tokens = config.burst;\n  }\n\n  if (bucket.tokens < 1) {\n    // Save bucket state (short TTL)\n    await env.KV.put(key, JSON.stringify(bucket), { expirationTtl: 60 });\n\n    const retryAfter = Math.ceil((1 - bucket.tokens) / (config.requestsPerMinute / 60));\n    return {\n      allowed: false,\n      limit: config.requestsPerMinute,\n      remaining: 0,\n      reset: Math.ceil((bucket.window_start + config.windowMs) / 1000),\n      retryAfter,\n    };\n  }\n\n  // Consume token\n  bucket.tokens -= 1;\n  await env.KV.put(key, JSON.stringify(bucket), { expirationTtl: 60 });\n\n  return {\n    allowed: true,\n    limit: config.requestsPerMinute,\n    remaining: Math.floor(bucket.tokens),\n    reset: Math.ceil((bucket.window_start + config.windowMs) / 1000),\n    retryAfter: 0,\n  };\n}\n\nexport interface RateLimitResult {\n  allowed: boolean;\n  limit: number;\n  remaining: number;\n  reset: number;\n  retryAfter: number;\n}\n\n/**\n * Rate limit response headers (RFC 6585 + IETF draft):\n *   X-RateLimit-Limit: 600\n *   X-RateLimit-Remaining: 599\n *   X-RateLimit-Reset: 1719916800\n *   Retry-After: 60 (when limited)\n */\n```\n\n---\n\n## 9. Request Validation\n\n### 9.1 Validation Pipeline\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                      REQUEST VALIDATION PIPELINE                             │\n│                                                                              │\n│  Stage 1: Structural (Zod schemas)                                            │\n│    ├── Content-Type header check                                              │\n│    ├── Body JSON parsing                                                      │\n│    ├── Zod schema validation                                                  │\n│    └── Reject: 400 with `zodError.errors` array                               │\n│                                                                              │\n│  Stage 2: Semantic                                                            │\n│    ├── Tenant exists and is active                                            │\n│    ├── User exists in tenant                                                  │\n│    ├── Role permits this action                                               │\n│    └── Reject: 403 with reason code                                           │\n│                                                                              │\n│  Stage 3: Business Rules                                                      │\n│    ├── Plan limits (e.g., max connectors)                                     │\n│    ├── Rate limit compliance                                                  │\n│    └── Reject: 402 (payment required) or 429                                  │\n│                                                                              │\n│  Stage 4: Security                                                            │\n│    ├── Input sanitization (XSS, SQLi patterns)                                │\n│    ├── Max body size (1MB for API, 10MB for file upload)                      │\n│    └── Reject: 413 or 400                                                     │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n### 9.2 Zod Schema Examples\n\n```typescript\n// packages/validation/src/schemas.ts\n\n/**\n * Base tenant-scoped request. All request bodies extend this.\n * NOTE: tenant_id is NEVER in the body. It comes from JWT/API key resolution.\n */\n  // Intentionally empty — tenant context is injected by Gateway\n});\n\n  type: z.string().min(1).max(64),\n  data: z.record(z.unknown()),\n  org_id: z.string().optional(), // optional — defaults to user's org\n  relationships: z\n    .array(\n      z.object({\n        target_type: z.string(),\n        target_id: z.string(),\n        relation_type: z.string(),\n      })\n    )\n    .optional(),\n  source: z.enum([\"api\", \"webhook\", \"mcp\", \"ui\", \"twin\"]).default(\"api\"),\n});\n\n  capability: z.string().regex(/^[a-z0-9_]+\\.[a-z0-9_]+\\.[a-z0-9_]+$/),\n  payload: z.record(z.unknown()),\n  async: z.boolean().default(false),\n  correlation_id: z.string().uuid().optional(),\n});\n\n  context: z.string().max(10000),\n  entity_ids: z.array(z.string().uuid()).optional(),\n  projection: z\n    .enum([\n      \"sales\",\n      \"cs\",\n      \"finance\",\n      \"executive\",\n      \"architecture\",\n      \"operations\",\n      \"personal\",\n      \"developer\",\n    ])\n    .optional(),\n  max_proposals: z.number().int().min(1).max(10).default(3),\n});\n\n  // Webhooks are validated by provider-specific signature verification\n  // Body is passed through to webhook-ingress service\n  provider: z.string(),\n  signature: z.string().optional(),\n  timestamp: z.string().datetime().optional(),\n  event_type: z.string(),\n  payload: z.record(z.unknown()),\n});\n```\n\n---\n\n## 10. Transport Layer\n\n### 10.1 Supported Transports\n\n| Transport     | Protocol           | Use Case                       | Endpoint                                         | Auth                   |\n| ------------- | ------------------ | ------------------------------ | ------------------------------------------------ | ---------------------- |\n| **HTTP/REST** | HTTP/1.1, HTTP/2   | Primary API for SDKs, webhooks | `api.integratewise.ai/api/v1/*`                  | JWT or API Key         |\n| **SSE**       | Server-Sent Events | MCP streams, real-time signals | `mcp.integratewise.ai/v1/mcp`                    | JWT                    |\n| **WebSocket** | WS over TLS        | Real-time workbench (future)   | `ws.integratewise.ai/v1/stream`                  | JWT                    |\n| **MCP**       | stdio / SSE        | AI assistant integration       | `mcp.integratewise.ai/v1/mcp`                    | JWT                    |\n| **Webhooks**  | HTTP POST          | Provider callbacks             | `api.integratewise.ai/api/v1/webhooks/:provider` | Signature verification |\n\n### 10.2 SSE / MCP Transport Specification\n\n```typescript\n// packages/mcp-types/src/transport.ts\n\n/**\n * MCP SSE Transport for Inbound MCP Pool.\n * Each SSE connection maps to one session in KV.\n */\nexport interface McpSseSession {\n  session_id: string;\n  tenant_id: string;\n  user_id: string;\n  created_at: number;\n  last_event_at: number;\n  event_stream: ReadableStream; // Cloudflare Workers stream\n  tools_version: string; // Discovery cache version\n}\n\n/**\n * SSE Event Types\n */\nexport type McpSseEvent =\n  | { type: \"connected\"; session_id: string; tenant_id: string }\n  | { type: \"tool_catalog\"; tools: McpToolDefinition[] }\n  | { type: \"invocation_result\"; invocation_id: string; result: unknown }\n  | { type: \"signal\"; signal_type: string; payload: unknown }\n  | { type: \"error\"; code: string; message: string }\n  | { type: \"ping\" }; // keep-alive every 30s\n\n/**\n * MCP Tool Definition (assembled from Discovery)\n */\nexport interface McpToolDefinition {\n  name: string;\n  description: string;\n  inputSchema: JSONSchema7;\n  capability: string; // e.g., \"salesforce.opportunity.update\"\n  permission: string; // required RBAC permission\n  provider: string;\n  read_only: boolean;\n}\n\n/**\n * Session lifecycle:\n *   1. Client POST /mcp/sessions → Gateway validates JWT\n *   2. Gateway creates McpSseSession in KV (TTL = 1 hour)\n *   3. Gateway returns SSE stream with initial `connected` event\n *   4. Client POST /mcp/tools → Gateway assembles catalog from Discovery\n *   5. Client POST /mcp/invoke → Gateway routes to MCP_CONNECTOR binding\n *   6. Result streamed back via SSE\n *   7. Client DELETE /mcp/sessions/:id or TTL expiry → cleanup\n */\n```\n\n### 10.3 Webhook Transport\n\n```typescript\n// services/webhook-ingress/src/index.ts\n\n/**\n * Webhook ingress is a SEPARATE Worker (not the Gateway).\n * It receives raw provider webhooks, verifies signatures,\n * normalizes payloads, and enqueues to NORMALIZER_QUEUE.\n *\n * Why separate from Gateway?\n *   - Webhooks have different scaling patterns (burst traffic)\n *   - Signature verification is provider-specific\n *   - Failures should not block API traffic\n */\n\nexport interface WebhookIngressConfig {\n  provider: string; // \"salesforce\", \"hubspot\", \"stripe\", \"github\", ...\n  secret_header: string; // \"X-HubSpot-Signature\", \"Stripe-Signature\", ...\n  secret_env_key: string; // env var name for verification secret\n  idempotency_header: string; // \"X-Idempotency-Key\", \"X-Delivery-ID\", ...\n}\n\n/**\n * Webhook processing flow:\n *   1. Provider POST → webhook-ingress Worker\n *   2. Verify signature (provider-specific HMAC)\n *   3. Check idempotency (KV dedup key)\n *   4. Extract tenant_id from connection mapping (D1: connector_tenant_map)\n *   5. Normalize to WebhookEnvelope\n *   6. Enqueue to NORMALIZER_QUEUE\n *   7. Return 200 (provider retries if not 200)\n */\nexport interface WebhookEnvelope {\n  webhook_id: string; // idempotency key\n  tenant_id: string;\n  provider: string;\n  event_type: string;\n  received_at: number;\n  payload: unknown;\n  signature_valid: boolean;\n  raw_headers: Record<string, string>;\n}\n```\n\n---\n\n## 11. Discovery Subsystem\n\n### 11.1 Discovery Contract\n\nDiscovery is the **capability handshake**. Every consumer — human, AI agent, SDK — calls `GET /api/v1/discovery` to learn what exists and what they can do. This is a **living contract**: it changes as connectors are added, roles are modified, or plan limits are adjusted.\n\n```typescript\n// packages/sdk/src/discovery.ts\n\n/**\n * Discovery Response — the single source of truth for consumer capability.\n * Cached client-side for 5 minutes. Gateway caches in KV for 60 seconds.\n */\nexport interface DiscoveryResponse {\n  /** Identity context */\n  identity: {\n    user_id: string;\n    email: string;\n    name: string;\n    tenant_id: string;\n    tenant_name: string;\n    org_id: string;\n    org_name: string;\n    role: string;\n    tier: string;\n  };\n\n  /** Workspace state */\n  workspace: {\n    connected_connectors: ConnectorInfo[];\n    plan_limits: PlanLimits;\n    features_enabled: string[];\n    onboarding_complete: boolean;\n  };\n\n  /** Available projections (8 lenses) */\n  projections: ProjectionInfo[];\n\n  /** Named capabilities this tenant can execute */\n  capabilities: CapabilityInfo[];\n\n  /** Gated features based on plan */\n  features: FeatureInfo[];\n\n  /** Role-based navigation structure */\n  navigation: NavigationNode[];\n\n  /** User personalization */\n  personalization: {\n    recent_entities: string[];\n    preferred_projection: string;\n    pinned_capabilities: string[];\n    theme: \"light\" | \"dark\" | \"system\";\n  };\n\n  /** Current limits and usage */\n  limits: {\n    rate_limit: RateLimitInfo;\n    storage_used_bytes: number;\n    storage_limit_bytes: number;\n    api_calls_this_period: number;\n    api_call_limit: number;\n  };\n\n  /** MCP endpoint (for AI assistants) */\n  mcp: {\n    endpoint: string;\n    version: string;\n    transport: \"sse\";\n    authenticated: boolean;\n  };\n\n  /** Version metadata */\n  version: {\n    api: string;\n    schema: string;\n    discovery_cache_version: string;\n  };\n}\n\nexport interface ConnectorInfo {\n  id: string;\n  provider: string;\n  name: string;\n  status: \"connected\" | \"syncing\" | \"error\" | \"disconnected\";\n  last_sync_at: string | null;\n  entity_count: number;\n  capabilities: string[]; // capabilities this connector enables\n}\n\nexport interface ProjectionInfo {\n  id: string;\n  name: string; // \"Sales\", \"CS\", \"Finance\", ...\n  description: string;\n  icon: string;\n  enabled: boolean;\n  required_connectors: string[];\n  required_role: string;\n}\n\nexport interface CapabilityInfo {\n  name: string; // e.g., \"analyze_account\"\n  description: string;\n  category: string;\n  input_schema: JSONSchema7;\n  output_schema: JSONSchema7;\n  permission: string;\n  read_only: boolean;\n  providers: string[]; // which connectors support this\n  deprecated: boolean;\n  deprecation_note?: string;\n}\n\nexport interface PlanLimits {\n  max_connectors: number;\n  max_users: number;\n  max_api_calls_per_minute: number;\n  max_storage_bytes: number;\n  max_mcp_streams: number;\n  retention_days: number;\n  features: string[];\n}\n```\n\n### 11.2 Discovery Assembly Flow\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                     DISCOVERY ASSEMBLY (Inside Gateway)                      │\n│                                                                              │\n│  Client: GET /api/v1/discovery + Bearer JWT                                  │\n│       │                                                                      │\n│       ▼                                                                      │\n│  ┌─────────────────────────────────────────────────────────────────────┐    │\n│  │  1. Check KV cache: `discovery:<tenant_id>:<role>:<version>`       │    │\n│  │     → Cache hit? Return cached response (60s TTL)                  │    │\n│  └─────────────────────────────────────────────────────────────────────┘    │\n│       │ Cache miss                                                          │\n│       ▼                                                                      │\n│  ┌─────────────────────────────────────────────────────────────────────┐    │\n│  │  2. Parallel service binding calls:                                  │    │\n│  │     ├── TENANTS     → getTenant, getUser, getOrg                   │    │\n│  │     ├── CONNECTOR   → listConnectors (filtered by tenant)          │    │\n│  │     ├── AGENT_REGISTRY → listCapabilities (filtered by role)       │    │\n│  │     ├── L2          → listProjections (filtered by tenant config)  │    │\n│  │     └── BILLING     → getPlanLimits                                │    │\n│  └─────────────────────────────────────────────────────────────────────┘    │\n│       │                                                                      │\n│       ▼                                                                      │\n│  ┌─────────────────────────────────────────────────────────────────────┐    │\n│  │  3. Assemble DiscoveryResponse                                       │    │\n│  │  4. Write to KV cache                                                │    │\n│  │  5. Return to client                                                 │    │\n│  └─────────────────────────────────────────────────────────────────────┘    │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## 12. Security Boundaries\n\n### 12.1 Trust Zones\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  ZONE 0: Internet (Untrusted)                                               │\n│  ├── Any IP can reach Gateway                                               │\n│  ├── WAF blocks obvious attacks                                             │\n│  └── Rate limits prevent abuse                                              │\n├─────────────────────────────────────────────────────────────────────────────┤\n│  ZONE 1: Gateway (Semi-Trusted)                                             │\n│  ├── Authenticated requests only                                            │\n│  ├── Tenant context resolved and validated                                  │\n│  ├── No direct DB access — only service bindings                            │\n│  └── Audit log enqueued for every request                                   │\n├─────────────────────────────────────────────────────────────────────────────┤\n│  ZONE 2: Internal Services (Trusted)                                        │\n│  ├── Service bindings only — no public URLs                                 │\n│  ├── Tenant context propagated implicitly                                   │\n│  ├── Fine-grained RBAC enforced                                             │\n│  └── Direct D1/KV access permitted                                          │\n├─────────────────────────────────────────────────────────────────────────────┤\n│  ZONE 3: Provider Fabric (External, Untrusted)                              │\n│  ├── Credentials stored in Nango vault / CF Secrets                         │\n│  ├── Entity360 injected before outbound calls                               │\n│  └── Circuit breakers protect against provider failures                     │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n### 12.2 Security Checklist (P0)\n\n| #   | Control                    | Implementation                                                   | Verification                              |\n| --- | -------------------------- | ---------------------------------------------------------------- | ----------------------------------------- |\n| 1   | TLS 1.3 only               | Cloudflare edge certificate                                      | `curl -I -v https://api.integratewise.ai` |\n| 2   | HSTS header                | `Strict-Transport-Security: max-age=31536000; includeSubDomains` | Response headers                          |\n| 3   | CORS whitelist             | `app.integratewise.ai`, `*.pages.dev`                            | Preflight response                        |\n| 4   | Content Security Policy    | `default-src 'self'` on web assets                               | Response headers                          |\n| 5   | JWT signature verification | RS256 with Cloudflare Secrets                                    | Unit test: tampered JWT → 403             |\n| 6   | API key hashing            | Argon2id, never store plaintext                                  | DB inspection                             |\n| 7   | Session revocation         | KV blacklist + TTL                                               | Test: revoked JWT → 403                   |\n| 8   | Rate limiting              | Token bucket per tenant                                          | Load test: 600 req/min → 429              |\n| 9   | Input validation           | Zod schemas on all routes                                        | Fuzz test: invalid payloads → 400         |\n| 10  | Audit logging              | Every request → spine_audit_log                                  | D1 query: count audit rows                |\n| 11  | Secret rotation            | Cloudflare Secrets API, 90-day max age                           | Admin dashboard check                     |\n| 12  | No .env in repo            | `.gitignore` + secret scanning                                   | `git log --all -- .env`                   |\n\n---\n\n## 13. Integration Points with Adjacent Layers\n\n### 13.1 Identity → Capability Layer\n\n| Integration               | Direction       | Data                          | Mechanism                       |\n| ------------------------- | --------------- | ----------------------------- | ------------------------------- |\n| Gateway → `tenants`       | Request         | Auth callbacks, user CRUD     | Service binding `TENANTS`       |\n| Gateway → `act`           | Request         | Capability execution          | Service binding `ACT`           |\n| Gateway → `mcp-connector` | Bidirectional   | MCP tool catalog, invocations | Service binding `MCP_CONNECTOR` |\n| Gateway → `govern`        | Request         | Proposal approval/rejection   | Service binding `GOVERN`        |\n| Gateway → `pipeline`      | Fire-and-forget | Audit log entries             | Service binding `PIPELINE`      |\n\n### 13.2 Identity → Continuity Layer\n\n| Integration            | Direction       | Data                           | Mechanism                               |\n| ---------------------- | --------------- | ------------------------------ | --------------------------------------- |\n| Gateway → `continuity` | Fire-and-forget | Session events (login, logout) | Queue `SESSION_EVENTS_QUEUE`            |\n| `continuity` → Gateway | Background      | Memory decay notifications     | Not applicable (no callback to Gateway) |\n\n### 13.3 Identity → Governance Layer\n\n| Integration        | Direction       | Data               | Mechanism                                      |\n| ------------------ | --------------- | ------------------ | ---------------------------------------------- |\n| Gateway → `govern` | Request         | Approval actions   | Service binding `GOVERN`                       |\n| `govern` → Gateway | Response        | Approval status    | Service binding response                       |\n| Gateway audit      | Fire-and-forget | All auth decisions | Service binding `PIPELINE` → `spine_audit_log` |\n\n### 13.4 Discovery ↔ All Layers\n\nDiscovery is the **cross-layer contract**. It reads from:\n\n- **Identity Layer:** User role, tenant, org\n- **Capability Layer:** Available capabilities from `agent-registry`\n- **Continuity Layer:** Connected connectors, entity counts\n- **Governance Layer:** Enabled features per plan tier\n\n---\n\n## 14. Data Model (D1 Schema)\n\n```sql\n-- Identity & Ingress Layer D1 tables\n-- All tables have `tenant_id` as the first column in PRIMARY KEY or INDEX\n\n-- Tenants\nCREATE TABLE tenants (\n  id TEXT PRIMARY KEY,           -- e.g., \"acme_corp\"\n  name TEXT NOT NULL,\n  slug TEXT UNIQUE NOT NULL,\n  status TEXT NOT NULL DEFAULT 'active', -- active, suspended, pending_deletion\n  tier TEXT NOT NULL DEFAULT 'free',     -- free, pro, enterprise, customer_zero\n  created_at TEXT NOT NULL DEFAULT (datetime('now')),\n  updated_at TEXT NOT NULL DEFAULT (datetime('now')),\n  owner_id TEXT NOT NULL,\n  settings TEXT -- JSON: { onboarding_complete, preferred_projection, ... }\n);\n\n-- Organizations (within a tenant)\nCREATE TABLE orgs (\n  id TEXT PRIMARY KEY,\n  tenant_id TEXT NOT NULL,\n  name TEXT NOT NULL,\n  slug TEXT NOT NULL,\n  created_at TEXT NOT NULL DEFAULT (datetime('now')),\n  UNIQUE(tenant_id, slug)\n);\n\n-- Users\nCREATE TABLE users (\n  id TEXT PRIMARY KEY,\n  tenant_id TEXT NOT NULL,\n  org_id TEXT NOT NULL,\n  email TEXT NOT NULL,\n  name TEXT,\n  avatar_url TEXT,\n  auth_provider TEXT NOT NULL,   -- google, github, email\n  auth_provider_id TEXT,         -- external ID from auth provider\n  status TEXT NOT NULL DEFAULT 'active',\n  last_login_at TEXT,\n  created_at TEXT NOT NULL DEFAULT (datetime('now')),\n  UNIQUE(tenant_id, email)\n);\n\n-- RBAC Roles\nCREATE TABLE rbac_roles (\n  id TEXT PRIMARY KEY,\n  tenant_id TEXT NOT NULL,\n  name TEXT NOT NULL,            -- owner, admin, member, viewer, api\n  permissions TEXT NOT NULL,     -- JSON array: [\"spine:read\", \"twin:propose\", ...]\n  inherits_from TEXT,            -- role ID or null\n  created_at TEXT NOT NULL DEFAULT (datetime('now')),\n  UNIQUE(tenant_id, name)\n);\n\n-- User-Role Mapping\nCREATE TABLE user_roles (\n  user_id TEXT NOT NULL,\n  role_id TEXT NOT NULL,\n  tenant_id TEXT NOT NULL,\n  org_id TEXT NOT NULL,\n  assigned_at TEXT NOT NULL DEFAULT (datetime('now')),\n  assigned_by TEXT NOT NULL,\n  PRIMARY KEY (user_id, role_id),\n  FOREIGN KEY (user_id) REFERENCES users(id),\n  FOREIGN KEY (role_id) REFERENCES rbac_roles(id)\n);\n\n-- API Keys\nCREATE TABLE api_keys (\n  id TEXT PRIMARY KEY,\n  tenant_id TEXT NOT NULL,\n  org_id TEXT NOT NULL,\n  name TEXT NOT NULL,\n  key_hash TEXT NOT NULL,        -- Argon2id hash\n  scope TEXT NOT NULL,           -- JSON array\n  rate_limit_bucket TEXT NOT NULL DEFAULT 'default',\n  created_by TEXT NOT NULL,\n  created_at TEXT NOT NULL DEFAULT (datetime('now')),\n  expires_at TEXT,\n  revoked_at TEXT,\n  last_used_at TEXT\n);\n\n-- Audit Log (written via Pipeline — Gateway enqueues, Pipeline commits)\nCREATE TABLE spine_audit_log (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  tenant_id TEXT NOT NULL,\n  user_id TEXT,\n  api_key_id TEXT,\n  action TEXT NOT NULL,          -- route + method\n  resource_type TEXT,\n  resource_id TEXT,\n  status_code INTEGER,\n  latency_ms INTEGER,\n  correlation_id TEXT NOT NULL,\n  ip_hash TEXT,                  -- privacy-preserving\n  user_agent_hash TEXT,\n  timestamp TEXT NOT NULL DEFAULT (datetime('now')),\n  details TEXT                   -- JSON: request summary\n);\nCREATE INDEX idx_audit_tenant ON spine_audit_log(tenant_id, timestamp);\nCREATE INDEX idx_audit_correlation ON spine_audit_log(correlation_id);\n\n-- Invitation (for org invites)\nCREATE TABLE invitations (\n  id TEXT PRIMARY KEY,\n  tenant_id TEXT NOT NULL,\n  org_id TEXT NOT NULL,\n  email TEXT NOT NULL,\n  role_id TEXT NOT NULL,\n  invited_by TEXT NOT NULL,\n  token_hash TEXT NOT NULL,      -- hash of the invite token\n  expires_at TEXT NOT NULL,\n  accepted_at TEXT,\n  created_at TEXT NOT NULL DEFAULT (datetime('now')),\n  UNIQUE(tenant_id, email) WHERE accepted_at IS NULL\n);\n```\n\n---\n\n## 15. Error Handling & Response Contract\n\nAll Gateway responses follow a uniform error envelope:\n\n```typescript\n// packages/auth-types/src/errors.ts\n\nexport interface GatewayError {\n  error: {\n    code: string; // machine-readable: \"invalid_token\", \"rate_limited\", ...\n    message: string; // human-readable\n    details?: unknown; // context-specific (Zod errors, etc.)\n    request_id: string; // correlation_id for tracing\n    timestamp: string; // ISO 8601\n  };\n}\n\n/**\n * HTTP Status → Error Code Mapping\n */\n  400: {\n    invalid_json: \"Request body is not valid JSON\",\n    validation_failed: \"Request failed schema validation\",\n    missing_field: \"Required field is missing\",\n  },\n  401: {\n    missing_token: \"Authorization header is required\",\n    invalid_token: \"Token is malformed or signature is invalid\",\n    expired_token: \"Token has expired\",\n  },\n  403: {\n    insufficient_permissions: \"Role does not have required permission\",\n    invalid_tenant_claim: \"JWT tenant claim mismatch\",\n    tenant_suspended: \"Tenant account is suspended\",\n    session_revoked: \"Session has been revoked\",\n    cross_tenant_access: \"Access to another tenant is forbidden\",\n  },\n  404: {\n    route_not_found: \"The requested endpoint does not exist\",\n    resource_not_found: \"The requested resource does not exist\",\n  },\n  429: {\n    rate_limited: \"Too many requests\",\n  },\n  500: {\n    internal_error: \"An unexpected error occurred\",\n    service_unavailable: \"A downstream service is unavailable\",\n  },\n} as const;\n```\n\n---\n\n## 16. Operational Concerns\n\n### 16.1 Gateway Deployment\n\n```yaml\n# services/gateway/wrangler.toml\nname = \"integratewise-gateway\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2026-07-01\"\n\n# Service bindings to all 26 services\n[[services]]\nbinding = \"TENANTS\"\nservice = \"iw-tenants\"\n\n[[services]]\nbinding = \"CONNECTOR\"\nservice = \"iw-connector\"\n\n# ... (all 26 bindings)\n\n[[services]]\nbinding = \"MCP_CONNECTOR\"\nservice = \"iw-mcp-connector\"\n\n# KV namespaces\n[[kv_namespaces]]\nbinding = \"KV\"\nid = \"<discovery-cache-kv-id>\"\n\n# Rate limiting (Cloudflare Rate Limiting API)\n[limits]\ncpu_ms = 50  # Gateway is lightweight — auth, route, bind\n\n# Secrets (set via `wrangler secret put`)\n# JWT_PRIVATE_KEY — RSA private key for signing\n# JWT_PUBLIC_KEY — RSA public key for verification (also cached in KV)\n```\n\n### 16.2 Monitoring & Alerting\n\n| Metric                    | Source            | Threshold             | Alert |\n| ------------------------- | ----------------- | --------------------- | ----- |\n| Gateway p99 latency       | Workers Analytics | > 100ms               | P2    |\n| Auth failure rate         | `spine_audit_log` | > 5% of requests      | P1    |\n| Rate limit hits           | KV counters       | > 10% of tenants      | P2    |\n| JWT verification errors   | Gateway logs      | > 1% of requests      | P1    |\n| Discovery cache miss rate | KV stats          | > 20%                 | P3    |\n| MCP session count         | KV key count      | Approaching KV limits | P2    |\n\n### 16.3 Scaling Considerations\n\n- **Gateway:** Single Worker handles all ingress. Cloudflare auto-scales. CPU limit 50ms per request.\n- **Rate limit buckets:** KV-backed, eventual consistency acceptable. For stricter consistency, use D1 counter table.\n- **Session store:** KV with TTL. Sessions are ephemeral — no need for strong consistency.\n- **Discovery cache:** KV with 60s TTL. Stale data is acceptable (capabilities don't change that fast).\n- **Webhook-ingress:** Separate Worker to isolate burst traffic from API traffic.\n\n---\n\n## 17. Changelog\n\n| Version | Date       | Change                                                                                                                                                                                                                       |\n| ------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| 2.0.0   | 2026-07-02 | Initial v2.0 specification. Unified Identity + Ingress into single domain. Formalized JWT claims, API key model, RBAC hierarchy, rate limit tiers, MCP SSE transport, Discovery contract. Aligned with 6-layer architecture. |\n\n---\n\n**END OF SPECIFICATION**\n\n_This document is the definitive reference for the Identity & Ingress Layers of the IntegrateWise Continuity Bridge v2.0. All downstream implementations must conform to the interfaces, flows, and security boundaries defined herein._\n"}