@eetr/eetr-auth-client 0.2.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 ADDED
@@ -0,0 +1,350 @@
1
+ # @eetr/eetr-auth-client
2
+
3
+ TypeScript client library for the [eetr-auth](https://github.com/eetr-ai/eetr-auth) OAuth 2.1 / OIDC server.
4
+
5
+ It wraps the server's token, introspection, UserInfo, admin, and passkey-management
6
+ endpoints, plus helpers for OIDC discovery and JWT verification. Everything is
7
+ `fetch`-based and ships with full type definitions.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @eetr/eetr-auth-client
13
+ ```
14
+
15
+ ```bash
16
+ pnpm add @eetr/eetr-auth-client
17
+ # or
18
+ yarn add @eetr/eetr-auth-client
19
+ ```
20
+
21
+ **Requirements:** Node.js 18+ (the library relies on the global `fetch`; `decodeJwtPayload`
22
+ also uses Node's `Buffer`). `jose` is a runtime dependency used for JWT verification.
23
+
24
+ The package is ESM-only (`"type": "module"`) and exports both JavaScript and `.d.ts` types.
25
+
26
+ ## Quick start
27
+
28
+ ```ts
29
+ import {
30
+ fetchOIDCDiscovery,
31
+ exchangeToken,
32
+ validateJwt,
33
+ getUserInfo,
34
+ } from "@eetr/eetr-auth-client";
35
+
36
+ const ISSUER = "https://auth.example.com";
37
+
38
+ // 1. Discover endpoints
39
+ const discovery = await fetchOIDCDiscovery(ISSUER);
40
+
41
+ // 2. Exchange an authorization code for tokens (PKCE)
42
+ const tokens = await exchangeToken(
43
+ {
44
+ grantType: "authorization_code",
45
+ clientId: "my-client",
46
+ code: authorizationCode,
47
+ redirectUri: "https://app.example.com/callback",
48
+ codeVerifier,
49
+ },
50
+ { tokenEndpoint: discovery.token_endpoint }
51
+ );
52
+
53
+ // 3. Verify the access/ID token against the server's JWKS
54
+ const payload = await validateJwt(tokens.access_token, discovery.jwks_uri, {
55
+ issuer: ISSUER,
56
+ audience: "my-client",
57
+ });
58
+
59
+ // 4. Fetch the user's profile
60
+ const user = await getUserInfo(tokens.access_token, discovery.userinfo_endpoint);
61
+ ```
62
+
63
+ ## API reference
64
+
65
+ ### Discovery
66
+
67
+ ```ts
68
+ fetchOIDCDiscovery(issuerUrl: string): Promise<OIDCDiscovery>
69
+ fetchOAuthMetadata(issuerUrl: string): Promise<OAuthServerMetadata>
70
+ ```
71
+
72
+ Fetch the server's `/.well-known/openid-configuration` or
73
+ `/.well-known/oauth-authorization-server` metadata. Use the returned
74
+ `token_endpoint`, `jwks_uri`, `userinfo_endpoint`, etc. to configure the rest of
75
+ the client rather than hard-coding paths.
76
+
77
+ ### Authorization URL
78
+
79
+ ```ts
80
+ buildAuthorizationUrl(authorizationEndpoint: string, params: AuthorizationUrlParams): string
81
+ ```
82
+
83
+ Builds the authorization-request URL for the `authorization_code` + PKCE flow.
84
+ `code_challenge` is required (the server only accepts `S256`). Pass `nonce` to
85
+ request an OIDC `nonce` that the server binds into the issued `id_token`, then
86
+ verify it on the returned token with [`validateIdToken`](#jwt-verification).
87
+
88
+ ```ts
89
+ const url = buildAuthorizationUrl(discovery.authorization_endpoint, {
90
+ clientId: "my-client",
91
+ redirectUri: "https://app.example.com/callback",
92
+ codeChallenge, // base64url SHA-256 of your PKCE verifier
93
+ scope: "openid profile email",
94
+ state,
95
+ nonce,
96
+ });
97
+ ```
98
+
99
+ Prefer the `OIDCScope` constants and the `scopes` array so a typo can't silently
100
+ drop `openid` (which would make `/userinfo` return `403 insufficient_scope`). `scope`
101
+ and `scopes` are merged and de-duplicated, so either or both work:
102
+
103
+ ```ts
104
+ import { OIDCScope, STANDARD_OIDC_SCOPES } from "@eetr/eetr-auth-client";
105
+
106
+ const url = buildAuthorizationUrl(discovery.authorization_endpoint, {
107
+ clientId: "my-client",
108
+ redirectUri: "https://app.example.com/callback",
109
+ codeChallenge,
110
+ scopes: [OIDCScope.OpenId, OIDCScope.Profile, OIDCScope.Email], // or STANDARD_OIDC_SCOPES
111
+ state,
112
+ nonce,
113
+ });
114
+ ```
115
+
116
+ > The client must be **granted** these scopes by an admin, and `openid`/`profile`/`email`
117
+ > are seeded on the server. Requesting a scope the client wasn't granted fails with
118
+ > `invalid_scope`.
119
+
120
+ ### Token exchange
121
+
122
+ ```ts
123
+ exchangeToken(params: ExchangeTokenParams, config: ExchangeTokenConfig): Promise<TokenResponse>
124
+ ```
125
+
126
+ Performs an OAuth token request. `grantType` is one of `"authorization_code"`,
127
+ `"client_credentials"`, or `"refresh_token"`; supply the fields relevant to the grant.
128
+ When the `openid` scope was granted on an `authorization_code` exchange, the
129
+ `TokenResponse` also includes a signed `id_token`.
130
+
131
+ ```ts
132
+ // Client credentials (machine-to-machine)
133
+ const tokens = await exchangeToken(
134
+ {
135
+ grantType: "client_credentials",
136
+ clientId: "service-a",
137
+ clientSecret: process.env.CLIENT_SECRET,
138
+ scope: "admin",
139
+ },
140
+ { tokenEndpoint: discovery.token_endpoint }
141
+ );
142
+ ```
143
+
144
+ On a non-2xx response it throws an [`OAuthError`](#error-handling) carrying the
145
+ server's `error` code and `error_description`.
146
+
147
+ ### TokenManager
148
+
149
+ A small helper that caches an access token and transparently refreshes it (using a
150
+ 30-second expiry skew) when a refresh token is available.
151
+
152
+ ```ts
153
+ import { TokenManager } from "@eetr/eetr-auth-client";
154
+
155
+ const manager = new TokenManager({
156
+ issuerUrl: ISSUER,
157
+ clientId: "my-client",
158
+ clientSecret: process.env.CLIENT_SECRET, // optional for public clients
159
+ tokenEndpoint: discovery.token_endpoint,
160
+ });
161
+
162
+ manager.setTokens(tokens); // seed from an initial exchange
163
+ const accessToken = await manager.getAccessToken(); // refreshes if expired
164
+ ```
165
+
166
+ `getAccessToken()` throws an `OAuthError` with code `no_token` if there is no valid
167
+ token and no refresh token to fall back on.
168
+
169
+ ### JWT verification
170
+
171
+ ```ts
172
+ validateJwt(token: string, jwksUri: string, options?: ValidateJwtOptions): Promise<JWTPayload>
173
+ validateIdToken(token: string, jwksUri: string, options?: ValidateIdTokenOptions): Promise<IDTokenClaims>
174
+ decodeJwtPayload(token: string): JWTPayload
175
+ ```
176
+
177
+ `validateJwt` verifies the signature against the server's JWKS (remote keys are
178
+ cached per `jwksUri`) and validates `issuer`/`audience`/expiry (`clockTolerance`
179
+ defaults to 5 seconds). `decodeJwtPayload` decodes the payload **without** verifying
180
+ the signature — use it only for inspecting claims you have already verified.
181
+
182
+ ```ts
183
+ const payload = await validateJwt(accessToken, discovery.jwks_uri, {
184
+ issuer: ISSUER,
185
+ audience: "my-client",
186
+ clockTolerance: 10,
187
+ });
188
+ ```
189
+
190
+ `validateIdToken` verifies an OIDC `id_token` the same way and, when you pass the
191
+ `nonce` you sent to the authorization endpoint, additionally checks the token's
192
+ `nonce` claim — throwing `id_token nonce mismatch` on a mismatch. It returns the
193
+ typed `IDTokenClaims` (`sub`, `auth_time`, `nonce`, `at_hash`, plus scope-gated
194
+ `name`/`preferred_username`/`picture`/`email`/`email_verified`).
195
+
196
+ ```ts
197
+ const claims = await validateIdToken(tokens.id_token!, discovery.jwks_uri, {
198
+ issuer: ISSUER,
199
+ audience: "my-client",
200
+ nonce, // the value passed to buildAuthorizationUrl
201
+ });
202
+ ```
203
+
204
+ ### Token introspection
205
+
206
+ ```ts
207
+ introspectToken(params: IntrospectTokenParams, config: IntrospectTokenConfig): Promise<TokenValidationResponse>
208
+ ```
209
+
210
+ Asks the server whether a token is active within a given environment. The endpoint
211
+ is published as `token_introspection_endpoint` in the OAuth metadata (defaults to
212
+ `${ISSUER}/api/token/validate`).
213
+
214
+ ```ts
215
+ const metadata = await fetchOAuthMetadata(ISSUER);
216
+
217
+ const result = await introspectToken(
218
+ { token: accessToken, scopes: ["read"], environmentName: "production" },
219
+ { introspectionEndpoint: metadata.token_introspection_endpoint! }
220
+ );
221
+ // → { valid, active, client_id, expires_at }
222
+ ```
223
+
224
+ ### UserInfo
225
+
226
+ ```ts
227
+ getUserInfo(accessToken: string, userInfoEndpoint: string): Promise<UserInfoResponse>
228
+ ```
229
+
230
+ Returns the OIDC UserInfo claims for the bearer token. The endpoint **requires the
231
+ `openid` scope**; only `sub` is always present, and the remaining claims are gated by
232
+ scope (`profile` → `name`/`preferred_username`/`picture`, `email` →
233
+ `email`/`email_verified`).
234
+
235
+ On failure it throws an [`OAuthError`](#error-handling). A token that is valid but
236
+ lacks `openid` yields a `403`, surfaced as `code === "insufficient_scope"` (use
237
+ `err.isInsufficientScope`) so you can re-authorize with the right scopes rather than
238
+ discarding the token:
239
+
240
+ ```ts
241
+ try {
242
+ const info = await getUserInfo(accessToken, discovery.userinfo_endpoint);
243
+ } catch (err) {
244
+ if (err instanceof OAuthError && err.isInsufficientScope) {
245
+ // token is fine but missing `openid` — restart the dance requesting it
246
+ }
247
+ }
248
+ ```
249
+
250
+ #### Normalized profile
251
+
252
+ ```ts
253
+ toUserProfile(userInfo: UserInfoResponse, idTokenClaims?: IDTokenClaims): UserProfile
254
+ ```
255
+
256
+ Merges the UserInfo response (and optionally the decoded `id_token` claims) into a
257
+ single camelCased `UserProfile` (`sub`, `name`, `preferredUsername`, `picture`,
258
+ `email`, `emailVerified`). UserInfo wins; the id_token fills any gap.
259
+
260
+ ```ts
261
+ const info = await getUserInfo(tokens.access_token, discovery.userinfo_endpoint);
262
+ const profile = toUserProfile(info, decodeJwtPayload(tokens.id_token!));
263
+ // → { sub, name?, preferredUsername?, picture?, email?, emailVerified? }
264
+ ```
265
+
266
+ ### Admin API
267
+
268
+ User management against the server's admin API. Requires an access token from a
269
+ client configured as an **admin API client** on the server.
270
+
271
+ ```ts
272
+ import {
273
+ getAdminUser,
274
+ createAdminUser,
275
+ updateAdminUser,
276
+ deleteAdminUser,
277
+ } from "@eetr/eetr-auth-client";
278
+
279
+ const config = { baseUrl: ISSUER, accessToken }; // AdminClientConfig
280
+
281
+ const created = await createAdminUser(
282
+ { username: "alice", password: "•••", email: "alice@example.com" },
283
+ config
284
+ );
285
+
286
+ await updateAdminUser("alice", { name: "Alice B." }, config);
287
+ const user = await getAdminUser("alice", config); // by username or UUID
288
+ await deleteAdminUser(created.id, config);
289
+ ```
290
+
291
+ All admin calls throw `OAuthError` on non-2xx responses.
292
+
293
+ ### Passkey management
294
+
295
+ List, rename, and remove a user's passkeys. The access token must belong to the
296
+ user whose passkeys are being managed.
297
+
298
+ ```ts
299
+ import { listPasskeys, renamePasskey, removePasskey } from "@eetr/eetr-auth-client";
300
+
301
+ const config = { baseUrl: ISSUER, accessToken }; // UserClientConfig
302
+
303
+ const passkeys = await listPasskeys(config);
304
+ await renamePasskey(passkeys[0].id, "Work laptop", config);
305
+ await removePasskey(passkeys[0].id, config);
306
+ ```
307
+
308
+ > Only passkey *management* is available here. Creating or authenticating with a
309
+ > passkey is a WebAuthn ceremony that requires a browser and cannot be driven from a
310
+ > server-side client. `removePasskey` deletes the server-side record only — it does
311
+ > not remove the credential from the device/authenticator.
312
+
313
+ ## Error handling
314
+
315
+ API helpers throw `OAuthError` (a subclass of `Error`) on non-2xx responses, exposing
316
+ the server's machine-readable `code` alongside the message:
317
+
318
+ ```ts
319
+ import { OAuthError } from "@eetr/eetr-auth-client";
320
+
321
+ try {
322
+ await exchangeToken(params, config);
323
+ } catch (err) {
324
+ if (err instanceof OAuthError) {
325
+ console.error(err.code, err.message); // e.g. "invalid_grant"
326
+ }
327
+ }
328
+ ```
329
+
330
+ `OAuthError` also exposes the HTTP `status` and the server's `description`
331
+ (`error_description`) when available, plus an `isInsufficientScope` convenience for
332
+ the `/userinfo` 403 case (token valid but missing `openid`).
333
+
334
+ The discovery helpers throw a plain `Error` with the HTTP status on failure.
335
+
336
+ ## Types
337
+
338
+ The package exports TypeScript types for every request and response shape, including
339
+ `TokenResponse`, `UserInfoResponse`, `UserProfile`, `IDTokenClaims`, `OIDCDiscovery`,
340
+ `OAuthServerMetadata`, `AuthClientConfig`, `JWTPayload`, `TokenValidationResponse`,
341
+ `GrantType`, `ExchangeTokenParams`/`Config`, `AuthorizationUrlParams`,
342
+ `IntrospectTokenParams`/`Config`, `OIDCScopeValue`, `ValidateJwtOptions`,
343
+ `ValidateIdTokenOptions`, `AdminUserRecord`, `AdminClientConfig`, `CreateUserParams`,
344
+ `UpdateUserParams`, `PasskeySummary`, and `UserClientConfig`. It also exports the
345
+ `OIDCScope` / `STANDARD_OIDC_SCOPES` constants, the `resolveScopeParam` helper, and
346
+ the `toUserProfile` profile normalizer.
347
+
348
+ ## License
349
+
350
+ See the [eetr-auth repository](https://github.com/eetr-ai/eetr-auth).
package/dist/api.d.ts CHANGED
@@ -1,14 +1,54 @@
1
1
  import type { TokenResponse, UserInfoResponse, TokenValidationResponse } from "./types.js";
2
2
  export declare class OAuthError extends Error {
3
3
  readonly code: string;
4
- constructor(code: string, message: string);
4
+ /** HTTP status of the failed response, when the error came from one. */
5
+ readonly status?: number | undefined;
6
+ /** The server's `error_description`, when present. */
7
+ readonly description?: string | undefined;
8
+ constructor(code: string, message: string,
9
+ /** HTTP status of the failed response, when the error came from one. */
10
+ status?: number | undefined,
11
+ /** The server's `error_description`, when present. */
12
+ description?: string | undefined);
13
+ /**
14
+ * True when the access token was valid but lacked a required scope (HTTP 403
15
+ * `insufficient_scope`) — e.g. calling `/userinfo` with a token that was not
16
+ * granted `openid`. Re-authorize requesting the missing scope rather than
17
+ * treating the token as invalid.
18
+ */
19
+ get isInsufficientScope(): boolean;
5
20
  }
6
21
  export type GrantType = "authorization_code" | "client_credentials" | "refresh_token";
22
+ export interface AuthorizationUrlParams {
23
+ clientId: string;
24
+ redirectUri: string;
25
+ /** PKCE S256 challenge (required by this server). */
26
+ codeChallenge: string;
27
+ /** Space-delimited scope string. Merged with `scopes` if both are given. */
28
+ scope?: string;
29
+ /**
30
+ * Scopes as an array (e.g. `[OIDCScope.OpenId, OIDCScope.Profile]`). Merged
31
+ * with `scope`, de-duplicated. Prefer this with the `OIDCScope` constants so
32
+ * `openid` can't be dropped by a typo.
33
+ */
34
+ scopes?: readonly string[];
35
+ state?: string;
36
+ /** OIDC nonce, bound into the issued id_token for replay protection. */
37
+ nonce?: string;
38
+ }
39
+ /**
40
+ * Build an authorization-request URL for the authorization_code + PKCE flow.
41
+ * Includes the OIDC `nonce` when provided so it is bound into the id_token.
42
+ */
43
+ export declare function buildAuthorizationUrl(authorizationEndpoint: string, params: AuthorizationUrlParams): string;
7
44
  export interface ExchangeTokenParams {
8
45
  grantType: GrantType;
9
46
  clientId: string;
10
47
  clientSecret?: string;
48
+ /** Space-delimited scope string. Merged with `scopes` if both are given. */
11
49
  scope?: string;
50
+ /** Scopes as an array (e.g. with the `OIDCScope` constants). Merged with `scope`. */
51
+ scopes?: readonly string[];
12
52
  code?: string;
13
53
  redirectUri?: string;
14
54
  codeVerifier?: string;
@@ -22,6 +62,13 @@ export interface IntrospectTokenParams {
22
62
  token: string;
23
63
  scopes?: string[];
24
64
  environmentName: string;
65
+ /**
66
+ * Optional audience binding. When set to the calling resource server's own
67
+ * client_id, the token is only reported valid if it was issued to that client —
68
+ * preventing a token minted for a sibling client in the same environment from
69
+ * being accepted here. Omit to keep the previous (audience-agnostic) behavior.
70
+ */
71
+ clientId?: string;
25
72
  }
26
73
  export interface IntrospectTokenConfig {
27
74
  introspectionEndpoint: string;
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAE3F,qBAAa,UAAW,SAAQ,KAAK;aAEjB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,MAAM,SAAS,GAAG,oBAAoB,GAAG,oBAAoB,GAAG,eAAe,CAAC;AAEtF,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,aAAa,CAAC,CA2BxB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,EAC7B,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,uBAAuB,CAAC,CAWlC;AAED,wBAAsB,WAAW,CAC/B,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAY3B"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAG3F,qBAAa,UAAW,SAAQ,KAAK;aAEjB,IAAI,EAAE,MAAM;IAE5B,wEAAwE;aACxD,MAAM,CAAC,EAAE,MAAM;IAC/B,sDAAsD;aACtC,WAAW,CAAC,EAAE,MAAM;gBALpB,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM;IACf,wEAAwE;IACxD,MAAM,CAAC,EAAE,MAAM,YAAA;IAC/B,sDAAsD;IACtC,WAAW,CAAC,EAAE,MAAM,YAAA;IAMtC;;;;;OAKG;IACH,IAAI,mBAAmB,IAAI,OAAO,CAEjC;CACF;AAED,MAAM,MAAM,SAAS,GAAG,oBAAoB,GAAG,oBAAoB,GAAG,eAAe,CAAC;AAEtF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,qBAAqB,EAAE,MAAM,EAC7B,MAAM,EAAE,sBAAsB,GAC7B,MAAM,CAYR;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,aAAa,CAAC,CA4BxB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,EAC7B,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,uBAAuB,CAAC,CAYlC;AAED,wBAAsB,WAAW,CAC/B,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAuB3B"}
package/dist/api.js CHANGED
@@ -1,10 +1,48 @@
1
+ import { resolveScopeParam } from "./scopes.js";
1
2
  export class OAuthError extends Error {
2
3
  code;
3
- constructor(code, message) {
4
+ status;
5
+ description;
6
+ constructor(code, message,
7
+ /** HTTP status of the failed response, when the error came from one. */
8
+ status,
9
+ /** The server's `error_description`, when present. */
10
+ description) {
4
11
  super(message);
5
12
  this.code = code;
13
+ this.status = status;
14
+ this.description = description;
6
15
  this.name = "OAuthError";
7
16
  }
17
+ /**
18
+ * True when the access token was valid but lacked a required scope (HTTP 403
19
+ * `insufficient_scope`) — e.g. calling `/userinfo` with a token that was not
20
+ * granted `openid`. Re-authorize requesting the missing scope rather than
21
+ * treating the token as invalid.
22
+ */
23
+ get isInsufficientScope() {
24
+ return this.code === "insufficient_scope";
25
+ }
26
+ }
27
+ /**
28
+ * Build an authorization-request URL for the authorization_code + PKCE flow.
29
+ * Includes the OIDC `nonce` when provided so it is bound into the id_token.
30
+ */
31
+ export function buildAuthorizationUrl(authorizationEndpoint, params) {
32
+ const url = new URL(authorizationEndpoint);
33
+ url.searchParams.set("response_type", "code");
34
+ url.searchParams.set("client_id", params.clientId);
35
+ url.searchParams.set("redirect_uri", params.redirectUri);
36
+ url.searchParams.set("code_challenge", params.codeChallenge);
37
+ url.searchParams.set("code_challenge_method", "S256");
38
+ const scope = resolveScopeParam(params.scope, params.scopes);
39
+ if (scope)
40
+ url.searchParams.set("scope", scope);
41
+ if (params.state)
42
+ url.searchParams.set("state", params.state);
43
+ if (params.nonce)
44
+ url.searchParams.set("nonce", params.nonce);
45
+ return url.toString();
8
46
  }
9
47
  export async function exchangeToken(params, config) {
10
48
  const body = new URLSearchParams();
@@ -12,8 +50,9 @@ export async function exchangeToken(params, config) {
12
50
  body.set("client_id", params.clientId);
13
51
  if (params.clientSecret)
14
52
  body.set("client_secret", params.clientSecret);
15
- if (params.scope)
16
- body.set("scope", params.scope);
53
+ const scope = resolveScopeParam(params.scope, params.scopes);
54
+ if (scope)
55
+ body.set("scope", scope);
17
56
  if (params.code)
18
57
  body.set("code", params.code);
19
58
  if (params.redirectUri)
@@ -41,6 +80,7 @@ export async function introspectToken(params, config) {
41
80
  token: params.token,
42
81
  scopes: params.scopes ?? [],
43
82
  environmentName: params.environmentName,
83
+ ...(params.clientId ? { clientId: params.clientId } : {}),
44
84
  }),
45
85
  });
46
86
  return res.json();
@@ -51,7 +91,16 @@ export async function getUserInfo(accessToken, userInfoEndpoint) {
51
91
  });
52
92
  if (!res.ok) {
53
93
  const data = (await res.json().catch(() => ({})));
54
- throw new OAuthError(data.error ?? "invalid_token", `UserInfo request failed: ${res.status}`);
94
+ // A 403 means the token is valid but missing a required scope (`openid`).
95
+ // Surface it as `insufficient_scope` so callers can re-authorize with the
96
+ // right scopes instead of discarding the token as invalid. A 401 (or a
97
+ // non-JSON body) stays `invalid_token`.
98
+ const code = data.error ?? (res.status === 403 ? "insufficient_scope" : "invalid_token");
99
+ const message = data.error_description ??
100
+ (code === "insufficient_scope"
101
+ ? "UserInfo requires the openid scope on the access token."
102
+ : `UserInfo request failed: ${res.status}`);
103
+ throw new OAuthError(code, message, res.status, data.error_description);
55
104
  }
56
105
  return res.json();
57
106
  }
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,10 +1,15 @@
1
- export type { TokenResponse, UserInfoResponse, OIDCDiscovery, OAuthServerMetadata, AuthClientConfig, JWTPayload, TokenValidationResponse, } from "./types.js";
1
+ export type { TokenResponse, UserInfoResponse, UserProfile, OIDCDiscovery, OAuthServerMetadata, AuthClientConfig, JWTPayload, IDTokenClaims, TokenValidationResponse, } from "./types.js";
2
+ export { OIDCScope, STANDARD_OIDC_SCOPES, resolveScopeParam, } from "./scopes.js";
3
+ export type { OIDCScopeValue } from "./scopes.js";
4
+ export { toUserProfile } from "./profile.js";
2
5
  export { fetchOIDCDiscovery, fetchOAuthMetadata } from "./discovery.js";
3
- export { OAuthError, exchangeToken, introspectToken, getUserInfo, } from "./api.js";
4
- export type { GrantType, ExchangeTokenParams, ExchangeTokenConfig, IntrospectTokenParams, IntrospectTokenConfig, } from "./api.js";
6
+ export { OAuthError, exchangeToken, introspectToken, getUserInfo, buildAuthorizationUrl, } from "./api.js";
7
+ export type { GrantType, ExchangeTokenParams, ExchangeTokenConfig, IntrospectTokenParams, IntrospectTokenConfig, AuthorizationUrlParams, } from "./api.js";
5
8
  export { TokenManager } from "./tokens.js";
6
- export { validateJwt, decodeJwtPayload } from "./jwt.js";
7
- export type { ValidateJwtOptions } from "./jwt.js";
9
+ export { validateJwt, validateIdToken, decodeJwtPayload } from "./jwt.js";
10
+ export type { ValidateJwtOptions, ValidateIdTokenOptions } from "./jwt.js";
8
11
  export { getAdminUser, createAdminUser, updateAdminUser, deleteAdminUser, } from "./admin.js";
9
12
  export type { AdminUserRecord, AdminClientConfig, CreateUserParams, UpdateUserParams, } from "./admin.js";
13
+ export { listPasskeys, renamePasskey, removePasskey } from "./passkeys.js";
14
+ export type { PasskeySummary, UserClientConfig } from "./passkeys.js";
10
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,EACV,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAExE,OAAO,EACL,UAAU,EACV,aAAa,EACb,eAAe,EACf,WAAW,GACZ,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACzD,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,OAAO,EACL,YAAY,EACZ,eAAe,EACf,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,EACV,aAAa,EACb,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,SAAS,EACT,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAExE,OAAO,EACL,UAAU,EACV,aAAa,EACb,eAAe,EACf,WAAW,EACX,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC1E,YAAY,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAE3E,OAAO,EACL,YAAY,EACZ,eAAe,EACf,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC3E,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
+ export { OIDCScope, STANDARD_OIDC_SCOPES, resolveScopeParam, } from "./scopes.js";
2
+ export { toUserProfile } from "./profile.js";
1
3
  export { fetchOIDCDiscovery, fetchOAuthMetadata } from "./discovery.js";
2
- export { OAuthError, exchangeToken, introspectToken, getUserInfo, } from "./api.js";
4
+ export { OAuthError, exchangeToken, introspectToken, getUserInfo, buildAuthorizationUrl, } from "./api.js";
3
5
  export { TokenManager } from "./tokens.js";
4
- export { validateJwt, decodeJwtPayload } from "./jwt.js";
6
+ export { validateJwt, validateIdToken, decodeJwtPayload } from "./jwt.js";
5
7
  export { getAdminUser, createAdminUser, updateAdminUser, deleteAdminUser, } from "./admin.js";
8
+ export { listPasskeys, renamePasskey, removePasskey } from "./passkeys.js";
6
9
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAExE,OAAO,EACL,UAAU,EACV,aAAa,EACb,eAAe,EACf,WAAW,GACZ,MAAM,UAAU,CAAC;AASlB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGzD,OAAO,EACL,YAAY,EACZ,eAAe,EACf,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,OAAO,EACL,SAAS,EACT,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAExE,OAAO,EACL,UAAU,EACV,aAAa,EACb,eAAe,EACf,WAAW,EACX,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAUlB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAG1E,OAAO,EACL,YAAY,EACZ,eAAe,EACf,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC;AAQpB,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
package/dist/jwt.d.ts CHANGED
@@ -1,9 +1,19 @@
1
- import type { JWTPayload } from "./types.js";
1
+ import type { IDTokenClaims, JWTPayload } from "./types.js";
2
2
  export interface ValidateJwtOptions {
3
3
  audience?: string | string[];
4
4
  issuer?: string;
5
5
  clockTolerance?: number;
6
6
  }
7
+ export interface ValidateIdTokenOptions extends ValidateJwtOptions {
8
+ /** When set, the id_token's `nonce` claim must match this value (OIDC replay defense). */
9
+ nonce?: string;
10
+ }
7
11
  export declare function validateJwt(token: string, jwksUri: string, options?: ValidateJwtOptions): Promise<JWTPayload>;
12
+ /**
13
+ * Verify an OIDC id_token: checks the RS256 signature against the JWKS plus
14
+ * `iss`/`aud`/`exp` (via {@link validateJwt}), and — when `options.nonce` is
15
+ * supplied — that the token's `nonce` claim matches, defending against replay.
16
+ */
17
+ export declare function validateIdToken(token: string, jwksUri: string, options?: ValidateIdTokenOptions): Promise<IDTokenClaims>;
8
18
  export declare function decodeJwtPayload(token: string): JWTPayload;
9
19
  //# sourceMappingURL=jwt.d.ts.map
package/dist/jwt.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAWD,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAQ1D"}
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAuB,SAAQ,kBAAkB;IAChE,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAWD,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,aAAa,CAAC,CAOxB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAQ1D"}
package/dist/jwt.js CHANGED
@@ -15,6 +15,19 @@ export async function validateJwt(token, jwksUri, options = {}) {
15
15
  });
16
16
  return payload;
17
17
  }
18
+ /**
19
+ * Verify an OIDC id_token: checks the RS256 signature against the JWKS plus
20
+ * `iss`/`aud`/`exp` (via {@link validateJwt}), and — when `options.nonce` is
21
+ * supplied — that the token's `nonce` claim matches, defending against replay.
22
+ */
23
+ export async function validateIdToken(token, jwksUri, options = {}) {
24
+ const { nonce, ...jwtOptions } = options;
25
+ const payload = (await validateJwt(token, jwksUri, jwtOptions));
26
+ if (nonce !== undefined && payload.nonce !== nonce) {
27
+ throw new Error("id_token nonce mismatch");
28
+ }
29
+ return payload;
30
+ }
18
31
  export function decodeJwtPayload(token) {
19
32
  const parts = token.split(".");
20
33
  if (parts.length !== 3) {
package/dist/jwt.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AASrD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAiD,CAAC;AAE3E,SAAS,OAAO,CAAC,OAAe;IAC9B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,kBAAkB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,SAAS,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC;AACjC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,KAAa,EACb,OAAe,EACf,UAA8B,EAAE;IAEhC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE;QAC/C,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,CAAC;KAC5C,CAAC,CAAC;IACH,OAAO,OAAqB,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5D,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAe,CAAC;AACxC,CAAC"}
1
+ {"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAcrD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAiD,CAAC;AAE3E,SAAS,OAAO,CAAC,OAAe;IAC9B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,kBAAkB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,SAAS,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC;AACjC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,KAAa,EACb,OAAe,EACf,UAA8B,EAAE;IAEhC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE;QAC/C,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,CAAC;KAC5C,CAAC,CAAC;IACH,OAAO,OAAqB,CAAC;AAC/B,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,KAAa,EACb,OAAe,EACf,UAAkC,EAAE;IAEpC,MAAM,EAAE,KAAK,EAAE,GAAG,UAAU,EAAE,GAAG,OAAO,CAAC;IACzC,MAAM,OAAO,GAAG,CAAC,MAAM,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,CAAkB,CAAC;IACjF,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5D,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAe,CAAC;AACxC,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Safe, display-only view of a passkey returned by the auth server. Mirrors the
3
+ * server-side `PasskeySummary` — never includes the public key, counter, or raw
4
+ * credential id.
5
+ */
6
+ export interface PasskeySummary {
7
+ id: string;
8
+ name: string | null;
9
+ /** Backed up / synced across devices (e.g. iCloud Keychain, Google Password Manager). */
10
+ synced: boolean;
11
+ /** Single-device credential. */
12
+ deviceBound: boolean;
13
+ createdAt: string;
14
+ lastUsedAt: string | null;
15
+ }
16
+ /**
17
+ * Config for the passkey management endpoints. These are user-scoped: the access
18
+ * token must belong to the user whose passkeys are being managed.
19
+ *
20
+ * Note: only passkey *management* (list/rename/remove) is available here. Creating
21
+ * or authenticating with a passkey is a WebAuthn ceremony that requires a browser
22
+ * and cannot be driven by a server-side client.
23
+ */
24
+ export interface UserClientConfig {
25
+ baseUrl: string;
26
+ accessToken: string;
27
+ }
28
+ /**
29
+ * List the authenticated user's passkeys.
30
+ */
31
+ export declare function listPasskeys(config: UserClientConfig): Promise<PasskeySummary[]>;
32
+ /**
33
+ * Rename one of the authenticated user's passkeys. Throws `OAuthError` with code
34
+ * `not_found` if the passkey does not exist or is not owned by the user.
35
+ */
36
+ export declare function renamePasskey(id: string, name: string, config: UserClientConfig): Promise<void>;
37
+ /**
38
+ * Remove one of the authenticated user's passkeys (server-side record only — this
39
+ * does not delete the credential from the device/authenticator). Throws `OAuthError`
40
+ * with code `not_found` if the passkey does not exist or is not owned by the user.
41
+ */
42
+ export declare function removePasskey(id: string, config: UserClientConfig): Promise<void>;
43
+ //# sourceMappingURL=passkeys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkeys.d.ts","sourceRoot":"","sources":["../src/passkeys.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,yFAAyF;IACzF,MAAM,EAAE,OAAO,CAAC;IAChB,gCAAgC;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAqBD;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAStF;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQvF"}
@@ -0,0 +1,57 @@
1
+ import { OAuthError } from "./api.js";
2
+ function passkeysUrl(baseUrl, id) {
3
+ const trimmed = baseUrl.replace(/\/+$/, "");
4
+ if (id === undefined) {
5
+ return `${trimmed}/api/users/passkey`;
6
+ }
7
+ return `${trimmed}/api/users/passkey/${encodeURIComponent(id)}`;
8
+ }
9
+ async function parseError(res) {
10
+ const data = (await res.json().catch(() => ({})));
11
+ return new OAuthError(data.error ?? "server_error", data.error_description ?? `Passkey API request failed: ${res.status}`);
12
+ }
13
+ /**
14
+ * List the authenticated user's passkeys.
15
+ */
16
+ export async function listPasskeys(config) {
17
+ const res = await fetch(passkeysUrl(config.baseUrl), {
18
+ headers: { Authorization: `Bearer ${config.accessToken}` },
19
+ });
20
+ if (!res.ok) {
21
+ throw await parseError(res);
22
+ }
23
+ const data = (await res.json());
24
+ return data.passkeys;
25
+ }
26
+ /**
27
+ * Rename one of the authenticated user's passkeys. Throws `OAuthError` with code
28
+ * `not_found` if the passkey does not exist or is not owned by the user.
29
+ */
30
+ export async function renamePasskey(id, name, config) {
31
+ const res = await fetch(passkeysUrl(config.baseUrl, id), {
32
+ method: "PATCH",
33
+ headers: {
34
+ Authorization: `Bearer ${config.accessToken}`,
35
+ "Content-Type": "application/json",
36
+ },
37
+ body: JSON.stringify({ name }),
38
+ });
39
+ if (!res.ok) {
40
+ throw await parseError(res);
41
+ }
42
+ }
43
+ /**
44
+ * Remove one of the authenticated user's passkeys (server-side record only — this
45
+ * does not delete the credential from the device/authenticator). Throws `OAuthError`
46
+ * with code `not_found` if the passkey does not exist or is not owned by the user.
47
+ */
48
+ export async function removePasskey(id, config) {
49
+ const res = await fetch(passkeysUrl(config.baseUrl, id), {
50
+ method: "DELETE",
51
+ headers: { Authorization: `Bearer ${config.accessToken}` },
52
+ });
53
+ if (!res.ok) {
54
+ throw await parseError(res);
55
+ }
56
+ }
57
+ //# sourceMappingURL=passkeys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkeys.js","sourceRoot":"","sources":["../src/passkeys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AA+BtC,SAAS,WAAW,CAAC,OAAe,EAAE,EAAW;IAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC5C,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;QACrB,OAAO,GAAG,OAAO,oBAAoB,CAAC;IACxC,CAAC;IACD,OAAO,GAAG,OAAO,sBAAsB,kBAAkB,CAAC,EAAE,CAAC,EAAE,CAAC;AAClE,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,GAAa;IACrC,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAG/C,CAAC;IACF,OAAO,IAAI,UAAU,CACnB,IAAI,CAAC,KAAK,IAAI,cAAc,EAC5B,IAAI,CAAC,iBAAiB,IAAI,+BAA+B,GAAG,CAAC,MAAM,EAAE,CACtE,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAwB;IACzD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;QACnD,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,WAAW,EAAE,EAAE;KAC3D,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAmC,CAAC;IAClE,OAAO,IAAI,CAAC,QAAQ,CAAC;AACvB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,EAAU,EACV,IAAY,EACZ,MAAwB;IAExB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE;QACvD,MAAM,EAAE,OAAO;QACf,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,MAAM,CAAC,WAAW,EAAE;YAC7C,cAAc,EAAE,kBAAkB;SACnC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC;KAC/B,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAU,EAAE,MAAwB;IACtE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE;QACvD,MAAM,EAAE,QAAQ;QAChB,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,CAAC,WAAW,EAAE,EAAE;KAC3D,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { IDTokenClaims, UserInfoResponse, UserProfile } from "./types.js";
2
+ /**
3
+ * Merge `/userinfo` claims (and optionally id_token claims) into a normalized,
4
+ * camelCased {@link UserProfile}. UserInfo values take precedence over id_token
5
+ * values — UserInfo is the authoritative, freshest source — and the id_token
6
+ * fills any gap for claims `/userinfo` did not return.
7
+ *
8
+ * Fields stay optional because the server only returns claims the access token's
9
+ * scopes allow (`profile` → name/preferredUsername/picture, `email` →
10
+ * email/emailVerified), so a profile built from an `openid`-only token carries
11
+ * just `sub`.
12
+ */
13
+ export declare function toUserProfile(userInfo: UserInfoResponse, idTokenClaims?: IDTokenClaims): UserProfile;
14
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../src/profile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,gBAAgB,EAC1B,aAAa,CAAC,EAAE,aAAa,GAC5B,WAAW,CAcb"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Merge `/userinfo` claims (and optionally id_token claims) into a normalized,
3
+ * camelCased {@link UserProfile}. UserInfo values take precedence over id_token
4
+ * values — UserInfo is the authoritative, freshest source — and the id_token
5
+ * fills any gap for claims `/userinfo` did not return.
6
+ *
7
+ * Fields stay optional because the server only returns claims the access token's
8
+ * scopes allow (`profile` → name/preferredUsername/picture, `email` →
9
+ * email/emailVerified), so a profile built from an `openid`-only token carries
10
+ * just `sub`.
11
+ */
12
+ export function toUserProfile(userInfo, idTokenClaims) {
13
+ const prefer = (primary, fallback) => primary !== undefined ? primary : fallback;
14
+ return {
15
+ sub: userInfo.sub ?? idTokenClaims?.sub ?? "",
16
+ name: prefer(userInfo.name, idTokenClaims?.name),
17
+ preferredUsername: prefer(userInfo.preferred_username, idTokenClaims?.preferred_username),
18
+ picture: prefer(userInfo.picture, idTokenClaims?.picture),
19
+ email: prefer(userInfo.email, idTokenClaims?.email),
20
+ emailVerified: prefer(userInfo.email_verified, idTokenClaims?.email_verified),
21
+ };
22
+ }
23
+ //# sourceMappingURL=profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.js","sourceRoot":"","sources":["../src/profile.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAC3B,QAA0B,EAC1B,aAA6B;IAE7B,MAAM,MAAM,GAAG,CAAI,OAAsB,EAAE,QAAuB,EAAiB,EAAE,CACnF,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC7C,OAAO;QACL,GAAG,EAAE,QAAQ,CAAC,GAAG,IAAI,aAAa,EAAE,GAAG,IAAI,EAAE;QAC7C,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC;QAChD,iBAAiB,EAAE,MAAM,CACvB,QAAQ,CAAC,kBAAkB,EAC3B,aAAa,EAAE,kBAAkB,CAClC;QACD,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC;QACzD,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,CAAC;QACnD,aAAa,EAAE,MAAM,CAAC,QAAQ,CAAC,cAAc,EAAE,aAAa,EAAE,cAAc,CAAC;KAC9E,CAAC;AACJ,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Standard OpenID Connect scopes recognized by the eetr-auth server. These are seeded
3
+ * on install; an admin still grants them to each client. Use them when building an
4
+ * authorization request or token exchange so a typo can't silently drop `openid` (which
5
+ * would make `/userinfo` return 403 insufficient_scope).
6
+ */
7
+ export declare const OIDCScope: {
8
+ /** Required to mint an id_token and to call the `/userinfo` endpoint. */
9
+ readonly OpenId: "openid";
10
+ /** Gates the `name` / `preferred_username` / `picture` claims. */
11
+ readonly Profile: "profile";
12
+ /** Gates the `email` / `email_verified` claims. */
13
+ readonly Email: "email";
14
+ };
15
+ export type OIDCScopeValue = (typeof OIDCScope)[keyof typeof OIDCScope];
16
+ /** The standard OIDC scopes, in canonical order: `openid profile email`. */
17
+ export declare const STANDARD_OIDC_SCOPES: readonly OIDCScopeValue[];
18
+ /**
19
+ * Merge a free-form `scope` string and/or a `scopes` array into a single
20
+ * space-delimited scope string, trimmed, de-duplicated, and order-preserving
21
+ * (the `scope` string first, then `scopes`). Returns `undefined` when neither
22
+ * yields any scope, so callers can omit the parameter entirely.
23
+ */
24
+ export declare function resolveScopeParam(scope?: string, scopes?: readonly string[]): string | undefined;
25
+ //# sourceMappingURL=scopes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../src/scopes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,SAAS;IACpB,yEAAyE;;IAEzE,kEAAkE;;IAElE,mDAAmD;;CAE3C,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAExE,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,EAAE,SAAS,cAAc,EAIzD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GACzB,MAAM,GAAG,SAAS,CAapB"}
package/dist/scopes.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Standard OpenID Connect scopes recognized by the eetr-auth server. These are seeded
3
+ * on install; an admin still grants them to each client. Use them when building an
4
+ * authorization request or token exchange so a typo can't silently drop `openid` (which
5
+ * would make `/userinfo` return 403 insufficient_scope).
6
+ */
7
+ export const OIDCScope = {
8
+ /** Required to mint an id_token and to call the `/userinfo` endpoint. */
9
+ OpenId: "openid",
10
+ /** Gates the `name` / `preferred_username` / `picture` claims. */
11
+ Profile: "profile",
12
+ /** Gates the `email` / `email_verified` claims. */
13
+ Email: "email",
14
+ };
15
+ /** The standard OIDC scopes, in canonical order: `openid profile email`. */
16
+ export const STANDARD_OIDC_SCOPES = [
17
+ OIDCScope.OpenId,
18
+ OIDCScope.Profile,
19
+ OIDCScope.Email,
20
+ ];
21
+ /**
22
+ * Merge a free-form `scope` string and/or a `scopes` array into a single
23
+ * space-delimited scope string, trimmed, de-duplicated, and order-preserving
24
+ * (the `scope` string first, then `scopes`). Returns `undefined` when neither
25
+ * yields any scope, so callers can omit the parameter entirely.
26
+ */
27
+ export function resolveScopeParam(scope, scopes) {
28
+ const out = [];
29
+ const seen = new Set();
30
+ const add = (value) => {
31
+ const trimmed = value.trim();
32
+ if (trimmed && !seen.has(trimmed)) {
33
+ seen.add(trimmed);
34
+ out.push(trimmed);
35
+ }
36
+ };
37
+ if (scope)
38
+ scope.split(/\s+/).forEach(add);
39
+ if (scopes)
40
+ scopes.forEach(add);
41
+ return out.length > 0 ? out.join(" ") : undefined;
42
+ }
43
+ //# sourceMappingURL=scopes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scopes.js","sourceRoot":"","sources":["../src/scopes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,yEAAyE;IACzE,MAAM,EAAE,QAAQ;IAChB,kEAAkE;IAClE,OAAO,EAAE,SAAS;IAClB,mDAAmD;IACnD,KAAK,EAAE,OAAO;CACN,CAAC;AAIX,4EAA4E;AAC5E,MAAM,CAAC,MAAM,oBAAoB,GAA8B;IAC7D,SAAS,CAAC,MAAM;IAChB,SAAS,CAAC,OAAO;IACjB,SAAS,CAAC,KAAK;CAChB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAc,EACd,MAA0B;IAE1B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,EAAE;QAC5B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAClC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAClB,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,CAAC;IACH,CAAC,CAAC;IACF,IAAI,KAAK;QAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3C,IAAI,MAAM;QAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACpD,CAAC"}
package/dist/types.d.ts CHANGED
@@ -6,6 +6,12 @@ export interface TokenResponse {
6
6
  scope?: string;
7
7
  id_token?: string;
8
8
  }
9
+ /**
10
+ * UserInfo claims. Only `sub` is always present; the server gates the remaining
11
+ * claims on the access token's granted scopes — `name`/`preferred_username`/`picture`
12
+ * require the `profile` scope and `email`/`email_verified` require the `email` scope.
13
+ * The endpoint itself requires the `openid` scope.
14
+ */
9
15
  export interface UserInfoResponse {
10
16
  sub: string;
11
17
  name?: string;
@@ -14,6 +20,21 @@ export interface UserInfoResponse {
14
20
  picture?: string;
15
21
  preferred_username?: string;
16
22
  }
23
+ /**
24
+ * Normalized user profile, merged from the `/userinfo` response and (optionally) the
25
+ * id_token claims via {@link toUserProfile}. Claim names are camelCased. Every field
26
+ * except `sub` is optional because the server only returns claims the access token's
27
+ * scopes allow (`profile` → name/preferredUsername/picture, `email` →
28
+ * email/emailVerified).
29
+ */
30
+ export interface UserProfile {
31
+ sub: string;
32
+ name?: string;
33
+ preferredUsername?: string;
34
+ picture?: string;
35
+ email?: string;
36
+ emailVerified?: boolean;
37
+ }
17
38
  export interface OIDCDiscovery {
18
39
  issuer: string;
19
40
  authorization_endpoint: string;
@@ -61,4 +82,16 @@ export interface TokenValidationResponse {
61
82
  client_id: string | null;
62
83
  expires_at: string | null;
63
84
  }
85
+ /** Claims an issued OIDC id_token may carry (OpenID Connect Core 1.0). */
86
+ export interface IDTokenClaims extends JWTPayload {
87
+ sub?: string;
88
+ auth_time?: number;
89
+ nonce?: string;
90
+ at_hash?: string;
91
+ name?: string;
92
+ preferred_username?: string;
93
+ picture?: string;
94
+ email?: string;
95
+ email_verified?: boolean;
96
+ }
64
97
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB,EAAE,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,uBAAuB,EAAE,MAAM,EAAE,CAAC;IAClC,qCAAqC,EAAE,MAAM,EAAE,CAAC;IAChD,gCAAgC,EAAE,MAAM,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB,EAAE,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,gCAAgC,EAAE,MAAM,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eetr/eetr-auth-client",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript client library for eetr-auth OAuth 2.1 / OIDC server",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",