@relyper/sp-auth 0.1.0 → 0.4.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 +216 -69
- package/dist/client.d.ts +33 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +57 -4
- package/dist/client.js.map +1 -1
- package/dist/coins.d.ts +139 -0
- package/dist/coins.d.ts.map +1 -0
- package/dist/coins.js +210 -0
- package/dist/coins.js.map +1 -0
- package/dist/oidc/claims.d.ts +4 -0
- package/dist/oidc/claims.d.ts.map +1 -0
- package/dist/oidc/claims.js +50 -0
- package/dist/oidc/claims.js.map +1 -0
- package/dist/oidc/client.d.ts +44 -0
- package/dist/oidc/client.d.ts.map +1 -0
- package/dist/oidc/client.js +340 -0
- package/dist/oidc/client.js.map +1 -0
- package/dist/oidc/discovery.d.ts +15 -0
- package/dist/oidc/discovery.d.ts.map +1 -0
- package/dist/oidc/discovery.js +90 -0
- package/dist/oidc/discovery.js.map +1 -0
- package/dist/oidc/pkce.d.ts +6 -0
- package/dist/oidc/pkce.d.ts.map +1 -0
- package/dist/oidc/pkce.js +26 -0
- package/dist/oidc/pkce.js.map +1 -0
- package/dist/oidc/session.d.ts +47 -0
- package/dist/oidc/session.d.ts.map +1 -0
- package/dist/oidc/session.js +119 -0
- package/dist/oidc/session.js.map +1 -0
- package/dist/oidc/types.d.ts +130 -0
- package/dist/oidc/types.d.ts.map +1 -0
- package/dist/oidc/types.js +17 -0
- package/dist/oidc/types.js.map +1 -0
- package/dist/oidc-fastify.d.ts +123 -0
- package/dist/oidc-fastify.d.ts.map +1 -0
- package/dist/oidc-fastify.js +264 -0
- package/dist/oidc-fastify.js.map +1 -0
- package/dist/oidc.d.ts +15 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +14 -0
- package/dist/oidc.js.map +1 -0
- package/dist/types.d.ts +13 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +25 -5
package/README.md
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
# @relyper/sp-auth
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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,49 @@ npm install @relyper/sp-auth
|
|
|
14
10
|
|
|
15
11
|
Requires Node 20+. ESM only.
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
Two ways to establish who the user is, and they are not alternatives of equal
|
|
14
|
+
standing:
|
|
15
|
+
|
|
16
|
+
- **[OIDC login](#oidc-login)** — the app is a registered, confidential OIDC
|
|
17
|
+
client. Authorization Code Flow with PKCE, ID tokens verified against the
|
|
18
|
+
IdP's JWKS, sessions in encrypted cookies. **This is the one to use.**
|
|
19
|
+
- **[Gateway headers](#gateway-headers)** — the app trusts identity headers set
|
|
20
|
+
by a trusted proxy. No secret, no signature, no verification. Only defensible
|
|
21
|
+
when the app is unreachable except through that gateway.
|
|
22
|
+
|
|
23
|
+
On top of that, **[Relyper Coins](#relyper-coins)** lets an app spend from the
|
|
24
|
+
central wallets, authenticating with the same client credentials as the login.
|
|
25
|
+
|
|
26
|
+
## OIDC login
|
|
27
|
+
|
|
28
|
+
### 1. Register the service provider
|
|
29
|
+
|
|
30
|
+
In the Relyper IdP's admin UI under **OIDC Settings**: generate a client ID and
|
|
31
|
+
a client secret, set the redirect URI to `https://your-app.example/auth/callback`,
|
|
32
|
+
and list the application roles your app gates on under **Application Roles**.
|
|
33
|
+
|
|
34
|
+
The IdP stores only an Argon2id hash of the secret, so it cannot be read back
|
|
35
|
+
later. Copy it when it is generated.
|
|
36
|
+
|
|
37
|
+
### 2. Register the plugin
|
|
18
38
|
|
|
19
39
|
```ts
|
|
20
40
|
import Fastify from 'fastify';
|
|
21
|
-
import {
|
|
41
|
+
import { relyperOidcAuth } from '@relyper/sp-auth/oidc/fastify';
|
|
22
42
|
|
|
23
43
|
const app = Fastify();
|
|
24
44
|
|
|
25
|
-
await app.register(
|
|
45
|
+
await app.register(relyperOidcAuth, {
|
|
46
|
+
issuer: process.env.RELYPER_OIDC_ISSUER, // https://api.relyper.de
|
|
47
|
+
clientId: process.env.RELYPER_OIDC_CLIENT_ID,
|
|
48
|
+
clientSecret: process.env.RELYPER_OIDC_CLIENT_SECRET,
|
|
49
|
+
redirectUri: process.env.RELYPER_OIDC_REDIRECT_URI,
|
|
50
|
+
sessionSecret: process.env.SESSION_SECRET, // 32+ chars, yours alone
|
|
51
|
+
|
|
26
52
|
requiredRole: 'my_service_user',
|
|
27
53
|
protect: (request) => request.url.startsWith('/api/'),
|
|
28
54
|
meRoute: '/api/me',
|
|
55
|
+
|
|
29
56
|
// Turn the IdP identity into your own user record.
|
|
30
57
|
resolveUser: async (identity) => prisma.user.upsert({
|
|
31
58
|
where: { idpSubject: identity.subject },
|
|
@@ -35,76 +62,136 @@ await app.register(relyperAuth, {
|
|
|
35
62
|
});
|
|
36
63
|
|
|
37
64
|
app.get('/api/cases', async (request) => {
|
|
38
|
-
request.relyperIdentity; // { subject, email, displayName, roles }
|
|
65
|
+
request.relyperIdentity; // { subject, email, displayName, roles, tenantId, teams }
|
|
39
66
|
request.principal; // whatever resolveUser returned
|
|
40
67
|
});
|
|
41
68
|
```
|
|
42
69
|
|
|
43
|
-
|
|
70
|
+
That registers three routes — `/auth/login`, `/auth/callback`, `/auth/logout` —
|
|
71
|
+
and guards everything `protect` selects.
|
|
72
|
+
|
|
73
|
+
`sessionSecret` is **not** the client secret. It is the key this app seals its
|
|
74
|
+
own cookies with, it never leaves the process, and rotating it logs everyone out.
|
|
75
|
+
|
|
76
|
+
### 3. Drive it from the browser
|
|
44
77
|
|
|
45
78
|
```ts
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
import { fetchRelyperSession, startRelyperLogin, startRelyperLogout } from '@relyper/sp-auth/client';
|
|
80
|
+
|
|
81
|
+
const session = await fetchRelyperSession<{ id: string; email: string }>();
|
|
82
|
+
|
|
83
|
+
switch (session.status) {
|
|
84
|
+
case 'authenticated': return session.user;
|
|
85
|
+
case 'unauthenticated': return startRelyperLogin({ loginUrl: session.loginUrl });
|
|
86
|
+
case 'forbidden': return showNoAccessScreen(session.message);
|
|
87
|
+
case 'error': return showError();
|
|
50
88
|
}
|
|
51
89
|
```
|
|
52
90
|
|
|
53
|
-
|
|
91
|
+
`startRelyperLogin` is a full navigation, not a fetch: the IdP has to be able to
|
|
92
|
+
show its own login page and set its own cookie, which an XHR cannot do. It
|
|
93
|
+
remembers the current path and returns the user there afterwards.
|
|
54
94
|
|
|
55
|
-
|
|
95
|
+
### What the flow guarantees
|
|
56
96
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
97
|
+
| Step | What is checked |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `/auth/login` | Fresh `state`, `nonce` and PKCE verifier, sealed into a short-lived encrypted cookie |
|
|
100
|
+
| `/auth/callback` | `state` matches this browser's login; no cookie means no callback |
|
|
101
|
+
| Token exchange | `client_secret_basic` (or `_post`), with the PKCE verifier |
|
|
102
|
+
| ID token | RS256 signature against the IdP's JWKS, plus `iss`, `aud`, `exp`, `nonce`, `azp`, `sub` |
|
|
103
|
+
| Algorithms | Pinned, so `alg: none` and HMAC-with-public-key are refused |
|
|
104
|
+
| UserInfo (optional) | `sub` must match the ID token's, or the response is discarded |
|
|
105
|
+
| Role | Checked at login **and** on every request afterwards |
|
|
106
|
+
| `returnTo` | Local paths only, so the login cannot become an open redirect |
|
|
107
|
+
|
|
108
|
+
The client secret only ever travels from your server to the IdP's token
|
|
109
|
+
endpoint. It never reaches the browser.
|
|
110
|
+
|
|
111
|
+
### Sessions
|
|
112
|
+
|
|
113
|
+
The session is a cookie sealed with `sessionSecret` — encrypted (JWE, direct
|
|
114
|
+
A256GCM), not merely signed, so the browser cannot read the user's claims and
|
|
115
|
+
tampering fails to decrypt rather than yielding a forged value. The login cookie
|
|
116
|
+
and the session cookie use separate keys derived from that one secret, so
|
|
117
|
+
neither can be replayed as the other.
|
|
118
|
+
|
|
119
|
+
Sessions are stateless by default. That has one consequence worth knowing: a
|
|
120
|
+
logout clears the cookie in the browser that asked, but a cookie copied
|
|
121
|
+
beforehand keeps working until it expires. When that matters, keep a revocation
|
|
122
|
+
list:
|
|
62
123
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
result.
|
|
124
|
+
```ts
|
|
125
|
+
await app.register(relyperOidcAuth, {
|
|
126
|
+
// ...
|
|
127
|
+
onLogin: (result, request, sessionId) => revocations.remember(sessionId),
|
|
128
|
+
onLogout: (sessionId) => revocations.revoke(sessionId),
|
|
129
|
+
isSessionRevoked: (sessionId) => revocations.isRevoked(sessionId)
|
|
130
|
+
});
|
|
67
131
|
```
|
|
68
132
|
|
|
69
|
-
|
|
70
|
-
and safe to call on every request.
|
|
133
|
+
### Without Fastify
|
|
71
134
|
|
|
72
|
-
|
|
135
|
+
The client is framework-free:
|
|
73
136
|
|
|
74
137
|
```ts
|
|
75
|
-
import {
|
|
138
|
+
import { createRelyperOidcClient } from '@relyper/sp-auth/oidc';
|
|
76
139
|
|
|
77
|
-
const
|
|
140
|
+
const client = createRelyperOidcClient({ issuer, clientId, clientSecret, redirectUri });
|
|
78
141
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
142
|
+
// Start
|
|
143
|
+
const { url, transaction } = await client.createAuthorizationRequest({ returnTo: '/cases/7' });
|
|
144
|
+
// Persist `transaction` for this browser, then redirect to `url`.
|
|
145
|
+
|
|
146
|
+
// Finish
|
|
147
|
+
const { identity, claims, tokens } = await client.completeLogin({ query, transaction });
|
|
85
148
|
```
|
|
86
149
|
|
|
87
|
-
|
|
150
|
+
Every failure is a `RelyperOidcError` with a `code`, an HTTP `status`, a message
|
|
151
|
+
safe to show a user, and a `detail` safe to log.
|
|
88
152
|
|
|
89
|
-
|
|
153
|
+
### OIDC options
|
|
90
154
|
|
|
91
155
|
| Option | Default | Purpose |
|
|
92
156
|
| --- | --- | --- |
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
157
|
+
| `issuer` | – | Base URL of the IdP, as it appears in `iss`. Required. |
|
|
158
|
+
| `clientId` / `clientSecret` | – | This app's registration. Required. |
|
|
159
|
+
| `redirectUri` | – | Must match the registered URI byte for byte. Required. |
|
|
160
|
+
| `sessionSecret` | – | Key for this app's own cookies, 32+ chars. Fastify adapter only. |
|
|
161
|
+
| `scope` | `openid email profile roles tenant teams` | Requested scopes. |
|
|
162
|
+
| `tokenEndpointAuthMethod` | from discovery | `client_secret_basic` or `client_secret_post`. |
|
|
163
|
+
| `requiredRole` / `roleMatch` | – / `'any'` | Role gate. |
|
|
164
|
+
| `requireEmail` | `true` | Refuse a login with no address. |
|
|
165
|
+
| `useUserInfo` | `false` | Also call UserInfo; the Relyper IdP puts everything in the ID token. |
|
|
166
|
+
| `clockToleranceSeconds` | `60` | Leeway for `exp` / `iat`. |
|
|
167
|
+
| `discoveryTtlMs` | `3600000` | How long the discovery document is reused. |
|
|
168
|
+
| `requestTimeoutMs` | `10000` | Timeout for every call to the IdP. |
|
|
169
|
+
| `mapClaims` | `defaultClaimsToIdentity` | Custom claim mapping. |
|
|
170
|
+
| `fetch` | global | Custom fetch, used for discovery, tokens, UserInfo and JWKS alike. |
|
|
171
|
+
|
|
172
|
+
Fastify adapter additions: `sessionCookieName`, `loginCookieName`, `cookieDomain`,
|
|
173
|
+
`cookiePath`, `cookieSecure`, `sessionTtlSeconds`, `sessionAbsoluteTtlSeconds`,
|
|
174
|
+
`loginTtlSeconds`, `rollingSession`, `keepIdToken`, `loginPath`, `callbackPath`,
|
|
175
|
+
`logoutPath`, `postLogoutRedirect`, `loginErrorRedirect`, `protect`, `hook`,
|
|
176
|
+
`resolveUser`, `principalKey`, `meRoute`, `meResponse`,
|
|
177
|
+
`redirectUnauthenticated`, `errorBody`, `onAuthFailure`, `onLogin`, `onLogout`,
|
|
178
|
+
`isSessionRevoked`.
|
|
179
|
+
|
|
180
|
+
## Gateway headers
|
|
181
|
+
|
|
182
|
+
The original integration, for services behind a gateway that authenticates on
|
|
183
|
+
their behalf.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { relyperAuth } from '@relyper/sp-auth/fastify';
|
|
187
|
+
|
|
188
|
+
await app.register(relyperAuth, {
|
|
189
|
+
requiredRole: 'my_service_user',
|
|
190
|
+
protect: (request) => request.url.startsWith('/api/'),
|
|
191
|
+
meRoute: '/api/me',
|
|
192
|
+
resolveUser
|
|
193
|
+
});
|
|
194
|
+
```
|
|
108
195
|
|
|
109
196
|
| Header | Meaning |
|
|
110
197
|
| --- | --- |
|
|
@@ -116,30 +203,90 @@ Fastify adapter additions: `protect`, `hook`, `resolveUser`, `principalKey`,
|
|
|
116
203
|
Fallbacks when `acceptForwardedHeaders` is on: `x-forwarded-user`,
|
|
117
204
|
`x-forwarded-email`, `x-forwarded-preferred-username`, `x-forwarded-groups`.
|
|
118
205
|
|
|
119
|
-
|
|
206
|
+
Options: `requiredRole`, `roleMatch`, `requireEmail`, `headerNames`,
|
|
207
|
+
`acceptForwardedHeaders`, `devAuth`, `unauthenticatedStatus`, `forbiddenStatus`,
|
|
208
|
+
`message`, `parseRoles`. Fastify additions: `protect`, `hook`, `resolveUser`,
|
|
209
|
+
`principalKey`, `meRoute`, `meResponse`, `errorBody`, `onAuthFailure`,
|
|
210
|
+
`warnOnDevAuth`.
|
|
120
211
|
|
|
121
|
-
|
|
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.
|
|
212
|
+
The core is a pure function over headers — no framework, no I/O, never throws:
|
|
124
213
|
|
|
125
|
-
|
|
126
|
-
|
|
214
|
+
```ts
|
|
215
|
+
import { createRelyperAuth } from '@relyper/sp-auth';
|
|
216
|
+
|
|
217
|
+
const auth = createRelyperAuth({ requiredRole: 'my_service_user' });
|
|
218
|
+
const result = auth.authenticate(request.headers);
|
|
219
|
+
if (!result.ok) return reply.code(result.status).send({ error: result.message });
|
|
220
|
+
result.identity.subject;
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Security model — read this
|
|
224
|
+
|
|
225
|
+
This path trusts headers. It verifies no token and no signature. It is only safe
|
|
226
|
+
when the service is unreachable except through a gateway that **overwrites**
|
|
227
|
+
client-supplied `x-relyper-*` headers rather than passing them through.
|
|
228
|
+
|
|
229
|
+
If the service can be reached directly, anyone can send
|
|
230
|
+
`x-relyper-roles: my_service_user` and be admitted. `acceptForwardedHeaders`
|
|
231
|
+
widens that surface further, because `x-forwarded-*` is what any generic proxy
|
|
232
|
+
sets — which is why it is off by default.
|
|
233
|
+
|
|
234
|
+
Prefer the OIDC login. It needs no such assumption about the network.
|
|
235
|
+
|
|
236
|
+
## Relyper Coins
|
|
237
|
+
|
|
238
|
+
Relyper Coins are held centrally: the wallet at the identity provider is the
|
|
239
|
+
single source of truth for what a user can spend across every Relyper product.
|
|
240
|
+
A service provider never keeps its own balance.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { createRelyperCoinsClient, coinsBaseUrlFromIssuer, RelyperCoinsError } from '@relyper/sp-auth/coins';
|
|
244
|
+
|
|
245
|
+
const coins = createRelyperCoinsClient({
|
|
246
|
+
// The IdP publishes OIDC at the issuer root but mounts its API under /api.
|
|
247
|
+
baseUrl: coinsBaseUrlFromIssuer(process.env.RELYPER_OIDC_ISSUER),
|
|
248
|
+
clientId: process.env.RELYPER_OIDC_CLIENT_ID,
|
|
249
|
+
clientSecret: process.env.RELYPER_OIDC_CLIENT_SECRET
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const wallet = await coins.getWallet(identity.subject);
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
await coins.debit({
|
|
256
|
+
subject: identity.subject,
|
|
257
|
+
amountRc: 5,
|
|
258
|
+
reason: 'assistant.question',
|
|
259
|
+
idempotencyKey: 'my-app:' + requestHash, // a retry must not charge twice
|
|
260
|
+
product: 'my-app',
|
|
261
|
+
provider: 'openai',
|
|
262
|
+
model: 'gpt-x'
|
|
263
|
+
});
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error instanceof RelyperCoinsError && error.code === 'insufficient_funds') {
|
|
266
|
+
// error.balanceRc / error.requestedRc
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
127
270
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
271
|
+
The same client ID and secret as the OIDC login, so the IdP can attribute every
|
|
272
|
+
debit to one registered app and an operator can allow or revoke coin spending
|
|
273
|
+
per app. The app has to be ticked as **May consume Relyper Coins** in the IdP
|
|
274
|
+
admin UI, or `debit` throws with code `coins_not_enabled`.
|
|
131
275
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
276
|
+
`debit` refuses to overdraw a wallet. Because the exact cost of a request is
|
|
277
|
+
usually only known after the work is done, check the balance against an estimate
|
|
278
|
+
first and debit the real amount afterwards.
|
|
135
279
|
|
|
136
280
|
## Design
|
|
137
281
|
|
|
138
|
-
-
|
|
282
|
+
- The OIDC client does no I/O you did not ask for: discovery and JWKS are
|
|
283
|
+
fetched lazily, cached, and go through your `fetch` if you supply one.
|
|
139
284
|
- Identity and application user stay separate. The package hands you a
|
|
140
285
|
`RelyperIdentity`; `resolveUser` maps it to your own record. That boundary is
|
|
141
286
|
what makes the package reusable across service providers.
|
|
142
|
-
- `subject` is the IdP ID and never the primary key of your database.
|
|
287
|
+
- `subject` is the IdP's ID and never the primary key of your database.
|
|
288
|
+
- Errors carry a user-safe `message` and a separate `detail` for logs, so an
|
|
289
|
+
IdP's diagnostics never leak into a response.
|
|
143
290
|
|
|
144
291
|
## License
|
|
145
292
|
|
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
|
-
/**
|
|
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 };
|
package/dist/client.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/coins.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
/**
|
|
67
|
+
* Refused with 403 without saying it was the coin permission. Usually the
|
|
68
|
+
* request never reached the coins service: a wrong base URL, a gateway or a
|
|
69
|
+
* WAF. Read `url` and `responseBody` on the error.
|
|
70
|
+
*/
|
|
71
|
+
| 'forbidden'
|
|
72
|
+
/** The request was malformed. */
|
|
73
|
+
| 'invalid_request'
|
|
74
|
+
/** The identity provider was unreachable or answered unexpectedly. */
|
|
75
|
+
| 'unavailable';
|
|
76
|
+
export declare class RelyperCoinsError extends Error {
|
|
77
|
+
readonly code: RelyperCoinsErrorCode;
|
|
78
|
+
readonly status: number;
|
|
79
|
+
/** Present on insufficient_funds. */
|
|
80
|
+
readonly balanceRc?: number;
|
|
81
|
+
readonly requestedRc?: number;
|
|
82
|
+
readonly cause?: unknown;
|
|
83
|
+
/**
|
|
84
|
+
* What the service actually answered, and where it was asked.
|
|
85
|
+
*
|
|
86
|
+
* Kept because a status code alone does not identify a cause: a 403 from the
|
|
87
|
+
* coins service and a 403 from a proxy in front of it are the same number and
|
|
88
|
+
* mean entirely different things. Without the body, an operator holding a
|
|
89
|
+
* correctly configured client has nothing to go on but a message this library
|
|
90
|
+
* guessed. `responseError` is the service's own error code when it sent one.
|
|
91
|
+
*/
|
|
92
|
+
readonly url?: string;
|
|
93
|
+
readonly responseError?: string;
|
|
94
|
+
readonly responseBody?: string;
|
|
95
|
+
constructor(code: RelyperCoinsErrorCode, message: string, options?: {
|
|
96
|
+
status?: number;
|
|
97
|
+
balanceRc?: number;
|
|
98
|
+
requestedRc?: number;
|
|
99
|
+
cause?: unknown;
|
|
100
|
+
url?: string;
|
|
101
|
+
responseError?: string;
|
|
102
|
+
responseBody?: string;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export type RelyperCoinsOptions = {
|
|
106
|
+
/**
|
|
107
|
+
* Base URL of the identity provider's API, including the path prefix its
|
|
108
|
+
* routes are mounted under. For Relyper that is the issuer plus `/api`, e.g.
|
|
109
|
+
* `https://api.relyper.de/api` for issuer `https://api.relyper.de`.
|
|
110
|
+
*/
|
|
111
|
+
baseUrl: string;
|
|
112
|
+
/** Same credentials as the OIDC login. */
|
|
113
|
+
clientId: string;
|
|
114
|
+
clientSecret: string;
|
|
115
|
+
/** Default: 10000. */
|
|
116
|
+
requestTimeoutMs?: number;
|
|
117
|
+
fetch?: typeof globalThis.fetch;
|
|
118
|
+
};
|
|
119
|
+
export type RelyperCoinsClient = {
|
|
120
|
+
/** Balance and plan allowance of one user's wallet. */
|
|
121
|
+
getWallet(subject: string): Promise<RelyperCoinWallet>;
|
|
122
|
+
/** Recent ledger entries for one user. */
|
|
123
|
+
getLedger(subject: string, limit?: number): Promise<RelyperCoinLedgerEntry[]>;
|
|
124
|
+
/**
|
|
125
|
+
* Spends coins. Throws {@link RelyperCoinsError} with code `insufficient_funds`
|
|
126
|
+
* when the wallet cannot cover the amount -- the debit is refused, not
|
|
127
|
+
* overdrawn.
|
|
128
|
+
*/
|
|
129
|
+
debit(input: RelyperCoinDebit): Promise<RelyperCoinDebitResult>;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Derives the coins API base URL from an OIDC issuer.
|
|
133
|
+
*
|
|
134
|
+
* The Relyper IdP publishes OIDC at the issuer root but mounts its regular API
|
|
135
|
+
* under `/api`, so the two differ by exactly that segment.
|
|
136
|
+
*/
|
|
137
|
+
export declare function coinsBaseUrlFromIssuer(issuer: string): string;
|
|
138
|
+
export declare function createRelyperCoinsClient(options: RelyperCoinsOptions): RelyperCoinsClient;
|
|
139
|
+
//# sourceMappingURL=coins.d.ts.map
|