@relyper/sp-auth 0.1.0 → 0.3.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.
Files changed (44) hide show
  1. package/README.md +212 -69
  2. package/dist/client.d.ts +33 -1
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +57 -4
  5. package/dist/client.js.map +1 -1
  6. package/dist/coins.d.ts +118 -0
  7. package/dist/coins.d.ts.map +1 -0
  8. package/dist/coins.js +158 -0
  9. package/dist/coins.js.map +1 -0
  10. package/dist/oidc/claims.d.ts +4 -0
  11. package/dist/oidc/claims.d.ts.map +1 -0
  12. package/dist/oidc/claims.js +50 -0
  13. package/dist/oidc/claims.js.map +1 -0
  14. package/dist/oidc/client.d.ts +44 -0
  15. package/dist/oidc/client.d.ts.map +1 -0
  16. package/dist/oidc/client.js +340 -0
  17. package/dist/oidc/client.js.map +1 -0
  18. package/dist/oidc/discovery.d.ts +15 -0
  19. package/dist/oidc/discovery.d.ts.map +1 -0
  20. package/dist/oidc/discovery.js +90 -0
  21. package/dist/oidc/discovery.js.map +1 -0
  22. package/dist/oidc/pkce.d.ts +6 -0
  23. package/dist/oidc/pkce.d.ts.map +1 -0
  24. package/dist/oidc/pkce.js +26 -0
  25. package/dist/oidc/pkce.js.map +1 -0
  26. package/dist/oidc/session.d.ts +47 -0
  27. package/dist/oidc/session.d.ts.map +1 -0
  28. package/dist/oidc/session.js +119 -0
  29. package/dist/oidc/session.js.map +1 -0
  30. package/dist/oidc/types.d.ts +130 -0
  31. package/dist/oidc/types.d.ts.map +1 -0
  32. package/dist/oidc/types.js +17 -0
  33. package/dist/oidc/types.js.map +1 -0
  34. package/dist/oidc-fastify.d.ts +123 -0
  35. package/dist/oidc-fastify.d.ts.map +1 -0
  36. package/dist/oidc-fastify.js +264 -0
  37. package/dist/oidc-fastify.js.map +1 -0
  38. package/dist/oidc.d.ts +15 -0
  39. package/dist/oidc.d.ts.map +1 -0
  40. package/dist/oidc.js +14 -0
  41. package/dist/oidc.js.map +1 -0
  42. package/dist/types.d.ts +13 -1
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +25 -5
package/README.md CHANGED
@@ -1,12 +1,8 @@
1
1
  # @relyper/sp-auth
2
2
 
3
- Service-provider side of Relyper identity: turn gateway headers into a typed
4
- principal, gate access by role, and keep the identity layer out of your
5
- application code.
6
-
7
- Built for services that sit behind the Relyper auth gateway. The package covers
8
- the parts every service provider repeats — header parsing, role gating, the
9
- `/me` contract, dev login — and leaves persistence to you.
3
+ Relyper identity for a service provider. Register your app at the Relyper IdP,
4
+ authenticate it with its client secret, and get a typed principal with role
5
+ gating — without writing an OIDC client yourself.
10
6
 
11
7
  ```bash
12
8
  npm install @relyper/sp-auth
@@ -14,18 +10,45 @@ npm install @relyper/sp-auth
14
10
 
15
11
  Requires Node 20+. ESM only.
16
12
 
17
- ## Quick start (Fastify)
13
+ Two integrations live here, and they are not alternatives of equal standing:
14
+
15
+ - **[OIDC login](#oidc-login)** — the app is a registered, confidential OIDC
16
+ client. Authorization Code Flow with PKCE, ID tokens verified against the
17
+ IdP's JWKS, sessions in encrypted cookies. **This is the one to use.**
18
+ - **[Gateway headers](#gateway-headers)** — the app trusts identity headers set
19
+ by a trusted proxy. No secret, no signature, no verification. Only defensible
20
+ when the app is unreachable except through that gateway.
21
+
22
+ ## OIDC login
23
+
24
+ ### 1. Register the service provider
25
+
26
+ In the Relyper IdP's admin UI under **OIDC Settings**: generate a client ID and
27
+ a client secret, set the redirect URI to `https://your-app.example/auth/callback`,
28
+ and list the application roles your app gates on under **Application Roles**.
29
+
30
+ The IdP stores only an Argon2id hash of the secret, so it cannot be read back
31
+ later. Copy it when it is generated.
32
+
33
+ ### 2. Register the plugin
18
34
 
19
35
  ```ts
20
36
  import Fastify from 'fastify';
21
- import { relyperAuth } from '@relyper/sp-auth/fastify';
37
+ import { relyperOidcAuth } from '@relyper/sp-auth/oidc/fastify';
22
38
 
23
39
  const app = Fastify();
24
40
 
25
- await app.register(relyperAuth, {
41
+ await app.register(relyperOidcAuth, {
42
+ issuer: process.env.RELYPER_OIDC_ISSUER, // https://api.relyper.de
43
+ clientId: process.env.RELYPER_OIDC_CLIENT_ID,
44
+ clientSecret: process.env.RELYPER_OIDC_CLIENT_SECRET,
45
+ redirectUri: process.env.RELYPER_OIDC_REDIRECT_URI,
46
+ sessionSecret: process.env.SESSION_SECRET, // 32+ chars, yours alone
47
+
26
48
  requiredRole: 'my_service_user',
27
49
  protect: (request) => request.url.startsWith('/api/'),
28
50
  meRoute: '/api/me',
51
+
29
52
  // Turn the IdP identity into your own user record.
30
53
  resolveUser: async (identity) => prisma.user.upsert({
31
54
  where: { idpSubject: identity.subject },
@@ -35,76 +58,136 @@ await app.register(relyperAuth, {
35
58
  });
36
59
 
37
60
  app.get('/api/cases', async (request) => {
38
- request.relyperIdentity; // { subject, email, displayName, roles }
61
+ request.relyperIdentity; // { subject, email, displayName, roles, tenantId, teams }
39
62
  request.principal; // whatever resolveUser returned
40
63
  });
41
64
  ```
42
65
 
43
- Declare the type of your own principal once:
66
+ That registers three routes `/auth/login`, `/auth/callback`, `/auth/logout` —
67
+ and guards everything `protect` selects.
68
+
69
+ `sessionSecret` is **not** the client secret. It is the key this app seals its
70
+ own cookies with, it never leaves the process, and rotating it logs everyone out.
71
+
72
+ ### 3. Drive it from the browser
44
73
 
45
74
  ```ts
46
- declare module 'fastify' {
47
- interface FastifyRequest {
48
- principal: { id: string; email: string };
49
- }
75
+ import { fetchRelyperSession, startRelyperLogin, startRelyperLogout } from '@relyper/sp-auth/client';
76
+
77
+ const session = await fetchRelyperSession<{ id: string; email: string }>();
78
+
79
+ switch (session.status) {
80
+ case 'authenticated': return session.user;
81
+ case 'unauthenticated': return startRelyperLogin({ loginUrl: session.loginUrl });
82
+ case 'forbidden': return showNoAccessScreen(session.message);
83
+ case 'error': return showError();
50
84
  }
51
85
  ```
52
86
 
53
- ## Without Fastify
54
-
55
- The core is a pure function over headers no framework, no I/O:
87
+ `startRelyperLogin` is a full navigation, not a fetch: the IdP has to be able to
88
+ show its own login page and set its own cookie, which an XHR cannot do. It
89
+ remembers the current path and returns the user there afterwards.
56
90
 
57
- ```ts
58
- import { createRelyperAuth } from '@relyper/sp-auth';
91
+ ### What the flow guarantees
59
92
 
60
- const auth = createRelyperAuth({ requiredRole: 'my_service_user' });
61
- const result = auth.authenticate(request.headers); // Node headers or a fetch Headers object
93
+ | Step | What is checked |
94
+ | --- | --- |
95
+ | `/auth/login` | Fresh `state`, `nonce` and PKCE verifier, sealed into a short-lived encrypted cookie |
96
+ | `/auth/callback` | `state` matches this browser's login; no cookie means no callback |
97
+ | Token exchange | `client_secret_basic` (or `_post`), with the PKCE verifier |
98
+ | ID token | RS256 signature against the IdP's JWKS, plus `iss`, `aud`, `exp`, `nonce`, `azp`, `sub` |
99
+ | Algorithms | Pinned, so `alg: none` and HMAC-with-public-key are refused |
100
+ | UserInfo (optional) | `sub` must match the ID token's, or the response is discarded |
101
+ | Role | Checked at login **and** on every request afterwards |
102
+ | `returnTo` | Local paths only, so the login cannot become an open redirect |
103
+
104
+ The client secret only ever travels from your server to the IdP's token
105
+ endpoint. It never reaches the browser.
106
+
107
+ ### Sessions
108
+
109
+ The session is a cookie sealed with `sessionSecret` — encrypted (JWE, direct
110
+ A256GCM), not merely signed, so the browser cannot read the user's claims and
111
+ tampering fails to decrypt rather than yielding a forged value. The login cookie
112
+ and the session cookie use separate keys derived from that one secret, so
113
+ neither can be replayed as the other.
114
+
115
+ Sessions are stateless by default. That has one consequence worth knowing: a
116
+ logout clears the cookie in the browser that asked, but a cookie copied
117
+ beforehand keeps working until it expires. When that matters, keep a revocation
118
+ list:
62
119
 
63
- if (!result.ok) {
64
- return new Response(JSON.stringify({ error: result.message }), { status: result.status });
65
- }
66
- result.identity.subject;
120
+ ```ts
121
+ await app.register(relyperOidcAuth, {
122
+ // ...
123
+ onLogin: (result, request, sessionId) => revocations.remember(sessionId),
124
+ onLogout: (sessionId) => revocations.revoke(sessionId),
125
+ isSessionRevoked: (sessionId) => revocations.isRevoked(sessionId)
126
+ });
67
127
  ```
68
128
 
69
- `authenticate` never throws and never does I/O, which makes it easy to unit test
70
- and safe to call on every request.
129
+ ### Without Fastify
71
130
 
72
- ## Browser client
131
+ The client is framework-free:
73
132
 
74
133
  ```ts
75
- import { fetchRelyperSession } from '@relyper/sp-auth/client';
134
+ import { createRelyperOidcClient } from '@relyper/sp-auth/oidc';
76
135
 
77
- const session = await fetchRelyperSession<{ id: string; email: string }>();
136
+ const client = createRelyperOidcClient({ issuer, clientId, clientSecret, redirectUri });
78
137
 
79
- switch (session.status) {
80
- case 'authenticated': return session.user;
81
- case 'unauthenticated': return redirectToLogin();
82
- case 'forbidden': return showNoAccessScreen();
83
- case 'error': return showError();
84
- }
138
+ // Start
139
+ const { url, transaction } = await client.createAuthorizationRequest({ returnTo: '/cases/7' });
140
+ // Persist `transaction` for this browser, then redirect to `url`.
141
+
142
+ // Finish
143
+ const { identity, claims, tokens } = await client.completeLogin({ query, transaction });
85
144
  ```
86
145
 
87
- No framework dependency. A Vue composable or React hook around it is a few lines.
146
+ Every failure is a `RelyperOidcError` with a `code`, an HTTP `status`, a message
147
+ safe to show a user, and a `detail` safe to log.
88
148
 
89
- ## Options
149
+ ### OIDC options
90
150
 
91
151
  | Option | Default | Purpose |
92
152
  | --- | --- | --- |
93
- | `requiredRole` | – | Role(s) required for this service. Omit to only require an identity. |
94
- | `roleMatch` | `'any'` | With several required roles: one is enough, or all are needed. |
95
- | `requireEmail` | `true` | Set to `false` if your IdP does not send an address. |
96
- | `headerNames` | Relyper headers | Override individual header names for another gateway. |
97
- | `acceptForwardedHeaders` | `false` | Also accept `x-forwarded-*`. Off by default on purpose. |
98
- | `devAuth` | `false` | Local login without a gateway. Never enable in production. |
99
- | `unauthenticatedStatus` | `401` | Status when no identity arrives. |
100
- | `forbiddenStatus` | `403` | Status when the role is missing. |
101
- | `message` | per code | Fixed string or a function for the error message. |
102
- | `parseRoles` | comma-separated | Custom splitting of the roles header. |
103
-
104
- Fastify adapter additions: `protect`, `hook`, `resolveUser`, `principalKey`,
105
- `meRoute`, `meResponse`, `errorBody`, `onAuthFailure`, `warnOnDevAuth`.
106
-
107
- ## Headers
153
+ | `issuer` | – | Base URL of the IdP, as it appears in `iss`. Required. |
154
+ | `clientId` / `clientSecret` | | This app's registration. Required. |
155
+ | `redirectUri` | | Must match the registered URI byte for byte. Required. |
156
+ | `sessionSecret` | | Key for this app's own cookies, 32+ chars. Fastify adapter only. |
157
+ | `scope` | `openid email profile roles tenant teams` | Requested scopes. |
158
+ | `tokenEndpointAuthMethod` | from discovery | `client_secret_basic` or `client_secret_post`. |
159
+ | `requiredRole` / `roleMatch` | / `'any'` | Role gate. |
160
+ | `requireEmail` | `true` | Refuse a login with no address. |
161
+ | `useUserInfo` | `false` | Also call UserInfo; the Relyper IdP puts everything in the ID token. |
162
+ | `clockToleranceSeconds` | `60` | Leeway for `exp` / `iat`. |
163
+ | `discoveryTtlMs` | `3600000` | How long the discovery document is reused. |
164
+ | `requestTimeoutMs` | `10000` | Timeout for every call to the IdP. |
165
+ | `mapClaims` | `defaultClaimsToIdentity` | Custom claim mapping. |
166
+ | `fetch` | global | Custom fetch, used for discovery, tokens, UserInfo and JWKS alike. |
167
+
168
+ Fastify adapter additions: `sessionCookieName`, `loginCookieName`, `cookieDomain`,
169
+ `cookiePath`, `cookieSecure`, `sessionTtlSeconds`, `sessionAbsoluteTtlSeconds`,
170
+ `loginTtlSeconds`, `rollingSession`, `keepIdToken`, `loginPath`, `callbackPath`,
171
+ `logoutPath`, `postLogoutRedirect`, `loginErrorRedirect`, `protect`, `hook`,
172
+ `resolveUser`, `principalKey`, `meRoute`, `meResponse`,
173
+ `redirectUnauthenticated`, `errorBody`, `onAuthFailure`, `onLogin`, `onLogout`,
174
+ `isSessionRevoked`.
175
+
176
+ ## Gateway headers
177
+
178
+ The original integration, for services behind a gateway that authenticates on
179
+ their behalf.
180
+
181
+ ```ts
182
+ import { relyperAuth } from '@relyper/sp-auth/fastify';
183
+
184
+ await app.register(relyperAuth, {
185
+ requiredRole: 'my_service_user',
186
+ protect: (request) => request.url.startsWith('/api/'),
187
+ meRoute: '/api/me',
188
+ resolveUser
189
+ });
190
+ ```
108
191
 
109
192
  | Header | Meaning |
110
193
  | --- | --- |
@@ -116,31 +199,91 @@ Fastify adapter additions: `protect`, `hook`, `resolveUser`, `principalKey`,
116
199
  Fallbacks when `acceptForwardedHeaders` is on: `x-forwarded-user`,
117
200
  `x-forwarded-email`, `x-forwarded-preferred-username`, `x-forwarded-groups`.
118
201
 
119
- ## Security model read this
202
+ Options: `requiredRole`, `roleMatch`, `requireEmail`, `headerNames`,
203
+ `acceptForwardedHeaders`, `devAuth`, `unauthenticatedStatus`, `forbiddenStatus`,
204
+ `message`, `parseRoles`. Fastify additions: `protect`, `hook`, `resolveUser`,
205
+ `principalKey`, `meRoute`, `meResponse`, `errorBody`, `onAuthFailure`,
206
+ `warnOnDevAuth`.
207
+
208
+ The core is a pure function over headers — no framework, no I/O, never throws:
209
+
210
+ ```ts
211
+ import { createRelyperAuth } from '@relyper/sp-auth';
212
+
213
+ const auth = createRelyperAuth({ requiredRole: 'my_service_user' });
214
+ const result = auth.authenticate(request.headers);
215
+ if (!result.ok) return reply.code(result.status).send({ error: result.message });
216
+ result.identity.subject;
217
+ ```
120
218
 
121
- This package trusts headers. It does **not** verify a token or a signature. That
122
- is only safe when your service is unreachable except through a gateway that
123
- strips client-supplied `x-relyper-*` headers and sets them itself.
219
+ ### Security model read this
124
220
 
125
- If your service can be reached directly, anyone can send
126
- `x-relyper-roles: my_service_user` and be admitted. Two rules follow:
221
+ This path trusts headers. It verifies no token and no signature. It is only safe
222
+ when the service is unreachable except through a gateway that **overwrites**
223
+ client-supplied `x-relyper-*` headers rather than passing them through.
127
224
 
128
- - Never expose the service port publicly without the gateway in front of it.
129
- - Never enable `devAuth` in production. It is off by default, and the Fastify
130
- adapter logs a warning the first time it is used.
225
+ If the service can be reached directly, anyone can send
226
+ `x-relyper-roles: my_service_user` and be admitted. `acceptForwardedHeaders`
227
+ widens that surface further, because `x-forwarded-*` is what any generic proxy
228
+ sets — which is why it is off by default.
131
229
 
132
- Token verification against the Relyper IdP (JWT/JWKS) is planned for a later
133
- version behind the same API, so switching should not require changes in calling
134
- code.
230
+ Prefer the OIDC login. It needs no such assumption about the network.
135
231
 
136
232
  ## Design
137
233
 
138
- - `authenticate` is pure: headers in, result out. No database, no fetch, no throw.
234
+ - The OIDC client does no I/O you did not ask for: discovery and JWKS are
235
+ fetched lazily, cached, and go through your `fetch` if you supply one.
139
236
  - Identity and application user stay separate. The package hands you a
140
237
  `RelyperIdentity`; `resolveUser` maps it to your own record. That boundary is
141
238
  what makes the package reusable across service providers.
142
- - `subject` is the IdP ID and never the primary key of your database.
239
+ - `subject` is the IdP's ID and never the primary key of your database.
240
+ - Errors carry a user-safe `message` and a separate `detail` for logs, so an
241
+ IdP's diagnostics never leak into a response.
143
242
 
144
243
  ## License
145
244
 
146
245
  MIT
246
+
247
+ ## Relyper Coins
248
+
249
+ Relyper Coins are held centrally: the wallet at the identity provider is the
250
+ single source of truth for what a user can spend across every Relyper product.
251
+ A service provider never keeps its own balance.
252
+
253
+ ```ts
254
+ import { createRelyperCoinsClient, coinsBaseUrlFromIssuer, RelyperCoinsError } from '@relyper/sp-auth/coins';
255
+
256
+ const coins = createRelyperCoinsClient({
257
+ // The IdP publishes OIDC at the issuer root but mounts its API under /api.
258
+ baseUrl: coinsBaseUrlFromIssuer(process.env.RELYPER_OIDC_ISSUER),
259
+ clientId: process.env.RELYPER_OIDC_CLIENT_ID,
260
+ clientSecret: process.env.RELYPER_OIDC_CLIENT_SECRET
261
+ });
262
+
263
+ const wallet = await coins.getWallet(identity.subject);
264
+
265
+ try {
266
+ await coins.debit({
267
+ subject: identity.subject,
268
+ amountRc: 5,
269
+ reason: 'assistant.question',
270
+ idempotencyKey: 'my-app:' + requestHash, // a retry must not charge twice
271
+ product: 'my-app',
272
+ provider: 'openai',
273
+ model: 'gpt-x'
274
+ });
275
+ } catch (error) {
276
+ if (error instanceof RelyperCoinsError && error.code === 'insufficient_funds') {
277
+ // error.balanceRc / error.requestedRc
278
+ }
279
+ }
280
+ ```
281
+
282
+ The same client ID and secret as the OIDC login, so the IdP can attribute every
283
+ debit to one registered app and an operator can allow or revoke coin spending
284
+ per app. The app has to be ticked as **May consume Relyper Coins** in the IdP
285
+ admin UI, or `debit` throws with code `coins_not_enabled`.
286
+
287
+ `debit` refuses to overdraw a wallet. Because the exact cost of a request is
288
+ usually only known after the work is done, check the balance against an estimate
289
+ first and debit the real amount afterwards.
package/dist/client.d.ts CHANGED
@@ -8,15 +8,20 @@ export type RelyperSession<TUser = RelyperIdentity> = {
8
8
  status: 'authenticated';
9
9
  user: TUser;
10
10
  }
11
- /** No identity passed through the gateway. Typically: login required. */
11
+ /**
12
+ * Nobody is signed in. With the OIDC integration the server also names the
13
+ * route that starts a login; {@link startRelyperLogin} sends the browser there.
14
+ */
12
15
  | {
13
16
  status: 'unauthenticated';
14
17
  response: Response;
18
+ loginUrl: string;
15
19
  }
16
20
  /** Signed in, but without the role required for this service provider. */
17
21
  | {
18
22
  status: 'forbidden';
19
23
  response: Response;
24
+ message?: string;
20
25
  } | {
21
26
  status: 'error';
22
27
  response: Response;
@@ -30,8 +35,35 @@ export type FetchSessionOptions = {
30
35
  signal?: AbortSignal;
31
36
  /** Default: 'same-origin'. */
32
37
  credentials?: RequestCredentials;
38
+ /** Fallback when the server names no login route. Default: '/auth/login'. */
39
+ loginPath?: string;
33
40
  };
34
41
  export declare function fetchRelyperSession<TUser = RelyperIdentity>(options?: FetchSessionOptions): Promise<RelyperSession<TUser>>;
42
+ export type StartLoginOptions = {
43
+ /** Route that starts the login. Default: '/auth/login'. */
44
+ loginUrl?: string;
45
+ /**
46
+ * Where to land after the login. Default: the current path including query
47
+ * and hash, so the user resumes exactly where the session ran out.
48
+ */
49
+ returnTo?: string;
50
+ };
51
+ /**
52
+ * Sends the browser into the OIDC login.
53
+ *
54
+ * A full navigation, not a fetch: the identity provider has to be able to show
55
+ * its own login page and set its own session cookie, which an XHR cannot do.
56
+ * `location.replace` keeps the expired page out of the back-button history.
57
+ */
58
+ export declare function startRelyperLogin(options?: StartLoginOptions): void;
59
+ /**
60
+ * Ends the session. A form POST rather than fetch, so the browser follows the
61
+ * server's redirect and, where the identity provider supports it, the logout
62
+ * continues on to end the session there too.
63
+ */
64
+ export declare function startRelyperLogout(options?: {
65
+ logoutUrl?: string;
66
+ }): void;
35
67
  /** true if the identity has at least one of the given roles. */
36
68
  export declare function hasAnyRole(identity: Pick<RelyperIdentity, 'roles'>, roles: string[]): boolean;
37
69
  export type { RelyperIdentity };
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;GAIG;AAEH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,eAAe,IAC9C;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE;AAC1C,yEAAyE;GACvE;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE;AACnD,0EAA0E;GACxE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAC3C;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAE5C,MAAM,MAAM,mBAAmB,GAAG;IAChC,0BAA0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC,CAAC;AAEF,wBAAsB,mBAAmB,CAAC,KAAK,GAAG,eAAe,EAC/D,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAkBhC;AAED,gEAAgE;AAChE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAE7F;AAED,YAAY,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;GAIG;AAEH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,eAAe,IAC9C;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE;AAC1C;;;GAGG;GACD;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;AACrE,0EAA0E;GACxE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAE5C,MAAM,MAAM,mBAAmB,GAAG;IAChC,0BAA0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAIF,wBAAsB,mBAAmB,CAAC,KAAK,GAAG,eAAe,EAC/D,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAkChC;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,iBAAsB,GAAG,IAAI,CASvE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,IAAI,CAS7E;AAED,gEAAgE;AAChE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAE7F;AAUD,YAAY,EAAE,eAAe,EAAE,CAAC"}
package/dist/client.js CHANGED
@@ -1,3 +1,4 @@
1
+ const DEFAULT_LOGIN_PATH = '/auth/login';
1
2
  export async function fetchRelyperSession(options = {}) {
2
3
  const doFetch = options.fetch ?? globalThis.fetch;
3
4
  if (!doFetch)
@@ -5,21 +6,73 @@ export async function fetchRelyperSession(options = {}) {
5
6
  const response = await doFetch(options.path ?? '/api/me', {
6
7
  method: 'GET',
7
8
  credentials: options.credentials ?? 'same-origin',
9
+ // Without this the browser would follow a redirect to the identity provider
10
+ // inside the request, and the caller would see an opaque cross-origin
11
+ // failure instead of the status it can act on.
12
+ redirect: 'manual',
8
13
  headers: { accept: 'application/json', ...(options.headers ?? {}) },
9
14
  signal: options.signal
10
15
  });
11
- if (response.status === 401)
12
- return { status: 'unauthenticated', response };
13
- if (response.status === 403)
14
- return { status: 'forbidden', response };
16
+ if (response.status === 401 || response.type === 'opaqueredirect') {
17
+ const body = await readJsonBody(response);
18
+ const loginUrl = typeof body?.loginUrl === 'string' && body.loginUrl
19
+ ? body.loginUrl
20
+ : options.loginPath ?? DEFAULT_LOGIN_PATH;
21
+ return { status: 'unauthenticated', response, loginUrl };
22
+ }
23
+ if (response.status === 403) {
24
+ const body = await readJsonBody(response);
25
+ const message = typeof body?.error === 'string' ? body.error : undefined;
26
+ return { status: 'forbidden', response, message };
27
+ }
15
28
  if (!response.ok)
16
29
  return { status: 'error', response };
17
30
  const body = (await response.json());
18
31
  const user = body.user ?? body;
19
32
  return { status: 'authenticated', user };
20
33
  }
34
+ /**
35
+ * Sends the browser into the OIDC login.
36
+ *
37
+ * A full navigation, not a fetch: the identity provider has to be able to show
38
+ * its own login page and set its own session cookie, which an XHR cannot do.
39
+ * `location.replace` keeps the expired page out of the back-button history.
40
+ */
41
+ export function startRelyperLogin(options = {}) {
42
+ if (typeof window === 'undefined') {
43
+ throw new Error('@relyper/sp-auth/client: startRelyperLogin needs a browser environment.');
44
+ }
45
+ const base = options.loginUrl ?? DEFAULT_LOGIN_PATH;
46
+ const returnTo = options.returnTo
47
+ ?? window.location.pathname + window.location.search + window.location.hash;
48
+ const separator = base.includes('?') ? '&' : '?';
49
+ window.location.replace(base + separator + 'returnTo=' + encodeURIComponent(returnTo));
50
+ }
51
+ /**
52
+ * Ends the session. A form POST rather than fetch, so the browser follows the
53
+ * server's redirect and, where the identity provider supports it, the logout
54
+ * continues on to end the session there too.
55
+ */
56
+ export function startRelyperLogout(options = {}) {
57
+ if (typeof window === 'undefined') {
58
+ throw new Error('@relyper/sp-auth/client: startRelyperLogout needs a browser environment.');
59
+ }
60
+ const form = window.document.createElement('form');
61
+ form.method = 'POST';
62
+ form.action = options.logoutUrl ?? '/auth/logout';
63
+ window.document.body.appendChild(form);
64
+ form.submit();
65
+ }
21
66
  /** true if the identity has at least one of the given roles. */
22
67
  export function hasAnyRole(identity, roles) {
23
68
  return roles.some((role) => identity.roles.includes(role));
24
69
  }
70
+ async function readJsonBody(response) {
71
+ try {
72
+ return (await response.clone().json());
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
25
78
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AA2BA,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAO,GAAwB,EAAE;IAEjC,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IAClD,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IAE7F,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,SAAS,EAAE;QACxD,MAAM,EAAE,KAAK;QACb,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,aAAa;QACjD,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE;QACnE,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC;IAC5E,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;IACtE,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAEvD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA6B,CAAC;IACjE,MAAM,IAAI,GAAI,IAAyB,CAAC,IAAI,IAAK,IAAc,CAAC;IAChE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC;AAC3C,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,UAAU,CAAC,QAAwC,EAAE,KAAe;IAClF,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;AAC7D,CAAC"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Client for the Relyper Coins integration API.
3
+ *
4
+ * Relyper Coins are held centrally: the wallet at the identity provider is the
5
+ * single source of truth for what a user can spend across every Relyper product.
6
+ * A service provider therefore never keeps its own balance -- it asks, and it
7
+ * debits, and anything it stores locally is a mirror for its own reporting.
8
+ *
9
+ * The service provider authenticates with the same client ID and secret it uses
10
+ * for the OIDC login, so every debit is attributable to one registered client
11
+ * and an operator can allow or refuse coin spending per app.
12
+ */
13
+ export type RelyperCoinWallet = {
14
+ ownerKind: 'user' | 'tenant';
15
+ ownerId: string;
16
+ balanceRc: number;
17
+ lifetimeEarnedRc: number;
18
+ lifetimeSpentRc: number;
19
+ reputationPoints: number;
20
+ convertibleRc: number;
21
+ planTier: string | null;
22
+ planMonthlyAllowanceRc: number;
23
+ };
24
+ export type RelyperCoinLedgerEntry = {
25
+ direction: 'credit' | 'debit';
26
+ amountRc: number;
27
+ balanceAfterRc: number;
28
+ reason: string;
29
+ product: string | null;
30
+ provider: string | null;
31
+ model: string | null;
32
+ clientId: string | null;
33
+ createdAt: string;
34
+ };
35
+ export type RelyperCoinDebit = {
36
+ /** IdP subject of the user, i.e. `identity.subject` from the login. */
37
+ subject: string;
38
+ amountRc: number;
39
+ /** Short machine-readable reason, e.g. 'assistant.question'. */
40
+ reason: string;
41
+ /**
42
+ * Makes the debit repeatable without double-charging. Strongly recommended:
43
+ * a retry after a timeout would otherwise bill the user twice.
44
+ */
45
+ idempotencyKey?: string;
46
+ /** Product label shown in the identity provider's reporting. */
47
+ product?: string;
48
+ provider?: string;
49
+ model?: string;
50
+ tenantId?: string;
51
+ meta?: Record<string, unknown>;
52
+ };
53
+ export type RelyperCoinDebitResult = {
54
+ wallet: RelyperCoinWallet;
55
+ entry: RelyperCoinLedgerEntry | null;
56
+ /** True when this idempotency key had already been booked; nothing was charged again. */
57
+ deduplicated: boolean;
58
+ };
59
+ export type RelyperCoinsErrorCode =
60
+ /** The wallet does not hold enough coins. */
61
+ 'insufficient_funds'
62
+ /** This client is registered but not permitted to spend coins. */
63
+ | 'coins_not_enabled'
64
+ /** Client credentials rejected. */
65
+ | 'unauthorized'
66
+ /** The request was malformed. */
67
+ | 'invalid_request'
68
+ /** The identity provider was unreachable or answered unexpectedly. */
69
+ | 'unavailable';
70
+ export declare class RelyperCoinsError extends Error {
71
+ readonly code: RelyperCoinsErrorCode;
72
+ readonly status: number;
73
+ /** Present on insufficient_funds. */
74
+ readonly balanceRc?: number;
75
+ readonly requestedRc?: number;
76
+ readonly cause?: unknown;
77
+ constructor(code: RelyperCoinsErrorCode, message: string, options?: {
78
+ status?: number;
79
+ balanceRc?: number;
80
+ requestedRc?: number;
81
+ cause?: unknown;
82
+ });
83
+ }
84
+ export type RelyperCoinsOptions = {
85
+ /**
86
+ * Base URL of the identity provider's API, including the path prefix its
87
+ * routes are mounted under. For Relyper that is the issuer plus `/api`, e.g.
88
+ * `https://api.relyper.de/api` for issuer `https://api.relyper.de`.
89
+ */
90
+ baseUrl: string;
91
+ /** Same credentials as the OIDC login. */
92
+ clientId: string;
93
+ clientSecret: string;
94
+ /** Default: 10000. */
95
+ requestTimeoutMs?: number;
96
+ fetch?: typeof globalThis.fetch;
97
+ };
98
+ export type RelyperCoinsClient = {
99
+ /** Balance and plan allowance of one user's wallet. */
100
+ getWallet(subject: string): Promise<RelyperCoinWallet>;
101
+ /** Recent ledger entries for one user. */
102
+ getLedger(subject: string, limit?: number): Promise<RelyperCoinLedgerEntry[]>;
103
+ /**
104
+ * Spends coins. Throws {@link RelyperCoinsError} with code `insufficient_funds`
105
+ * when the wallet cannot cover the amount -- the debit is refused, not
106
+ * overdrawn.
107
+ */
108
+ debit(input: RelyperCoinDebit): Promise<RelyperCoinDebitResult>;
109
+ };
110
+ /**
111
+ * Derives the coins API base URL from an OIDC issuer.
112
+ *
113
+ * The Relyper IdP publishes OIDC at the issuer root but mounts its regular API
114
+ * under `/api`, so the two differ by exactly that segment.
115
+ */
116
+ export declare function coinsBaseUrlFromIssuer(issuer: string): string;
117
+ export declare function createRelyperCoinsClient(options: RelyperCoinsOptions): RelyperCoinsClient;
118
+ //# sourceMappingURL=coins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coins.d.ts","sourceRoot":"","sources":["../src/coins.ts"],"names":[],"mappings":"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"}