{"_id":"@alfadocs/auth","_rev":"5-4b89d2f6e37a08d0d908613fbd822f40","name":"@alfadocs/auth","dist-tags":{"latest":"0.5.0"},"versions":{"0.4.0":{"name":"@alfadocs/auth","version":"0.4.0","keywords":["oauth2","alfadocs","auth","bridge","supabase"],"author":{"name":"AlfaDocs"},"license":"MIT","_id":"@alfadocs/auth@0.4.0","maintainers":[{"name":"lockymic-alfa","email":"michael@alfadocs.com"}],"dist":{"shasum":"f8419ba76c6bfa6f95e8f94a9e34559977624cce","tarball":"https://registry.npmjs.org/@alfadocs/auth/-/auth-0.4.0.tgz","fileCount":34,"integrity":"sha512-odYOyFjoy4ijmzendeHWnySXir0hJjqk7VJuRKsA2+3tlPJj/8XqsNFGE0lAL/SBpWD/V0gpycuW1jVKu1En0g==","signatures":[{"sig":"MEUCIGb+q1PYKnOK/PmMWqzOuufNmKIE2GrizHetsBL3qdoyAiEAlhbeNVwUvfUbyGVNkZ8P2yfYXDxJcVWE286bHrt4YoA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":92623},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","default":"./dist/react/index.js"},"./supabase-bridge":{"types":"./dist/supabase-bridge/index.d.ts","import":"./dist/supabase-bridge/index.js","default":"./dist/supabase-bridge/index.js"}},"gitHead":"ffa07b392124f8978023308b468772e349bc1fb9","scripts":{"test":"vitest run","build":"tsc && tsc -p tsconfig.react.json","prepare":"npm run build","test:deno":"deno test tests/deno/","local:test-app":"npm run build && node tests/local-app/server.mjs"},"_npmUser":{"name":"lockymic-alfa","email":"michael@alfadocs.com"},"_npmVersion":"10.9.8","description":"Bridgeable Alfadocs auth core with infrastructure adapters","directories":{},"_nodeVersion":"22.22.3","dependencies":{"jose":"^6.2.2"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^25.0.1","react":"^19.2.0","dotenv":"^17.4.1","undici":"^8.0.2","vitest":"^3.2.4","deno-bin":"^2.2.7","react-dom":"^19.2.0","typescript":"^5.6.3","@types/react":"^19.2.0","@testing-library/react":"^16.1.0"},"peerDependencies":{"react":"^18.2.0 || ^19.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/auth_0.4.0_1780062523606_0.6600656411335057","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@alfadocs/auth","version":"0.4.1","keywords":["oauth2","alfadocs","auth","bridge","supabase"],"author":{"name":"AlfaDocs"},"license":"BUSL-1.1","_id":"@alfadocs/auth@0.4.1","maintainers":[{"name":"lockymic-alfa","email":"michael@alfadocs.com"}],"dist":{"shasum":"64930342eda3c498f9b317a68572b99e0921ba8e","tarball":"https://registry.npmjs.org/@alfadocs/auth/-/auth-0.4.1.tgz","fileCount":35,"integrity":"sha512-E6mJjwYl06B2SYPvx7X+GkLuPsstTQBk20H/vFiSFFet1Iy+1pygNwtRmsz/JmAI4udTcsd0yVWiv3xrhes10w==","signatures":[{"sig":"MEQCIHXDB5i2mkJ+8iyH/sDdbUtuCinKLL4RwHVo0ITkTOErAiB/rSVp+SlIfEvNYqwEnwtWl1EktWXYjRHtqxG/dppQXQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":97100},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","default":"./dist/react/index.js"},"./supabase-bridge":{"types":"./dist/supabase-bridge/index.d.ts","import":"./dist/supabase-bridge/index.js","default":"./dist/supabase-bridge/index.js"}},"gitHead":"fd408f61d9aad1c3ce63175b076fa2c9b62b4d4d","scripts":{"test":"vitest run","build":"tsc && tsc -p tsconfig.react.json","prepare":"npm run build","test:deno":"deno test tests/deno/","local:test-app":"npm run build && node tests/local-app/server.mjs"},"_npmUser":{"name":"lockymic-alfa","email":"michael@alfadocs.com"},"_npmVersion":"10.9.8","description":"Bridgeable Alfadocs auth core with infrastructure adapters","directories":{},"_nodeVersion":"22.22.3","dependencies":{"jose":"^6.2.2"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^25.0.1","react":"^19.2.0","dotenv":"^17.4.1","undici":"^8.0.2","vitest":"^3.2.4","deno-bin":"^2.2.7","react-dom":"^19.2.0","typescript":"^5.6.3","@types/react":"^19.2.0","@testing-library/react":"^16.1.0"},"peerDependencies":{"react":"^18.2.0 || ^19.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/auth_0.4.1_1780062854413_0.13697166738639943","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@alfadocs/auth","version":"0.5.0","keywords":["oauth2","alfadocs","auth","bridge","supabase"],"author":{"name":"AlfaDocs"},"license":"BUSL-1.1","_id":"@alfadocs/auth@0.5.0","maintainers":[{"name":"lockymic-alfa","email":"michael@alfadocs.com"}],"dist":{"shasum":"d4a6d551583bac666fa872167b19137dfd68145e","tarball":"https://registry.npmjs.org/@alfadocs/auth/-/auth-0.5.0.tgz","fileCount":35,"integrity":"sha512-QxfVZqbwyN/ki/nklmvD4k10i8cjqAVeA0zqaKV6wQXHuHWif6kuZAuc4e2e8yv+jhDZiQTtCVUleM0k0K8kqA==","signatures":[{"sig":"MEUCIBhijevXhTrSQc0WG8IOfNLRl5oMf7MlvQ7XuPmKGJq3AiEAy7z3FwR37WpXs9Vq+KtLPFmPGBXHGg+Gp91l6bk7q+c=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":99216},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","default":"./dist/react/index.js"},"./supabase-bridge":{"types":"./dist/supabase-bridge/index.d.ts","import":"./dist/supabase-bridge/index.js","default":"./dist/supabase-bridge/index.js"}},"gitHead":"7c62d0cb14ee3dc6f72154cef383c0305007bd4a","scripts":{"test":"vitest run","build":"tsc && tsc -p tsconfig.react.json","prepare":"npm run build","test:deno":"deno test tests/deno/","local:test-app":"npm run build && node tests/local-app/server.mjs"},"_npmUser":{"name":"lockymic-alfa","email":"michael@alfadocs.com"},"_npmVersion":"10.9.8","description":"Bridgeable Alfadocs auth core with infrastructure adapters","directories":{},"_nodeVersion":"22.22.3","dependencies":{"jose":"^6.2.2"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^25.0.1","react":"^19.2.0","dotenv":"^17.4.1","undici":"^8.0.2","vitest":"^3.2.4","deno-bin":"^2.2.7","react-dom":"^19.2.0","typescript":"^5.6.3","@types/react":"^19.2.0","@testing-library/react":"^16.1.0"},"peerDependencies":{"react":"^18.2.0 || ^19.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/auth_0.5.0_1780642343981_0.11977600714515657","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-05-29T13:48:43.421Z","modified":"2026-07-01T09:31:57.320Z","0.4.0":"2026-05-29T13:48:43.744Z","0.4.1":"2026-05-29T13:54:14.562Z","0.5.0":"2026-06-05T06:52:24.115Z"},"author":{"name":"AlfaDocs"},"license":"BUSL-1.1","keywords":["oauth2","alfadocs","auth","bridge","supabase"],"description":"Bridgeable Alfadocs auth core with infrastructure adapters","maintainers":[{"email":"enea@alfadocs.com","name":"enea-alfadocs"},{"email":"michael@alfadocs.com","name":"lockymic-alfa"},{"email":"boris@alfadocs.com","name":"strochkov-alfadocs"}],"readme":"# @alfadocs/auth\n\nBridgeable Alfadocs auth core with infrastructure adapters.\n\n## Architecture\n\n```mermaid\nflowchart TB\n  subgraph app[\"Host app (Edge / Node / local server)\"]\n    A[\"createAlfadocsAuth(config)\"]\n    A --> R[\"handleRequest / handlers\"]\n  end\n\n  subgraph core[\"@alfadocs/auth core\"]\n    R --> OAuth[\"OAuth: PKCE, authorize, token, /me\"]\n    R --> Cookies[\"Session + pre-auth cookies\"]\n    R --> S[\"storage: AuthStorage\"]\n  end\n\n  subgraph contract[\"Bridge contract\"]\n    I[\"AuthStorage = UserStore + SessionStore\"]\n  end\n\n  subgraph bridge[\"Example: Supabase bridge\"]\n    SB[\"createSupabaseStorage(...)\"]\n    SB --> REST[\"PostgREST /rest/v1/alfa_users, alfa_sessions\"]\n    SB -.->|implements| I\n  end\n\n  S --> SB\n\n  OAuth --> AD[\"Alfadocs: /oauth2/*, /api/v1/me\"]\n```\n\nThe **core** owns the OAuth flow and cookies. **`AuthStorage`** is the persistence seam; **`createSupabaseStorage`** is one implementation (PostgREST only: `fetch`, no Postgres driver).\n\n**Login flow:** browser → `handleLogin` (redirect) → Alfadocs → `handleCallback` (code exchange + profile) → storage upsert user + session → `Set-Cookie` → later `handleSession` uses cookie → `getSession` / `getUser`.\n\n## Install\n\n```bash\nnpm install @alfadocs/auth\n```\n\n## Supabase tables (one-time)\n\nTables **`alfa_users`** and **`alfa_sessions`** are **multi-tenant**: every row includes **`app_id`**. Primary keys are **`(app_id, id)`** and **`(app_id, cookie_value)`**. The bridge sets `app_id` from **`appId`** or, if omitted, from **`oauthClientId`** (Alfadocs OAuth **`client_id`**) on every PostgREST request.\n\n**Breaking:** if you previously used the old **`alfadocs_auth_ensure_schema`** RPC, new migrations **`DROP FUNCTION IF EXISTS`** it. If you had tables **without** `app_id`, drop `alfa_sessions` then `alfa_users` before applying.\n\n**Apply DDL:** use [`supabase/migrations/`](supabase/migrations/) — run **`supabase db push`** with the [Supabase CLI](https://supabase.com/docs/guides/cli), or paste the latest migration SQL into the [Supabase SQL editor](https://supabase.com/dashboard). That creates **`alfa_*`** in `public` and **`NOTIFY pgrst, 'reload schema'`** so PostgREST reloads.\n\n**Shared auth project (e.g. one central Supabase used by several deployed apps):** one Supabase project can back many apps. Use **`AUTH_SUPABASE_URL`** and **`AUTH_SUPABASE_KEY`**. For the `app_id` row scope, pass **`appId`** (e.g. **`AUTH_APP_ID`**) and/or **`oauthClientId`** (your Alfadocs OAuth **`client_id`**). If **`appId`** is omitted, **`oauthClientId`** is used — fine when one OAuth client maps to one tenant. Prefer an explicit **`appId`** when several apps share the same `client_id` or you want a non-public identifier. **`createSupabaseStorage({ supabaseUrl, serviceRoleKey, appId?, oauthClientId? })`**. Treat the service role as a root secret.\n\n**RLS:** the shipped migration enables **row level security** on **`alfa_*`** with **no policies** for normal roles, so **`anon` / `authenticated`** PostgREST traffic cannot read or write those rows (hardening if a key is misused). The **service role** used by this bridge **bypasses RLS** in Supabase, so your server-side `fetch` calls keep working. Add policies only if you intentionally expose these tables to user JWTs.\n\n## App Supabase vs auth Supabase (don’t mix keys)\n\nMany apps already have a **Supabase project** for product data (often with the **`anon`** key in the browser and **`SUPABASE_URL`** in env). **`createSupabaseStorage`** is separate: it talks to the project where **`alfa_users`** / **`alfa_sessions`** live.\n\n| | **Your app’s Supabase** (typical) | **Auth storage Supabase** (`createSupabaseStorage`) |\n|--|--|--|\n| **Purpose** | Your tables, RLS, maybe Supabase Auth for end users | Only Alfadocs session + user rows for this library |\n| **URL env** | Often `SUPABASE_URL`, `VITE_SUPABASE_URL`, etc. | Use **`AUTH_SUPABASE_URL`** (or pass that string as `supabaseUrl`) |\n| **Key** | Often **`anon`** in clients; server may use **service role** for admin jobs | Must be **service role** JWT for the **same project as `AUTH_SUPABASE_URL`** — set as **`AUTH_SUPABASE_KEY`** |\n| **Safe in browser?** | `anon` only | **Never** — service role bypasses RLS |\n\n**Common mistake:** pasting the app project’s **anon** key or **wrong project’s** service role into `createSupabaseStorage` → 401/404 on `alfa_*`, data missing, or writes to the wrong database. The URL and service role **must both** come from the project that actually has **`alfa_users`** / **`alfa_sessions`** (or from your dedicated central auth project).\n\nUsing **one** Supabase project for both app data and Alfadocs auth is fine **on purpose** — still use **`AUTH_SUPABASE_URL`** / **`AUTH_SUPABASE_KEY`** in code for the bridge so env names stay unambiguous.\n\n## Usage\n\n```ts\nimport { createAlfadocsAuth } from \"@alfadocs/auth\";\nimport { createSupabaseStorage } from \"@alfadocs/auth/supabase-bridge\";\n\nconst auth = createAlfadocsAuth({\n  clientId: \"...\",\n  clientSecret: \"...\",\n  redirectUri: \"...\",\n  appOrigin: \"https://myapp.example\",\n  storage: createSupabaseStorage({\n    // Must be the project where alfa_users / alfa_sessions exist (see table above).\n    supabaseUrl: process.env.AUTH_SUPABASE_URL!,\n    serviceRoleKey: process.env.AUTH_SUPABASE_KEY!,\n    oauthClientId: process.env.ALFADOCS_CLIENT_ID!,\n    // Optional override: appId: process.env.AUTH_APP_ID,\n  }),\n});\n```\n\n`auth.handleRequest(req)` routes:\n- `OPTIONS <any-path>` -> CORS preflight\n- `GET /login` -> start OAuth login\n- `GET /callback` -> callback exchange + user/session persistence\n- `GET /session` -> session check\n- `POST /logout` -> logout (origin-checked, cookie clear + session invalidation)\n\n## React binding (`@alfadocs/auth/react`)\n\nThe core above is **server-side** — it holds the client secret, runs the PKCE code-exchange, and sets an httpOnly session cookie. None of that can run in the browser. Mount `auth.handleRequest` behind a route (an Edge/serverless function or any HTTP handler) at, say, `/api/auth/*`, then drive it from React with the binding:\n\n```tsx\nimport { AlfadocsAuthProvider, useAlfadocsAuth } from \"@alfadocs/auth/react\";\n\n// Wrap your app once. `basePath` is where the BFF handler is mounted.\n<AlfadocsAuthProvider basePath=\"/api/auth\">\n  <App />\n</AlfadocsAuthProvider>;\n\n// Anywhere inside:\nfunction Account() {\n  const { status, user, connect, signOut } = useAlfadocsAuth();\n  if (status === \"loading\") return null;\n  if (status !== \"authenticated\") return <button onClick={connect}>Connect with AlfaDocs</button>;\n  return <button onClick={signOut}>Sign out {String(user?.email ?? \"\")}</button>;\n}\n```\n\n`useAlfadocsAuth()` returns `{ status, user, error, isAuthenticated, connect, signOut, refresh }`:\n- `connect()` — full-page navigation to `{basePath}/login` (the OAuth redirect can't be an XHR).\n- `signOut()` — `POST {basePath}/logout`, then re-reads the session.\n- `refresh()` — re-fetches `{basePath}/session`.\n\nAll requests are sent with `credentials: \"include\"` so the httpOnly session cookie is included (works same-origin, and cross-origin when the BFF enables credentialed CORS). The binding has **no** dependency on the server core and never sees the client secret — it only talks to the four endpoints above. `react` is an optional peer dependency: consumers using only the server core don't need it installed.\n\n## Host integration checklist (Supabase Edge + BFF)\n\nEnd-to-end notes for running **`createAlfadocsAuth`** on **Supabase Edge Functions** behind a SPA that talks to **cookie-authenticated BFFs**. Adapt names (function slug, env vars, flags) to your stack.\n\n### 1. Two Supabase surfaces (keep keys straight)\n\n| Role | Typical env on the Edge runtime | Used for |\n|------|----------------------------------|----------|\n| **App project** | `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` | Your product schema, optional legacy Supabase Auth, any tables you touch from **`resolveProfile`** |\n| **Auth storage project** | `AUTH_SUPABASE_URL`, `AUTH_SUPABASE_KEY` (service role) | Only `alfa_users` / `alfa_sessions` via `createSupabaseStorage` |\n\nThe **Alfadocs auth Edge function** often needs **both**: the bridge uses **`AUTH_*`**, while **`resolveProfile`** (below) commonly uses the Supabase client against the **app** project to persist tenant/user rows after `/me`.\n\nApply the multi-tenant **`alfa_*`** DDL to whichever project **`AUTH_SUPABASE_URL`** points at (see [Supabase tables](#supabase-tables-one-time) above).\n\n### 2. Alfadocs auth Edge function (Deno)\n\n- **Imports:** Deno can load this package from ESM in the function bundle, e.g.  \n  `https://esm.sh/gh/alfadocs/auth@main/src/index.ts` and  \n  `.../src/supabase-bridge/index.ts` (**pin a tag or commit** for production).\n- **`redirectUri`:** the function’s public callback URL, e.g.  \n  `{SUPABASE_URL}/functions/v1/<your-auth-function>/callback`  \n  Register that exact URL in the AlfaDocs OAuth client.\n- **`verify_jwt`:** set **`false`** for this function in `supabase/config.toml` if the gateway would otherwise require a Supabase JWT before the Alfadocs cookie exists.\n\n**Path prefix:** `handleRequest` expects paths like `/login`, `/callback`. Many hosts prefix the URL (e.g. `/functions/v1/<name>/...`). Strip the prefix and build a **`new Request(innerPath + search, …)`** before `auth.handleRequest`, or routes will not match.\n\n### 3. `appOrigin` for top-level navigations\n\nThe library scopes CORS, cookies, and redirects to **`appOrigin`**. That breaks down when:\n\n- **`GET /login`** is a **top-level navigation** (often no `Origin` header).\n- **`/callback`** is hit by the IdP redirect without your SPA’s origin.\n\nA robust pattern:\n\n1. Derive a candidate origin from a **trusted** `app_origin` query param (set by your SPA), then **`Origin`**, then **`Referer`** — each checked against an **explicit allowlist** (fixed production/staging URLs, preview hosts you control, localhost, etc.).\n2. On **`GET /login`**, set a short-lived **`Set-Cookie: alfa_app_origin=…`** (Secure, `SameSite=None`) so **`/callback`** can read back the same origin when headers are absent.\n\nExample login URL from the client:\n\n`{PUBLIC_SUPABASE_URL}/functions/v1/<your-auth-function>/login?app_origin={encodeURIComponent(window.location.origin)}`\n\n### 4. `resolveProfile`: `/me` → your domain model\n\nWhatever **`resolveProfile`** returns is stored in **`alfa_users.auth_data`** and surfaced on **`GET /session`**. Typical uses:\n\n- Use **`accessToken`**, **`meUrl`**, and **`fetchImpl`** to read AlfaDocs **`/me`** (and any follow-up API calls you need).\n- Upsert rows in **your app database** (tenants, users, token vault tables, etc.).\n- Return the fields your BFF and SPA need (stable tenant id, display name, internal foreign keys) so one session read avoids extra lookups.\n\nYou can keep **`auth_data`** minimal and resolve more server-side if you prefer.\n\n### 5. BFFs and the session cookie\n\nIf the browser does **not** hold a Supabase user JWT (cookie-only Alfadocs session), client-side PostgREST with RLS is usually not the primary path. Instead:\n\n- Expose a **BFF** Edge function (POST/GET as you design) that calls a small **`resolveSession(req)`** helper: parse **`alfadocs_session`**, use **`createSupabaseStorage`** with **`AUTH_*`** and **`oauthClientId`** to **`getSession` / `getUser`**, then read your claims from **`user.authData`**.\n- Call those endpoints with **`fetch(..., { credentials: \"include\" })`** so the HttpOnly session cookie is sent to the **same Supabase project origin** that issued it (`/functions/v1/...`).\n\nSet **`verify_jwt: false`** on BFF functions that rely on the Alfadocs cookie, and perform auth inside the handler — otherwise the gateway or CORS behavior can block credentialed calls.\n\n**CORS:** with credentials, echo **`Access-Control-Allow-Origin: <request Origin>`** for allowed origins only — never **`*`**. Reuse the same allowlist you use for **`appOrigin`**.\n\nAny Edge function that previously authenticated with **`Authorization: Bearer <supabase_jwt>`** needs a parallel path: same cookie session resolution, or an internal call to your BFF.\n\n### 6. Sliding session (optional)\n\nYou can re-issue the **`alfadocs_session`** cookie on successful **`GET /session`** with a renewed **`Max-Age`** so active users stay signed in without forking the library.\n\n### 7. Client routing and claims\n\n- Use a **feature flag** or build-time env to choose “legacy Supabase Auth login URL” vs “Alfadocs auth function login URL”.\n- UI and data hooks that today read **JWT `app_metadata` / claims** should branch: in cookie mode, load identity and tenant scope from your BFF (backed by **`auth_data`**).\n\n### 8. Edge secrets checklist\n\nOn the **app** project (or wherever the Alfadocs auth + BFF functions run), set at least:\n\n- **`ALFADOCS_CLIENT_ID`**, **`ALFADOCS_CLIENT_SECRET`**\n- **`AUTH_SUPABASE_URL`**, **`AUTH_SUPABASE_KEY`**\n- **`SUPABASE_URL`**, **`SUPABASE_SERVICE_ROLE_KEY`**\n\nAdd **`scopes`** on **`createAlfadocsAuth`** only if your product calls AlfaDocs APIs that require them.\n\n---\n\n**TL;DR:** Create **`alfa_*`** on the auth Supabase project → deploy an Edge wrapper with **path rewrite** and **`verify_jwt: false`** → **allowlist** `appOrigin` + optional **`alfa_app_origin`** cookie → implement **`resolveProfile`** for your schema → add **cookie-aware BFFs** that share one session resolver → point the SPA login at **`/functions/v1/<your-auth-function>/login`** and use **`credentials: \"include\"`** for API calls.\n\n## Storage interfaces\n\nThe core is now decoupled from infrastructure via split interfaces:\n- `UserStore` (`getUser`, `getUserByExternalId`, `createUser(userId, username, authData)`, `updateUser`)\n- `SessionStore` (`createSession`, `getSession`, `deleteSession`)\n- `AuthStorage` (`UserStore & SessionStore`)\n\n**The two lookup paths** (split in 0.5.0): the OAuth **callback** resolves users\nby the **external** (AlfaDocs) id via `getUserByExternalId`; the **session** path\nresolves them by the **storage** id (`StoredUser.id`, the value your\n`createUser`/`getSession` round-trip) via `getUser`. If your bridge uses the\nexternal id as its storage id (like `createSupabaseStorage`), implement both\nidentically; if it keeps separate columns, query the matching column in each.\nBefore 0.5.0 a single `getUser` served both paths, which silently broke bridges\nwith separate id spaces.\n\nThe bridge only speaks **PostgREST** (`fetch`), so it runs on **Supabase Edge**, Deno Deploy, Bun, Node, and Cloudflare Workers without a `postgres` driver.\n\n## Testing\n\n**Unit tests** (Vitest), from the repo root:\n\n```bash\nnpm test\n```\n\n**Deno smoke test** (loads the Supabase bridge under Deno with stubbed `fetch`):\n\n```bash\nnpm run test:deno\n```\n\nThis runs `deno test tests/deno/` with repo [`deno.json`](deno.json) enabling sloppy imports so Node-style `.js` specifiers in `src/` resolve to `.ts` sources under Deno.\n\nTests live under `tests/core/`, `tests/supabase-bridge/`, and `tests/deno/`.\n\n**Local end-to-end smoke test** against a real Alfadocs client and Supabase project (no Edge Function required): build, configure env, run the sample server.\n\n```bash\nnpm run local:test-app\n```\n\nCreate `tests/local-app/.env` with the variables listed there (or export them in your shell). Full steps and troubleshooting: [tests/local-app/README.md](tests/local-app/README.md).\n","readmeFilename":"README.md"}