@~lyre/auth 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,89 @@ Ships built ESM output from `dist/`, with type declarations.
13
13
  pnpm add @~lyre/auth # or: npm i / yarn add
14
14
  ```
15
15
 
16
+ ## Sign in with Nipate Accounts
17
+
18
+ Since 0.12.0 sign-in follows the Accounts OpenID Connect contract
19
+ (`nipate/platform/docs/12-accounts-oidc.md`): authorization code + PKCE S256
20
+ for every app, `state` + `nonce`, form-encoded token requests with client
21
+ authentication, an RS256 `id_token` verified against the JWKS, and rotating
22
+ refresh tokens. No new dependencies: JWT checks use `node:crypto`.
23
+
24
+ Register the app in Accounts first (client id = app id; exact redirect URIs).
25
+ Confidential apps (anything with a server) get a client secret; public apps
26
+ (browser-only, CLI) have none and authenticate with `client_id` only.
27
+
28
+ ### SvelteKit
29
+
30
+ ```ts
31
+ // src/hooks.server.ts
32
+ import { createAccountsClientConfig } from '@~lyre/auth';
33
+ import { createAuthHandle } from '@~lyre/auth/sveltekit';
34
+
35
+ export const handle = createAuthHandle({
36
+ config: createAccountsClientConfig({
37
+ baseUrl: 'https://accounts.nipate.africa', // ACCOUNTS_BASE_URL (issuer: ACCOUNTS_ISSUER)
38
+ clientId: 'shop', // ACCOUNTS_CLIENT_ID
39
+ clientSecret: process.env.ACCOUNTS_CLIENT_SECRET, // confidential apps only
40
+ redirectUri: 'https://shop.example/auth/callback',
41
+ logoutRedirectUri: 'https://shop.example/'
42
+ // scopes default to: openid profile email offline_access
43
+ }),
44
+ sessionSecret: process.env.SESSION_SECRET, // signs the session AND the login-state cookie
45
+ endAccountsSession: false, // true: logout also ends the Accounts session
46
+ protect: (event) => event.url.pathname.startsWith('/account')
47
+ });
48
+ ```
49
+
50
+ `/auth/login` stores `state`, `nonce` and the PKCE verifier in a 10-minute
51
+ signed httpOnly cookie and redirects to `/auth/authorize`; `/auth/callback`
52
+ checks state, exchanges the code, verifies the id_token and writes the app's
53
+ own session; requests refresh the access token shortly before it expires
54
+ (storing the rotated refresh token; a refused refresh signs the user out);
55
+ `/auth/logout` revokes the refresh token. Failures become a 400 naming the
56
+ `OidcError` code, or whatever `onAuthError` returns. Mock mode
57
+ (`useMock`) keeps the old demo flow for local work without Accounts.
58
+
59
+ ### Plain Node
60
+
61
+ ```ts
62
+ import { createOidcClient, isOidcError } from '@~lyre/auth';
63
+
64
+ const accounts = createOidcClient({
65
+ issuer: 'https://accounts.nipate.africa',
66
+ clientId: 'premail',
67
+ clientSecret: process.env.ACCOUNTS_CLIENT_SECRET, // omit for a public client
68
+ redirectUri: 'https://premail.example/auth/callback',
69
+ postLogoutRedirectUri: 'https://premail.example/',
70
+ discovery: true // optional: read endpoints from /.well-known/openid-configuration (cached)
71
+ });
72
+
73
+ // 1. Start: keep state, nonce and codeVerifier (server-side or in a signed httpOnly cookie).
74
+ const login = await accounts.beginLogin();
75
+ // createLoginStateCookie(login, { nextPath, secret }) / readLoginStateCookie(header, { secret })
76
+ res.redirect(login.url);
77
+
78
+ // 2. Callback: throws OidcError with a code (state_mismatch, nonce_mismatch, audience_mismatch,
79
+ // token_expired, token_request_failed + oauthError, …).
80
+ const { claims, tokens } = await accounts.handleCallback({ url: req.url, expected: login });
81
+ // claims.sub is the user id. Create your own session; store tokens.refreshToken.
82
+
83
+ // 3. Later
84
+ const fresh = await accounts.refresh(tokens.refreshToken!); // ALWAYS store fresh.refreshToken
85
+ const { claims: at } = await accounts.verifyAccessToken(fresh.accessToken); // local JWKS check
86
+ await accounts.revoke(fresh.refreshToken!);
87
+ res.redirect(await accounts.logoutUrl()); // optional: end the Accounts session
88
+ ```
89
+
90
+ Services that only receive bearer tokens use `createAccessTokenVerifier({ issuer, audience })`,
91
+ or `createUserSessionResolver({ config: { baseUrl }, audience })` from `@~lyre/auth/nest`.
92
+ Keys are cached and refetched when a token names an unknown `kid`; if the JWKS
93
+ is unreachable, userinfo vouches for the token instead (`remoteFallback`).
94
+
95
+ **Deprecated in 0.12.0** (still exported; removed in a later major):
96
+ `beginAccountsLoginRedirect`, `handleAccountsCallback`, `exchangeAuthorizationCode`,
97
+ `logoutFromAccounts`, and the `AuthorizationState` / `AccountsTokenResponse` types.
98
+
16
99
  ## Entry points
17
100
 
18
101
  ### `@~lyre/auth` — framework-agnostic core
@@ -33,7 +116,7 @@ import {
33
116
  } from '@~lyre/auth';
34
117
  ```
35
118
 
36
- The core depends only on `node:crypto` — no framework required.
119
+ The core depends only on `node:crypto` and `fetch` — no framework required.
37
120
 
38
121
  ### `@~lyre/auth/sveltekit` — turnkey SvelteKit adapter
39
122
 
@@ -65,7 +148,10 @@ import { createServiceKeyResolver, createUserSessionResolver, hasScopes }
65
148
  from '@~lyre/auth/nest';
66
149
 
67
150
  const keys = createServiceKeyResolver({ config: { baseUrl: ACCOUNTS_BASE_URL } });
68
- const sessions = createUserSessionResolver({ config: { baseUrl: ACCOUNTS_BASE_URL } });
151
+ const sessions = createUserSessionResolver({
152
+ config: { baseUrl: ACCOUNTS_BASE_URL },
153
+ audience: 'my-app' // accept only tokens issued to these client ids
154
+ });
69
155
 
70
156
  // In a Nest guard (req is the Fastify request):
71
157
  const key = await keys.resolve(req);
@@ -75,9 +161,9 @@ if (key.status === 'ok') {
75
161
  // key.context.accessibleApps tells you which apps this key may act on
76
162
  }
77
163
 
78
- // A user session bearer resolves to who + which app, from the token alone:
164
+ // An access-token bearer resolves to who + which app, verified locally against the JWKS:
79
165
  const user = await sessions.resolve(req);
80
- // → { userId, sessionId, appId, tenantId, email, name } | null
166
+ // → { userId, sessionId, appId, tenantId, scopes, verifiedBy } | null
81
167
  ```
82
168
 
83
169
  Semantics match `createServiceKeyHandle`: **absent key → `absent`** (fall through to
@@ -87,8 +173,9 @@ results too, so a bad key can't hammer Accounts. Neither resolver throws: a
87
173
  transport failure reads as "not authenticated", so a flaky Accounts cannot 500
88
174
  the consuming service.
89
175
 
90
- `resolve()` needs `GET /api/auth/me` to return `session.appId` (Accounts ≥ the
91
- release that added it); older deployments yield `appId: null`.
176
+ JWT bearers are verified locally (`aud` → `appId`, `tid` → `tenantId`, `sid` →
177
+ `sessionId`). Opaque pre-JWT session ids still go to the legacy
178
+ `GET /api/auth/me` until you pass `legacySessionTokens: false`.
92
179
 
93
180
  ## Publishing
94
181