{"_id":"@aether-zone/daimon","name":"@aether-zone/daimon","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@aether-zone/daimon","version":"0.1.0","description":"The pistis OAuth client for aether-zone's Next.js apps: the authorization code flow, the session cookies it lands in, and the route handlers that drive it.","license":"MIT","type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./ui":{"types":"./dist/ui/index.d.ts","import":"./dist/ui/index.js"},"./config":{"types":"./dist/config-entry/index.d.ts","import":"./dist/config-entry/index.js"},"./upload":{"types":"./dist/upload.d.ts","import":"./dist/upload.js"},"./format":{"types":"./dist/format.d.ts","import":"./dist/format.js"},"./package.json":"./package.json"},"publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"repository":{"type":"git","url":"git+https://github.com/Aether-zone/daimon.git","directory":"libs/daimon"},"peerDependencies":{"next":"^16.0.0","oauth4webapi":"^3.8.7","react":"^19.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"devDependencies":{"@types/react":"^19.0.0","next":"^16.3.2","oauth4webapi":"^3.8.7","react":"^19.2.8","vitest":"^3.0.0"},"scripts":{"build":"tsup","typecheck":"tsc -p tsconfig.json --noEmit","lint":"eslint .","test":"vitest run","test:watch":"vitest"},"_id":"@aether-zone/daimon@0.1.0","bugs":{"url":"https://github.com/Aether-zone/daimon/issues"},"homepage":"https://github.com/Aether-zone/daimon#readme","_integrity":"sha512-WI+VAG4m17XxHIVeeh+cIahIIODeDpFKuoDcwgX7JbXoAxzH7rmWWnq00Yy85B+lZXwsGMDKKxf42tw+A3bkbQ==","_resolved":"/tmp/17cff26317ca06569a9df3a185ca51dc/aether-zone-daimon-0.1.0.tgz","_from":"file:aether-zone-daimon-0.1.0.tgz","_nodeVersion":"22.23.2","_npmVersion":"10.9.8","dist":{"integrity":"sha512-WI+VAG4m17XxHIVeeh+cIahIIODeDpFKuoDcwgX7JbXoAxzH7rmWWnq00Yy85B+lZXwsGMDKKxf42tw+A3bkbQ==","shasum":"c8be8b43bc0f000b013be9e17172c4ec4d17e808","tarball":"https://registry.npmjs.org/@aether-zone/daimon/-/daimon-0.1.0.tgz","fileCount":20,"unpackedSize":127040,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEmWblq/GO70dJtIKX6kFTje32PcWFqTM6YPXNayWOhuAiAVSsk4afmbG8eCz/uUrixJf56BiZzW1DwfzlePGeXXIQ=="}]},"_npmUser":{"name":"pascalwilbrink","email":"pascal.wilbrink@gmail.com"},"directories":{},"maintainers":[{"name":"pascalwilbrink","email":"pascal.wilbrink@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/daimon_0.1.0_1788983632793_0.01348255653690833"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-09T19:53:52.698Z","0.1.0":"2026-09-09T19:53:52.928Z","modified":"2026-09-09T19:53:53.083Z"},"maintainers":[{"name":"pascalwilbrink","email":"pascal.wilbrink@gmail.com"}],"description":"The pistis OAuth client for aether-zone's Next.js apps: the authorization code flow, the session cookies it lands in, and the route handlers that drive it.","homepage":"https://github.com/Aether-zone/daimon#readme","repository":{"type":"git","url":"git+https://github.com/Aether-zone/daimon.git","directory":"libs/daimon"},"bugs":{"url":"https://github.com/Aether-zone/daimon/issues"},"license":"MIT","readme":"# @aether-zone/daimon\n\nThe pistis OAuth client for aether-zone's Next.js apps: the authorization code\nflow, the session cookies it lands in, and the route handlers that drive it.\n\n```sh\npnpm add @aether-zone/daimon\n```\n\nPeers: `next` ≥ 16, `oauth4webapi` ≥ 3, and `react` ≥ 19 for the `/ui` entry\npoint (optional — the rest of the library does not need it).\n\n## Entry points\n\nThe split is load-bearing rather than tidy. The root entry reaches for\n`server-only` and `next/headers`, which throw the moment they are pulled into a\nclient or middleware bundle — and the proxy still needs to know what this app's\nsession cookie is called.\n\n| Import | What it is | Safe where |\n| --- | --- | --- |\n| `@aether-zone/daimon` | `createAuth`, the session, the route handlers, the api client, the failure responder | Server components, route handlers, server actions |\n| `@aether-zone/daimon/ui` | `AuthShell`, `createSignedOutPage` | Anywhere React renders |\n| `@aether-zone/daimon/config` | `cookieNames`, `resolveConfig`, `createProxy`, the claim types | Anywhere, including the proxy |\n| `@aether-zone/daimon/upload` | `putToSignedUrl`, `cancelledOr` | The browser |\n| `@aether-zone/daimon/format` | `formatBytes`, `initials` | Anywhere |\n\n## Wiring an app up\n\nOne file, and everything downstream takes the session from it rather than\nreading the environment again:\n\n```ts\n// web/lib/auth.ts\nimport 'server-only';\n\nimport { createAuth } from '@aether-zone/daimon';\n\nexport const auth = createAuth({\n    cookiePrefix: 'loculus',\n    clientId: 'loculus',\n    redirectUri: 'http://localhost:3112/api/auth/callback',\n    scopes: 'profile email organizations',\n});\n\nexport const getSession = auth.getSession;\n```\n\nNext.js will not accept a route from a library, so each app keeps four one-line\nfiles that re-export the handlers:\n\n```ts\n// web/app/api/auth/login/route.ts\nimport { auth } from '@/lib/auth';\n\nexport const GET = auth.routes.login;\n```\n\n`login`, `callback` and `logout` need nothing else. The session route is\ndeliberately *not* in `auth.routes`, because its body is the one part of this\nflow that is genuinely per-app:\n\n```ts\n// web/app/api/auth/session/route.ts\nimport { createSessionRoute } from '@aether-zone/daimon';\n\nimport { auth } from '@/lib/auth';\n\nexport const GET = createSessionRoute(auth.getSession, (session) => ({\n    user: session?.user ?? null,\n    clientId: session?.clientId ?? null,\n}));\n```\n\n### The proxy\n\n`createProxy` supplies the handler. The `config` beside it has to be written out\nin the app's own file — Next parses that object statically, so\n`export const config = somethingImported` is refused, and so is\n`matcher: [AN_IMPORTED_CONSTANT]`. This library therefore exports no matcher: it\ncould not be used in the one place it would be needed.\n\n```ts\n// web/proxy.ts\nimport { createProxy } from '@aether-zone/daimon/config';\n\nexport default createProxy({ cookiePrefix: 'loculus' });\n\nexport const config = {\n    matcher: [\n        '/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',\n    ],\n};\n```\n\nWithout a matcher the proxy runs on every request, including `_next/static` and\nimages, and the sign-in redirect blocks CSS and JS from loading.\n\n## Configuration\n\n`createAuth` takes the app's defaults; the environment overrides each one.\n\n| Default | Environment | Notes |\n| --- | --- | --- |\n| `cookiePrefix` | — | Not overridable: changing it signs everyone out. |\n| `clientId` | `OAUTH_CLIENT_ID` | |\n| `redirectUri` | `OAUTH_REDIRECT_URI` | Must be registered with pistis. |\n| `scopes` | `OAUTH_SCOPES` | Space-separated. `organizations` is what puts the `orgs` claim on the token. |\n| `issuer` | `OAUTH_ISSUER` | Must equal the `iss` claim exactly. |\n| `consentUrl` | `OAUTH_CONSENT_URL` | The pistis **web app**, not the discovered `authorization_endpoint`. |\n| `signedOutPath` | — | Where signing out lands, and where a failed authorization comes back to. |\n\n`OAUTH_CLIENT_SECRET` has no default and no app-level fallback. A missing one\nfails at startup rather than at the token exchange, where pistis answers\n`invalid_client` — which reads as \"this client is not registered\" rather than\n\"this app was started without its secret\".\n\n### The one place pistis departs from discovery\n\n`consentUrl` is the pistis web app. `GET /api/oauth/authorize` answers with JSON\ndescribing the pending request rather than redirecting to a login page, so\npointing a browser at the discovered `authorization_endpoint` renders raw JSON.\nThe consent screen is a separate application driving that endpoint, and it is\nwhere a person is actually sent.\n\n## Why the session is memoized\n\n`getSession` is wrapped in React's `cache`. That is not an optimization: a\nlayout and the page inside it render concurrently and both want the session.\nWithout it, both notice the same expired access token and both spend the same\nrefresh token — which pistis treats as a replay, and answers by revoking the\nwhole family.\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-9a997072a8090fca3d914919e059579a"}