{"_id":"@buddy-works/identity","_rev":"4-93eff20cc2443099adc848edbd280cfd","name":"@buddy-works/identity","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.3":{"name":"@buddy-works/identity","version":"0.1.3","keywords":["buddy","buddy.works","auth","authentication","tunnel","sandbox","identity"],"author":{"url":"https://buddy.works","name":"Buddy"},"license":"MIT","_id":"@buddy-works/identity@0.1.3","maintainers":[{"name":"mical","email":"michal83h@gmail.com"},{"name":"bylek","email":"bylek77@gmail.com"},{"name":"bartoszwrobel591","email":"bartoszwrobel591@gmail.com"},{"name":"sztwiorok","email":"raphael@buddy.works"}],"homepage":"https://buddy.works","dist":{"shasum":"c5a1152763cdc28b4131a5145f9282699c61edef","tarball":"https://registry.npmjs.org/@buddy-works/identity/-/identity-0.1.3.tgz","fileCount":18,"integrity":"sha512-zhEfcaeA5DO5m50otBf5RVs4jGGb9iYCXhqG2MkZC6xAn63vhUd0779z/oQSIgXJkcLjZIXq1oZrHp9+mFT3ug==","signatures":[{"sig":"MEYCIQDnwcyM9rCMylPSpQ/CB6TZXDfqHktlUwdv/YYNUwLxZgIhAJUEc77no9pGadh/ogjfwJ/nrfEYWj2Dzl9p5f4bbgXL","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":89068},"main":"./dist/index.cjs","type":"module","_from":"file:buddy-works-identity-0.1.3.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./dev":{"types":"./dist/dev.d.ts","import":"./dist/dev.js","require":"./dist/dev.cjs"}},"scripts":{"dev":"tsup --watch","build":"tsup","example":"pnpm build && pnpm --filter buddy-identity-example-express start","typecheck":"tsc --noEmit","example:next":"pnpm build && pnpm --filter buddy-identity-example-nextjs dev"},"_npmUser":{"name":"bylek","email":"bylek77@gmail.com"},"_resolved":"/private/var/folders/4j/hgdkvvsx3qj_x44py_vd4d5r0000gn/T/e39b7cd31d56b206a4e0fba3ae6f73cb/buddy-works-identity-0.1.3.tgz","_integrity":"sha512-zhEfcaeA5DO5m50otBf5RVs4jGGb9iYCXhqG2MkZC6xAn63vhUd0779z/oQSIgXJkcLjZIXq1oZrHp9+mFT3ug==","_npmVersion":"11.9.0","description":"Zero-config authentication for apps running behind Buddy tunnels. One call to know who the current user is — no logins, sessions or user databases.","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"8.5.1","typescript":"5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/identity_0.1.3_1784707011096_0.666248236613679","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@buddy-works/identity","version":"0.1.4","keywords":["buddy","buddy.works","auth","authentication","tunnel","sandbox","identity"],"author":{"url":"https://buddy.works","name":"Buddy"},"license":"MIT","_id":"@buddy-works/identity@0.1.4","maintainers":[{"name":"mical","email":"michal83h@gmail.com"},{"name":"bylek","email":"bylek77@gmail.com"},{"name":"bartoszwrobel591","email":"bartoszwrobel591@gmail.com"},{"name":"sztwiorok","email":"raphael@buddy.works"}],"homepage":"https://buddy.works","dist":{"shasum":"5e94a5fbde1ea5f691d6db76a5d017537f6523d4","tarball":"https://registry.npmjs.org/@buddy-works/identity/-/identity-0.1.4.tgz","fileCount":18,"integrity":"sha512-IGrrsNngwGR0Tnr3JbUqR33kCNh82KkQp8Y5TYQBSbLy0vxkIIKtj0BSvBknF7AuzHoyneh1fle+s3no34rGIg==","signatures":[{"sig":"MEQCIGa8uoxxjFTzutB94PSEJd9v7zeVR+KLanbZV8r0J8MzAiBOwLGD72S5xFrf4T633rI1NYlvn/8pE6sDoGMaU8aZPA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":89105},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./dev":{"types":"./dist/dev.d.ts","import":"./dist/dev.js","require":"./dist/dev.cjs"}},"gitHead":"aeaee3a1da50eec253991a76fbc39c00da0724a8","scripts":{"dev":"tsup --watch","build":"tsup","example":"pnpm build && pnpm --filter buddy-identity-example-express start","typecheck":"tsc --noEmit","example:next":"pnpm build && pnpm --filter buddy-identity-example-nextjs dev"},"_npmUser":{"name":"bylek","email":"bylek77@gmail.com","approver":{"name":"bylek","email":"bylek77@gmail.com"}},"_npmVersion":"11.17.0","description":"Zero-config authentication for apps running behind Buddy tunnels. One call to know who the current user is — no logins, sessions or user databases.","directories":{},"sideEffects":false,"_nodeVersion":"26.5.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"tsup":"8.5.1","typescript":"5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/identity_0.1.4_1784707087553_0.13661313278546272","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@buddy-works/identity","version":"0.2.0","keywords":["buddy","buddy.works","auth","authentication","tunnel","sandbox","identity"],"author":{"url":"https://buddy.works","name":"Buddy"},"license":"MIT","_id":"@buddy-works/identity@0.2.0","maintainers":[{"name":"mical","email":"michal83h@gmail.com"},{"name":"bylek","email":"bylek77@gmail.com"},{"name":"bartoszwrobel591","email":"bartoszwrobel591@gmail.com"},{"name":"sztwiorok","email":"raphael@buddy.works"}],"homepage":"https://buddy.works","dist":{"shasum":"cd1bf6a287f13834750ee632518c68f10615d551","tarball":"https://registry.npmjs.org/@buddy-works/identity/-/identity-0.2.0.tgz","fileCount":18,"integrity":"sha512-hMYn9OkI3mZ0/h2HFgmd0nPZuTcL7cEUDRtZGbblbJnxwqXfohmOlErHjh2fQecmFhp8FfPZKNNLrW+VMhTx2Q==","signatures":[{"sig":"MEQCIBWBCmBPFWNqsOsw+9etFakOeWNVsLyuMT01JXXqUNHFAiBNXP2AxMCLjnWJ4TJwv5p9ehy7PAXvfp+d2ky6Su3Cjw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":89105},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./dev":{"types":"./dist/dev.d.ts","import":"./dist/dev.js","require":"./dist/dev.cjs"}},"gitHead":"ad8591fcb4718c38964bf32e4531e2788aa587ba","scripts":{"dev":"tsup --watch","build":"tsup","example":"pnpm build && pnpm --filter buddy-identity-example-express start","typecheck":"tsc --noEmit","example:next":"pnpm build && pnpm --filter buddy-identity-example-nextjs dev"},"_npmUser":{"name":"bylek","email":"bylek77@gmail.com","approver":{"name":"bylek","email":"bylek77@gmail.com"}},"_npmVersion":"11.17.0","description":"Zero-config authentication for apps running behind Buddy tunnels. One call to know who the current user is — no logins, sessions or user databases.","directories":{},"sideEffects":false,"_nodeVersion":"26.5.0","_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"tsup":"8.5.1","typescript":"5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/identity_0.2.0_1784707234676_0.19234552487895185","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-07-22T07:56:50.871Z","modified":"2026-09-02T06:47:18.459Z","0.1.3":"2026-07-22T07:56:51.226Z","0.1.4":"2026-07-22T07:58:07.674Z","0.2.0":"2026-07-22T08:00:34.764Z"},"author":{"url":"https://buddy.works","name":"Buddy"},"license":"MIT","homepage":"https://buddy.works","keywords":["buddy","buddy.works","auth","authentication","tunnel","sandbox","identity"],"description":"Zero-config authentication for apps running behind Buddy tunnels. One call to know who the current user is — no logins, sessions or user databases.","maintainers":[{"email":"michal83h@gmail.com","name":"mical"},{"email":"bylek77@gmail.com","name":"bylek"},{"email":"bartoszwrobel591@gmail.com","name":"bartoszwrobel591"},{"email":"mganczarczyk@proton.me","name":"mganczarczyk"},{"email":"raphael@buddy.works","name":"sztwiorok"}],"readme":"# @buddy-works/identity\n\nZero-config authentication for apps running behind [Buddy](https://buddy.works) tunnels.\n\nWhen a tunnel has Buddy authentication enabled, Buddy handles login and access control\nand issues a session cookie. This library gives your application the identity of the\ncurrent user in a single call — **no logins, no OAuth, no sessions, no user database**:\n\n```ts\nimport { getCurrentUser } from \"@buddy-works/identity\";\n\nconst user = await getCurrentUser(req);\n// => { name, email, owner, admin, avatar, sso_id } — or null when not logged in\n```\n\nPerfect for internal tools, dashboards and AI-built apps running in Buddy sandboxes:\naccess is managed centrally in the tunnel settings, and your code just receives\ntrusted user information.\n\n## Installation\n\n```sh\nnpm install @buddy-works/identity\n```\n\nRequires Node.js 18+ (built-in `fetch`). Works in every modern browser.\n\n## How it works\n\nBuddy tunnels expose a user-info endpoint on your app's domain:\n\n```\nGET /.buddy/auth/me\n```\n\nThe tunnel intercepts this path before the request reaches your app, verifies the\nBuddy session (a signed JWT stored in an `HttpOnly` cookie) and responds with:\n\n```json\n{\n  \"id\": 42,\n  \"name\": \"John Doe\",\n  \"email\": \"john@example.com\",\n  \"owner\": false,\n  \"admin\": true,\n  \"avatar\": \"https://…\",\n  \"sso_id\": \"john.doe@idp.example.com\",\n  \"groups\": [\n    { \"id\": 1, \"name\": \"everyone\" },\n    { \"id\": 2, \"name\": \"finance\" }\n  ]\n}\n```\n\nThis library is a thin, typed client for that endpoint. All security logic — JWT\nsignature verification, JWKS key management, claim validation — stays on Buddy's\nside, so your app never touches a token.\n\n## Usage\n\n### Client-side (browser)\n\nCall it with no arguments. The request goes to the current origin and the browser\nattaches the session cookie automatically:\n\n```ts\nimport { getCurrentUser } from \"@buddy-works/identity\";\n\nconst user = await getCurrentUser();\n\nif (user) {\n  document.querySelector(\"#hello\").textContent = `Welcome ${user.name}`;\n}\n```\n\n### Server-side (Express / Connect / plain Node)\n\nPass the incoming request so its cookie can be forwarded:\n\n```ts\nimport express from \"express\";\nimport { getCurrentUser } from \"@buddy-works/identity\";\n\nconst app = express();\n\napp.get(\"/api/whoami\", async (req, res) => {\n  const user = await getCurrentUser(req);\n  if (!user) return res.status(401).json({ error: \"Not logged in\" });\n  res.json({ message: `Welcome ${user.name}` });\n});\n```\n\nOr use the middleware to get `req.buddyUser` everywhere:\n\n```ts\nimport { buddyIdentity } from \"@buddy-works/identity\";\n\napp.use(buddyIdentity());\n// app.use(buddyIdentity({ required: true })); // reject anonymous requests with 401\n\napp.get(\"/api/admin\", (req, res) => {\n  if (!req.buddyUser?.admin) return res.status(403).json({ error: \"Admins only\" });\n  res.json({ secret: \"…\" });\n});\n```\n\nGroups managed centrally in Buddy make simple role systems free:\n\n```ts\napp.get(\"/api/reports\", (req, res) => {\n  const inFinance = req.buddyUser?.groups.some((g) => g.name === \"finance\");\n  if (!inFinance) return res.status(403).json({ error: \"Finance only\" });\n  res.json(reports);\n});\n```\n\n### Next.js / Remix / anything with Fetch API requests\n\n`getCurrentUser` also accepts a Fetch API `Request` or a `Headers` instance:\n\n```ts\n// Next.js route handler\nexport async function GET(request: Request) {\n  const user = await getCurrentUser(request);\n  return Response.json({ user });\n}\n\n// Next.js server component\nimport { headers } from \"next/headers\";\nconst user = await getCurrentUser(await headers());\n```\n\n### Explicit options\n\nWhen no request object is available, pass the pieces yourself:\n\n```ts\nconst user = await getCurrentUser({\n  baseUrl: \"https://myapp.buddytunnels.site\",\n  cookie: rawCookieHeader,\n});\n```\n\n## Local development (no tunnel)\n\nLocally there is no tunnel to answer `/.buddy/auth/me`. The `/dev` entry point\nships everything needed to develop against realistic user states anyway.\n\nEvery mocking API accepts the same input: a **preset name** (`\"owner\"`,\n`\"admin\"`, `\"member\"`), a **partial user** (merged onto the `admin` preset), or\n**`null`** for the logged-out state.\n\n**Server / dev-server** — mount the fake endpoint as middleware. Anything that\ntalks to `/.buddy/auth/me` over HTTP (your browser code, `getCurrentUser(req)`\non the server) now works locally:\n\n```ts\nimport { mockAuthEndpoint } from \"@buddy-works/identity/dev\";\n\nif (process.env.NODE_ENV !== \"production\") {\n  app.use(mockAuthEndpoint(process.env.MOCK_USER ?? \"admin\"));\n  // mockAuthEndpoint(\"member\")                      — preset\n  // mockAuthEndpoint({ name: \"Jane\", owner: true }) — custom user\n  // mockAuthEndpoint(null)                          — logged-out\n}\n```\n\n**SPA / tests / no middleware** — install an in-process mock; every\n`getCurrentUser()` call in this runtime returns it without any HTTP request:\n\n```ts\nif (import.meta.env.DEV) {\n  const { mockUser } = await import(\"@buddy-works/identity/dev\");\n  mockUser(\"member\");\n}\n```\n\n`clearMockUser()` removes it again (handy in test teardown), and the raw presets\nare exported as `MOCK_USERS` if you want to build your own states from them.\n\nIn Next.js, mount the endpoint with a dev-only rewrite instead of middleware —\nsee [`examples/nextjs`](./examples/nextjs). Behind a real tunnel the endpoint is\nintercepted before requests reach your app, so a leftover mock endpoint is never\nhit in production — but only enable mocks in development anyway.\n\n## Examples\n\nTwo runnable examples live in [`examples/`](./examples):\n\n- [`examples/express`](./examples/express) — Express + a vanilla-JS page: client-side,\n  server-side and admin-gating in one screen.\n- [`examples/nextjs`](./examples/nextjs) — Next.js App Router: Server Component\n  (`getCurrentUser(await headers())`), Client Component, Route Handler and an\n  admin-gated page. The local mock is wired up via a rewrite in `next.config.mjs`.\n\n```sh\npnpm install\npnpm example          # Express example on http://localhost:3000\npnpm example:next     # Next.js example on http://localhost:3000\n```\n\nBoth understand `MOCK=logged-out` (simulate a logged-out user) and `MOCK=0`\n(disable the mock — use when running behind a real Buddy tunnel).\n\n## API\n\n### `getCurrentUser(input?)`\n\nReturns `Promise<BuddyUser | null>`.\n\n- `input` — optional. One of:\n  - *(nothing)* — browser only; requests `/.buddy/auth/me` on the current origin.\n  - Node `IncomingMessage` / Express request — cookie and host are read from it.\n  - Fetch API `Request` or `Headers` — same, for edge/serverless frameworks.\n  - `{ baseUrl?, cookie?, fetch? }` — explicit options.\n- Returns `null` when the user is not logged in (the endpoint responds with a\n  non-2xx status or a login redirect).\n- Throws when the endpoint cannot be reached at all (e.g. running locally\n  without the [dev mock](#local-development-no-tunnel)).\n\n### `buddyIdentity(options?)`\n\nConnect/Express-style middleware. Resolves the user once per request and sets\n`req.buddyUser: BuddyUser | null`. With `{ required: true }` it responds `401`\nto unauthenticated requests.\n\nIn TypeScript, teach Express about the new property once, e.g. in a `.d.ts` file:\n\n```ts\nimport type { BuddyUser } from \"@buddy-works/identity\";\n\ndeclare global {\n  namespace Express {\n    interface Request {\n      buddyUser?: BuddyUser | null;\n    }\n  }\n}\n```\n\n### `@buddy-works/identity/dev`\n\nDevelopment helpers — see [Local development](#local-development-no-tunnel):\n\n- `mockAuthEndpoint(user?)` — middleware serving a fake `/.buddy/auth/me`.\n- `mockUser(user?)` / `clearMockUser()` — in-process mock for SPAs and tests.\n- `MOCK_USERS` — the `owner` / `admin` / `member` presets.\n- `resolveMockUser(input)` — resolves any mock input to a full `BuddyUser | null`.\n\n### Types\n\n```ts\ninterface BuddyUser {\n  id: number;            // Buddy user id\n  name: string;\n  email: string;\n  owner: boolean;        // workspace owner\n  admin: boolean;        // workspace administrator\n  avatar: string | null;\n  sso_id: string | null; // SSO identifier of the user in this workspace\n  groups: BuddyGroup[];  // workspace groups the user belongs to\n}\n\ninterface BuddyGroup {\n  id: number;\n  name: string;\n}\n```\n\n`sso_id` is the user's identifier in the workspace's SSO provider — it is a\nstring only when the workspace has SSO enabled **and** the user is bound to an\nSSO identity; otherwise it is `null`.\n\n`groups` is defensively normalized to `[]` (and `sso_id` to `null`) when\nmissing from the response, so you can always rely on both fields being present.\n\n## Client-side vs server-side\n\nThe package is **isomorphic** — one import works in both environments, because in\nboth cases it is just an HTTP call to the same endpoint with the same cookie:\n\n| | Client-side | Server-side |\n|---|---|---|\n| How the cookie travels | attached automatically by the browser (same-origin, `HttpOnly`) | forwarded from the incoming request's `Cookie` header |\n| Where the call goes | relative `/.buddy/auth/me`, intercepted by the tunnel | app's public origin (derived from `X-Forwarded-Host`/`Host`), intercepted by the tunnel |\n| Best for | showing who's logged in, personalizing UI | authorization decisions, SSR, API routes |\n\nThings to keep in mind:\n\n- **Authorization must happen server-side.** Client-side checks like\n  `user.admin && renderAdminPanel()` are UI conveniences — anyone can flip them in\n  DevTools. The tunnel already guarantees that *only allowed users reach the app at\n  all*; for anything finer-grained (admin-only endpoints, per-user data), check\n  `owner`/`admin` on the server.\n- **Server-side calls need outbound network access** to the app's own public\n  hostname, since the endpoint lives on the tunnel edge, not inside your app. This\n  costs one extra HTTP round-trip per call — cache the result per request (the\n  `buddyIdentity()` middleware does this for you).\n- **Client-side calls must be same-origin.** The session cookie belongs to the\n  tunnel domain and is `HttpOnly`, so a different origin can neither read nor send it.\n\nAn alternative design would verify the JWT locally in the SDK (reading the cookie,\nfetching Buddy's JWKS, checking signature and claims). It would save the extra HTTP\ncall on the server, but it drags token internals, key caching/rotation and claim\nvalidation into every app — and it wouldn't work in the browser at all, since the\ncookie is `HttpOnly`. Delegating verification to the tunnel keeps the SDK tiny and\nlets the endpoint evolve (e.g. richer user data) without shipping new SDK versions.\n\n## Development\n\nThis repo is a [pnpm](https://pnpm.io) workspace (the library at the root, examples\nin `examples/*`) with pinned dependencies:\n\n```sh\npnpm install               # install everything\npnpm build                 # build the library (tsup → ESM + CJS + d.ts)\npnpm typecheck             # typecheck the library\n```\n\nCI lives in [`.buddy/`](./.buddy) as Buddy pipelines, one per file:\n\n- [`tests.yml`](./.buddy/tests.yml) — build + typecheck on every push.\n- [`publish-npm.yml`](./.buddy/publish-npm.yml) — manual `pnpm publish` from `main`;\n  requires an encrypted `NPM_TOKEN` variable on the pipeline.\n\n## License\n\nMIT © [Buddy](https://buddy.works)\n","readmeFilename":"README.md"}