@idp.global/sdk 19.2.1 → 19.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.
Files changed (44) hide show
  1. package/changelog.md +58 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts_browser/classes.idpclient.js +1 -1
  4. package/dist_ts_browser/classes.idprequests.js +1 -1
  5. package/dist_ts_browser/classes.relyingpartybearerclient.d.ts +276 -0
  6. package/dist_ts_browser/classes.relyingpartybearerclient.js +591 -0
  7. package/dist_ts_browser/index.d.ts +1 -0
  8. package/dist_ts_browser/index.js +2 -1
  9. package/dist_ts_browser/plugins.js +1 -1
  10. package/dist_ts_browser/secret-delivery.js +1 -1
  11. package/dist_ts_relyingparty/classes.authorityobserver.d.ts +23 -0
  12. package/dist_ts_relyingparty/classes.authorityobserver.js +68 -13
  13. package/dist_ts_relyingparty/classes.errors.d.ts +11 -1
  14. package/dist_ts_relyingparty/classes.errors.js +6 -1
  15. package/dist_ts_relyingparty/classes.relyingpartyroutes.d.ts +74 -2
  16. package/dist_ts_relyingparty/classes.relyingpartyroutes.js +289 -14
  17. package/dist_ts_relyingparty/classes.relyingpartysessions.d.ts +209 -7
  18. package/dist_ts_relyingparty/classes.relyingpartysessions.js +734 -87
  19. package/dist_ts_relyingparty/classes.rpmodels.d.ts +71 -8
  20. package/dist_ts_relyingparty/classes.rpmodels.js +224 -10
  21. package/dist_ts_relyingparty/contract.bearer.d.ts +82 -0
  22. package/dist_ts_relyingparty/contract.bearer.js +9 -0
  23. package/dist_ts_relyingparty/helpers.bearer.d.ts +48 -0
  24. package/dist_ts_relyingparty/helpers.bearer.js +199 -0
  25. package/dist_ts_relyingparty/index.d.ts +1 -0
  26. package/dist_ts_relyingparty/index.js +2 -1
  27. package/dist_ts_relyingparty/interfaces.d.ts +122 -2
  28. package/dist_ts_relyingparty/testing.d.ts +22 -0
  29. package/dist_ts_relyingparty/testing.js +42 -1
  30. package/package.json +9 -11
  31. package/readme.md +353 -8
  32. package/ts/00_commitinfo_data.ts +1 -1
  33. package/ts_browser/classes.relyingpartybearerclient.ts +821 -0
  34. package/ts_browser/index.ts +1 -0
  35. package/ts_relyingparty/classes.authorityobserver.ts +80 -12
  36. package/ts_relyingparty/classes.errors.ts +16 -1
  37. package/ts_relyingparty/classes.relyingpartyroutes.ts +340 -16
  38. package/ts_relyingparty/classes.relyingpartysessions.ts +907 -94
  39. package/ts_relyingparty/classes.rpmodels.ts +131 -9
  40. package/ts_relyingparty/contract.bearer.ts +99 -0
  41. package/ts_relyingparty/helpers.bearer.ts +259 -0
  42. package/ts_relyingparty/index.ts +1 -0
  43. package/ts_relyingparty/interfaces.ts +144 -2
  44. package/ts_relyingparty/testing.ts +60 -0
@@ -1,6 +1,6 @@
1
1
  import { type IOidcProvider } from './classes.idpoidcprovider.js';
2
2
  import type { IRelyingPartyModels } from './classes.rpmodels.js';
3
- import type { IAuthorityTokenSource, IProviderToken, IRelyingPartyHooks, IRelyingPartyLiveSession, IRelyingPartyLoginOptions, IRelyingPartyLoginStart, IRelyingPartySession, IRelyingPartySessionOpening, IResolvedRelyingPartySession, TRelyingPartyPrincipals, TRelyingPartySessionEndReason } from './interfaces.js';
3
+ import type { IAuthorityTokenSource, IProviderToken, IRelyingPartyBearerDelivery, IRelyingPartyBearerHandoff, IRelyingPartyBearerLoginRequest, IRelyingPartyBearerLoginStart, IRelyingPartyBearerOptions, IRelyingPartyBearerReturnTargets, IRelyingPartyBearerTokens, IRelyingPartyHooks, IRelyingPartyLiveSession, IRelyingPartyLoginOptions, IRelyingPartyLoginStart, IRelyingPartySession, IRelyingPartySessionOpening, IRelyingPartySessionsPage, IRelyingPartySessionsPageOptions, IResolvedRelyingPartySession, TRelyingPartyPrincipals, TRelyingPartySessionEndReason } from './interfaces.js';
4
4
  export declare const RELYING_PARTY_DEFAULT_SESSION_MAX_AGE_SECONDS: number;
5
5
  export declare const RELYING_PARTY_DEFAULT_LOGIN_PATH_PREFIX = "/auth/";
6
6
  /** The label the CSRF HMAC starts with unless the application names its own. */
@@ -9,6 +9,14 @@ export declare const RELYING_PARTY_DEFAULT_CSRF_LABEL: string;
9
9
  export declare const LOGIN_ATTEMPT_MAX_AGE_SECONDS: number;
10
10
  /** The longest intent a login carries (`IRelyingPartyLoginOptions.intent`), in characters. */
11
11
  export declare const RELYING_PARTY_MAX_INTENT_LENGTH = 4096;
12
+ /** How many sessions one page of `sessionsOfPrincipal` holds unless its `limit` says otherwise. */
13
+ export declare const RELYING_PARTY_DEFAULT_SESSIONS_PAGE_LIMIT = 100;
14
+ /** The largest `limit` of `sessionsOfPrincipal`; the store reads no larger page. */
15
+ export declare const RELYING_PARTY_MAX_SESSIONS_PAGE_LIMIT = 1000;
16
+ /** How long a bearer hand-off code can be claimed, at most; never past its session. */
17
+ export declare const BEARER_HANDOFF_MAX_AGE_SECONDS = 60;
18
+ /** How long a bearer access token lives unless `bearer.accessTokenMaxAgeSeconds` says otherwise. */
19
+ export declare const RELYING_PARTY_DEFAULT_BEARER_ACCESS_TOKEN_MAX_AGE_SECONDS: number;
12
20
  /**
13
21
  * How long ending a session waits for the provider to take its tokens back.
14
22
  * The local session is already revoked when that call runs.
@@ -36,6 +44,12 @@ export interface IRelyingPartySessionsOptions<TPrincipal extends object | undefi
36
44
  sessionEncryptionKey: Buffer;
37
45
  /** 32 bytes; the CSRF HMAC key */
38
46
  csrfKey: Buffer;
47
+ /**
48
+ * `true` (the default): cookie sessions with a session-bound CSRF token,
49
+ * beside bearer hand-off mode when `bearer` is given. Bearer hand-off alone,
50
+ * without any cookie, is `IRelyingPartyBearerOnlySessionsOptions`.
51
+ */
52
+ cookieSessions?: true;
39
53
  /** the label the CSRF HMAC starts with; an application with live sessions keeps its own */
40
54
  csrfLabel?: string;
41
55
  sessionMaxAgeSeconds?: number;
@@ -50,19 +64,51 @@ export interface IRelyingPartySessionsOptions<TPrincipal extends object | undefi
50
64
  };
51
65
  /** the prefix of the login routes; a login never returns into it */
52
66
  loginPathPrefix?: string;
67
+ /**
68
+ * absent: no bearer hand-off mode. Needs `provider` and the bearer models
69
+ * (`createRelyingPartyModels` binds them).
70
+ */
71
+ bearer?: IRelyingPartyBearerOptions;
53
72
  hooks?: IRelyingPartyHooks;
54
73
  /** the clock every comparison reads; tests step it instead of sleeping */
55
74
  now?: () => number;
56
75
  }
76
+ /**
77
+ * Bearer-only mode (`cookieSessions: false`): the application's own clients
78
+ * sign in through bearer hand-off and nothing else. The relying party never
79
+ * reads or sets a cookie — not even the clearing intent cookie of a callback
80
+ * that is no bearer login — so it has no CSRF token and takes no CSRF key:
81
+ * `csrfKey`, `csrfLabel` and `cookieNames` are refused, not ignored. The
82
+ * cookie steps (`openSession`, `startLogin`, `completeLogin`, `abandonLogin`,
83
+ * `finalizeLogin`, `clearSessionCookie`, `clearIntentCookie`) throw
84
+ * `RelyingPartyError('login_method_unavailable')` and `resolveCookieSession`
85
+ * answers `null`.
86
+ */
87
+ export interface IRelyingPartyBearerOnlySessionsOptions<TPrincipal extends object | undefined = undefined> extends Omit<IRelyingPartySessionsOptions<TPrincipal>, 'provider' | 'csrfKey' | 'csrfLabel' | 'cookieNames' | 'cookieSessions' | 'bearer'> {
88
+ cookieSessions: false;
89
+ /** every bearer session is a provider login */
90
+ provider: IOidcProvider;
91
+ bearer: IRelyingPartyBearerOptions;
92
+ csrfKey?: never;
93
+ csrfLabel?: never;
94
+ cookieNames?: never;
95
+ }
57
96
  /**
58
97
  * The relying-party session layer: provider logins through the authorization
59
98
  * code flow with PKCE, HttpOnly cookie sessions with a session-bound CSRF
60
99
  * token, provider tokens sealed at rest and rotated under a cross-replica
61
100
  * lock, and session ends that hand the tokens back.
62
101
  *
63
- * Nothing here runs on a timer: expired login attempts are swept when the next
64
- * login starts, and the only timer — the revocation budget — is unreferenced
65
- * and cleared on every path. Nothing here does I/O while it is constructed,
102
+ * With `cookieSessions: false` it runs bearer hand-off mode alone and never
103
+ * touches a cookie (`IRelyingPartyBearerOnlySessionsOptions`).
104
+ *
105
+ * Nothing here runs on a timer: expired login attempts, bearer state and
106
+ * ended sessions are swept one bounded batch at a time when a login starts or
107
+ * the application opens a session, and the only timer — the revocation
108
+ * budget — is unreferenced and cleared on every path. Every session comes
109
+ * from a login start or from `openSession`, and each of those sweeps a batch
110
+ * of at least one first, so removal keeps pace with creation and the ended
111
+ * sessions kept stay bounded. Nothing here does I/O while it is constructed,
66
112
  * so it is built before the database connects.
67
113
  *
68
114
  * `TPrincipal` is what a `principals.resolve` answers; with `isActive`
@@ -76,9 +122,15 @@ export declare class RelyingPartySessions<TPrincipal extends object | undefined
76
122
  readonly intentCookieName: string;
77
123
  readonly loginPathPrefix: string;
78
124
  readonly sessionMaxAgeSeconds: number;
79
- constructor(optionsArg: IRelyingPartySessionsOptions<TPrincipal>);
125
+ constructor(optionsArg: IRelyingPartySessionsOptions<TPrincipal> | IRelyingPartyBearerOnlySessionsOptions<TPrincipal>);
80
126
  /** Whether provider logins are offered at all. */
81
127
  get providerLoginEnabled(): boolean;
128
+ /** Whether cookie sessions are offered; `false` only in bearer-only mode (`cookieSessions: false`). */
129
+ get cookieSessionsEnabled(): boolean;
130
+ /** Whether bearer hand-off mode is offered. */
131
+ get bearerLoginEnabled(): boolean;
132
+ /** The return targets bearer clients may name, per kind, as configured; `undefined` without bearer mode. */
133
+ get bearerReturnTargets(): Readonly<Required<IRelyingPartyBearerReturnTargets>> | undefined;
82
134
  /**
83
135
  * How long one replica holds the right to rotate a session's tokens. The
84
136
  * provider detects reuse of a superseded refresh token and kills the family,
@@ -96,7 +148,11 @@ export declare class RelyingPartySessions<TPrincipal extends object | undefined
96
148
  principalId: string;
97
149
  maxAgeSeconds?: number;
98
150
  }): Promise<IRelyingPartySessionOpening>;
99
- /** The live session behind the session cookie, or `null`. A read touches it at most once a minute. */
151
+ /**
152
+ * The live session behind the session cookie, or `null`. A read touches it
153
+ * at most once a minute. Always `null` in bearer-only mode, which reads no
154
+ * cookie.
155
+ */
100
156
  resolveCookieSession(cookieHeaderArg: string | null | undefined): Promise<IResolvedRelyingPartySession<TPrincipal> | null>;
101
157
  /**
102
158
  * Re-checks a session by its id with the same strength as a cookie request:
@@ -129,6 +185,16 @@ export declare class RelyingPartySessions<TPrincipal extends object | undefined
129
185
  endSessionsOfPrincipal(principalIdArg: string, optionsArg?: {
130
186
  keepSessionId?: string;
131
187
  }): Promise<number>;
188
+ /**
189
+ * One page of a principal's live sessions, newest first: the ones neither
190
+ * ended nor expired, and none at all while the principal is not active —
191
+ * exactly the sessions `isSessionLive` confirms. Pass the page's
192
+ * `nextCursor` as `cursor` to read the next one; the last page has none.
193
+ * The cursor is opaque, and one this method did not answer is refused with
194
+ * `invalid_sessions_cursor`. A session opened while the pages are read is
195
+ * listed only when it sorts after the cursor; none is listed twice.
196
+ */
197
+ sessionsOfPrincipal(principalIdArg: string, optionsArg?: IRelyingPartySessionsPageOptions): Promise<IRelyingPartySessionsPage>;
132
198
  clearSessionCookie(): string;
133
199
  clearIntentCookie(): string;
134
200
  /**
@@ -157,14 +223,90 @@ export declare class RelyingPartySessions<TPrincipal extends object | undefined
157
223
  * session this very attempt created.
158
224
  */
159
225
  finalizeLogin(cookieHeaderArg: string | null | undefined): Promise<string>;
226
+ /**
227
+ * Starts a bearer login for one of the application's own clients: the same
228
+ * provider login as `startLogin`, but the outcome goes to the return target
229
+ * the client named — exactly one of the configured targets of its kind —
230
+ * and it sets no cookie. The login is bound to the client's S256 challenge
231
+ * instead of a browser: whoever claims its hand-off code has to present the
232
+ * verifier. Every field of the request is checked as untrusted input.
233
+ */
234
+ startBearerLogin(requestArg: IRelyingPartyBearerLoginRequest, optionsArg?: IRelyingPartyLoginOptions): Promise<IRelyingPartyBearerLoginStart>;
235
+ /**
236
+ * Where the outcome of a login callback goes when it belongs to a bearer
237
+ * login: the client kind, the return target and the client's `state`, read
238
+ * without consuming anything. `null` for a cookie login, an unknown state,
239
+ * and a return target that is no longer configured — the callback is then
240
+ * not answered as a bearer login at all.
241
+ */
242
+ bearerDeliveryOf(callbackUrlArg: URL): Promise<IRelyingPartyBearerDelivery | null>;
243
+ /**
244
+ * Consumes a bearer login's attempt exactly once, exchanges the code, admits
245
+ * the principal and opens its session — everything `completeLogin` does,
246
+ * with the same failures — and answers a single-use hand-off code bound to
247
+ * the client's challenge. The code lives `BEARER_HANDOFF_MAX_AGE_SECONDS`
248
+ * at most and only its hash is stored. A code nobody claims ends its session.
249
+ */
250
+ completeBearerLogin(callbackUrlArg: URL): Promise<IRelyingPartyBearerHandoff>;
251
+ /** `abandonLogin` for a bearer login: the provider came back without a code. */
252
+ abandonBearerLogin(callbackUrlArg: URL): Promise<void>;
253
+ /**
254
+ * Trades a hand-off code and the verifier of its challenge for the first
255
+ * access and refresh token of the session. The code is consumed by the first
256
+ * claim whatever its verifier: a wrong verifier ends the session, and so
257
+ * does the code coming back after it was claimed (RFC 6749 §4.1.2). A code
258
+ * that expired unclaimed ends its session too.
259
+ */
260
+ claimBearerHandoff(codeArg: unknown, codeVerifierArg: unknown): Promise<IRelyingPartyBearerTokens>;
261
+ /**
262
+ * Rotates a bearer session's tokens: the refresh token is spent, a new
263
+ * access and refresh token answer, and the old access token stops working.
264
+ * Rotation is strict — a client serializes its refreshes — because a
265
+ * refresh token that comes back after it was spent ends the whole session.
266
+ * The provider login is checked on the way: a login idp.global ended ends
267
+ * the session here (`invalid_bearer_grant`), an unreachable provider fails
268
+ * with `AuthorityTokenError('authority_unavailable')` and spends nothing.
269
+ */
270
+ refreshBearerTokens(refreshTokenArg: unknown): Promise<IRelyingPartyBearerTokens>;
271
+ /**
272
+ * The live session behind a bearer access token, or `null`: the token of the
273
+ * session's current generation, not expired, its session live and its
274
+ * principal active. A read touches the session at most once a minute.
275
+ */
276
+ resolveBearerAccessToken(accessTokenArg: unknown): Promise<IRelyingPartyLiveSession<TPrincipal> | null>;
277
+ /** `resolveBearerAccessToken` for an `Authorization: Bearer <token>` header. */
278
+ resolveBearerSession(authorizationHeaderArg: string | null | undefined): Promise<IRelyingPartyLiveSession<TPrincipal> | null>;
279
+ /**
280
+ * Ends the session of a bearer token — a live access token or the current
281
+ * refresh token — like `endSession`. A refresh token that was already spent
282
+ * is reuse and ends the session as such. Answers whether this call ended it.
283
+ */
284
+ endBearerSession(tokenArg: unknown): Promise<boolean>;
160
285
  /**
161
286
  * An access token of this login session that is usable right now. A token
162
287
  * within `ACCESS_TOKEN_REFRESH_SKEW_MS` of its expiry — or one whose expiry
163
288
  * is unknown — is rotated first. The session has to belong to
164
289
  * `expectedPrincipalIdArg`, so a credential is never evaluated with another
165
290
  * person's tokens.
291
+ *
292
+ * With `rejectedAccessToken` (`IAuthorityTokenSource`) the call rotates the
293
+ * session's token whatever is stored, and only a rotation that it starts
294
+ * marks its answer `rotatedAfterRejection`: one that started before it, in
295
+ * this process or another replica's, says nothing about the rejection, so
296
+ * the call awaits it and throws `authority_unavailable`. A refused rotation
297
+ * ends the session (`refresh_token_refused`) and throws
298
+ * `authority_login_required`. A session without a refresh token has nothing
299
+ * to rotate with: it throws `authority_unavailable`, or ends as
300
+ * `access_token_unusable` when its token is dead.
301
+ */
302
+ liveProviderToken(sessionIdArg: string, expectedPrincipalIdArg: string, optionsArg?: {
303
+ rejectedAccessToken?: string;
304
+ }): Promise<IProviderToken>;
305
+ /**
306
+ * One rotation of the session in this process at a time. A forced call that
307
+ * finds one running waits for it, so a refusal still ends it, and is
308
+ * unavailable otherwise: that rotation started before the call.
166
309
  */
167
- liveProviderToken(sessionIdArg: string, expectedPrincipalIdArg: string): Promise<IProviderToken>;
168
310
  private refreshProviderToken;
169
311
  private runProviderRefresh;
170
312
  /**
@@ -174,15 +316,56 @@ export declare class RelyingPartySessions<TPrincipal extends object | undefined
174
316
  */
175
317
  private tokenAfterForeignRefresh;
176
318
  private requireProvider;
319
+ private requireCookieSessions;
320
+ private requireBearer;
321
+ private checkLoginOptions;
322
+ /** An unsaved attempt with its state, nonce and sealed verifier; the caller binds it and names where it returns. */
323
+ private newLoginAttempt;
324
+ /**
325
+ * The provider half of a consumed attempt: exchanges the code, admits the
326
+ * principal and opens the session. Nothing of a failed login survives: its
327
+ * tokens are handed back before the failure leaves.
328
+ */
329
+ private openProviderLogin;
177
330
  private newSessionDoc;
178
331
  private csrfTokenOf;
179
332
  private opening;
180
333
  private snapshot;
181
334
  /** A session document that is live right now with its active principal, touched at most once a minute. */
182
335
  private liveSession;
336
+ /**
337
+ * Stores the session of a provider login and links it to its attempt.
338
+ * `beforeOpenedArg` stores what the session is handed out with besides its
339
+ * cookie; like the hook after it, a throw discards the session.
340
+ */
183
341
  private createProviderSession;
342
+ /** Where a bearer attempt's outcome goes, while its return target is still configured; `null` otherwise. */
343
+ private bearerDeliveryOfAttempt;
344
+ /** Stores the next token generation of a live session and answers its tokens; none of them is stored in the clear. */
345
+ private issueBearerTokens;
346
+ /** A spent hand-off code or refresh token came back: whoever holds it, the session ends. */
347
+ private bearerCredentialReused;
348
+ /**
349
+ * Takes a hand-off whose code expired unclaimed and ends its session, which
350
+ * nothing can reach any more. The caller has read it expired; expiry never
351
+ * moves, so only a claim in between can stop this.
352
+ */
353
+ private endUnclaimedHandoff;
184
354
  /** Revokes a session that was never handed out and erases its sealed tokens. */
185
355
  private discardUnopenedSession;
356
+ /**
357
+ * Revokes a session not revoked yet and erases its sealed tokens; answers
358
+ * the document as it was before, `null` when it was over already. Its
359
+ * expiry comes forward to the revocation, never later, so the sweep finds
360
+ * an ended session by its expiry alone and `revokedAt` keeps when and that
361
+ * it was ended. Two conditional writes keep the earlier of the two times:
362
+ * the first ends a session within its expiry, the second one past it. A
363
+ * crash between them leaves an expired row unrevoked, which reads as over
364
+ * and is swept; a row revoked before 19.4.0 kept its later expiry, which is
365
+ * harmless because every reader checks `revokedAt` or reads live rows only,
366
+ * and it ages out.
367
+ */
368
+ private revokeSessionDocument;
186
369
  /**
187
370
  * Tells the application a session ended. Its hook never stands between the
188
371
  * local revocation and the provider revocation: a throw or a rejection is
@@ -219,6 +402,25 @@ export declare class RelyingPartySessions<TPrincipal extends object | undefined
219
402
  * start. A sweep that cannot run never blocks a sign-in.
220
403
  */
221
404
  private sweepExpiredAttempts;
405
+ /**
406
+ * One bounded page per bearer login start, oldest first: hand-offs whose
407
+ * code expired unclaimed end their session, and hand-offs and token
408
+ * generations an hour past their expiry are removed — an unclaimed hand-off
409
+ * only after its session was ended. A sweep that cannot run never blocks a
410
+ * sign-in.
411
+ */
412
+ private sweepBearerState;
413
+ /**
414
+ * Removes sessions an hour past their expiry, oldest first, one bounded
415
+ * page per login start and per session the application opens. An ended
416
+ * session's expiry is its revocation at the latest, so the expiry index
417
+ * finds expired and revoked sessions alike. Each of them ended already: the
418
+ * row goes without an end, a hook or a provider call. A row is deleted only
419
+ * while its expiry is still past the cutoff, so a row whose expiry moved
420
+ * after the page was read stays. A sweep that cannot run never blocks a
421
+ * sign-in.
422
+ */
423
+ private sweepEndedSessions;
222
424
  /**
223
425
  * Reports one diagnostic. Every cleanup step reports through here, so a
224
426
  * throwing sink must not end it: the event and the sink's failure go to