@dereekb/firebase-server 14.2.0 → 14.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.
@@ -1,8 +1,18 @@
1
+ import request from 'supertest';
1
2
  import { type INestApplication } from '@nestjs/common';
3
+ import { type Maybe } from '@dereekb/util';
2
4
  import { type OidcProviderProfileKey, type OidcTokenEndpointAuthMethod } from '@dereekb/firebase';
3
5
  import { OidcClientService } from '@dereekb/firebase-server/oidc';
4
6
  /**
5
- * Configuration for {@link performFullOAuthFlow}.
7
+ * A stage of the OAuth authorization code flow that {@link performOAuthFlow} can stop at.
8
+ *
9
+ * - `'auth'` — stop on the `/oidc/auth` response, before the login interaction.
10
+ * - `'callback'` — stop on the client callback redirect, before the token exchange.
11
+ * - `'token'` — run the whole flow, through the token exchange.
12
+ */
13
+ export type OAuthTestFlowStage = 'auth' | 'callback' | 'token';
14
+ /**
15
+ * Configuration for {@link performOAuthFlow} / {@link performFullOAuthFlow}.
6
16
  */
7
17
  export interface OAuthTestFlowConfig {
8
18
  /**
@@ -33,36 +43,226 @@ export interface OAuthTestFlowConfig {
33
43
  * "all registered scopes" resolution deliberately drops assignment-only scopes.
34
44
  */
35
45
  readonly providerProfiles?: readonly OidcProviderProfileKey[];
46
+ /**
47
+ * OAuth `prompt` parameter for the authorization request (e.g. `'consent'` to force the consent screen
48
+ * on a session that has already authorized the client). Omitted by default.
49
+ */
50
+ readonly prompt?: string;
51
+ /**
52
+ * Explicit subset of the requested OIDC scopes to grant at consent (the consent request's
53
+ * `grantedOIDCScopes`). Omitted by default, which grants every requested scope.
54
+ */
55
+ readonly grantedOIDCScopes?: readonly string[];
56
+ /**
57
+ * A prior flow's client and cookies to run this flow against — the same OAuth client, the same
58
+ * provider session, and therefore the same Grant. Omitted by default, which creates a fresh client
59
+ * and starts a fresh session.
60
+ *
61
+ * When the session is still logged in, the provider skips the login prompt and the flow goes straight
62
+ * to consent (or straight to the callback when nothing new needs consenting and `prompt` is unset).
63
+ */
64
+ readonly session?: OAuthTestFlowSession;
65
+ /**
66
+ * RFC 8707 `resource` indicator, sent on both `/oidc/auth` and `/oidc/token`.
67
+ *
68
+ * Must match a key registered on the provider's `resourceServers` config. The issued access
69
+ * token then carries that entry's `audience` and — when the entry sets
70
+ * `accessTokenFormat: 'jwt'` — is an RS256 JWT a remote resource server can verify against the
71
+ * provider's JWKS, rather than the default opaque token only the provider itself can validate.
72
+ */
73
+ readonly resource?: string;
74
+ /**
75
+ * Extra query parameters merged onto the `/oidc/auth` request, for parameters this config does
76
+ * not model explicitly.
77
+ */
78
+ readonly extraAuthParams?: Record<string, string | number>;
79
+ /**
80
+ * The stage to stop the flow at. Defaults to `'token'`, the full flow.
81
+ *
82
+ * - `'auth'` — issue the `/oidc/auth` request and stop, exposing the raw redirect response on
83
+ * {@link OAuthTestFlowResult.authResponse}. Nothing logs in, so no `uid` is needed.
84
+ * - `'callback'` — drive auth → login → consent → the callback redirect and stop before the token
85
+ * exchange, exposing {@link OAuthTestFlowResult.callbackUrl} and
86
+ * {@link OAuthTestFlowResult.consentRedirectUrl}. A callback carrying an `error` instead of a
87
+ * `code` (e.g. `access_denied`) is not treated as a failure at this stage, so a caller can assert
88
+ * on it.
89
+ * - `'token'` — the full flow, exchanging the authorization code for tokens.
90
+ *
91
+ * Ignored by {@link performFullOAuthFlow} / {@link setupAndPerformFullOAuthFlow}, whose result type
92
+ * guarantees tokens; stop early with {@link performOAuthFlow} / {@link setupAndPerformOAuthFlow}.
93
+ */
94
+ readonly stopAtStage?: OAuthTestFlowStage;
95
+ }
96
+ /**
97
+ * The client and cookies a flow ran with, for chaining a second flow onto the same provider session.
98
+ */
99
+ export interface OAuthTestFlowSession {
100
+ readonly client: OAuthTestFlowClient;
101
+ readonly cookieJar: OAuthTestFlowCookieJar;
102
+ }
103
+ /**
104
+ * The OAuth client a flow created (or reused).
105
+ */
106
+ export interface OAuthTestFlowClient {
107
+ readonly client_id: string;
108
+ readonly client_secret?: string;
109
+ readonly redirectUri: string;
110
+ }
111
+ /**
112
+ * Cookie jar helpers for the OAuth flow. See {@link createCookieJar}.
113
+ */
114
+ export interface OAuthTestFlowCookieJar {
115
+ readonly collectCookies: (res: request.Response) => void;
116
+ readonly cookieHeader: () => string;
117
+ }
118
+ /**
119
+ * Result of {@link performOAuthFlow}, covering every {@link OAuthTestFlowStage} it can stop at.
120
+ *
121
+ * Every field a stage produced is populated; the fields belonging to later stages are undefined.
122
+ */
123
+ export interface OAuthTestFlowResult {
124
+ /**
125
+ * The stage the flow stopped at.
126
+ */
127
+ readonly stage: OAuthTestFlowStage;
128
+ /**
129
+ * The raw `/oidc/auth` response, before any redirect was followed.
130
+ *
131
+ * Always populated. This is what a flow stopped at the `'auth'` stage asserts on — it may be a
132
+ * redirect to the login interaction, or an error callback (e.g. `error=invalid_target`).
133
+ */
134
+ readonly authResponse: request.Response;
135
+ /**
136
+ * The PKCE `code_verifier` the flow's `code_challenge` was derived from.
137
+ *
138
+ * Always populated, for a caller that performs its own `/oidc/token` request after stopping at the
139
+ * `'callback'` stage.
140
+ */
141
+ readonly codeVerifier: string;
142
+ /**
143
+ * The URL of the consent SCREEN the provider redirected to, when the flow reached it.
144
+ *
145
+ * Its `scopes` query parameter is the checkbox list the consent UI renders, so it is what scope
146
+ * withholding (e.g. an admin-only scope kept off a non-admin's screen) acts on.
147
+ *
148
+ * Undefined when the flow stopped at the `'auth'` stage, or when nothing needed consenting.
149
+ */
150
+ readonly consentRedirectUrl?: Maybe<URL>;
151
+ /**
152
+ * The client callback URL the flow ended on, carrying either a `code` or an `error`.
153
+ *
154
+ * Undefined when the flow stopped at the `'auth'` stage.
155
+ */
156
+ readonly callbackUrl?: Maybe<URL>;
157
+ /**
158
+ * The access token the token endpoint issued. Undefined unless the flow ran to the `'token'` stage.
159
+ */
160
+ readonly accessToken?: string;
161
+ /**
162
+ * The ID token the token endpoint issued. Undefined unless the flow ran to the `'token'` stage.
163
+ */
164
+ readonly idToken?: string;
165
+ /**
166
+ * The `token_type` the token endpoint reported for the access token.
167
+ */
168
+ readonly tokenType?: string;
169
+ /**
170
+ * The space-separated scope the token endpoint reported for the access token. Undefined unless the
171
+ * flow ran to the `'token'` stage.
172
+ */
173
+ readonly scope?: string;
174
+ /**
175
+ * The client and cookies this flow ran with, for chaining another flow onto the same session via
176
+ * {@link OAuthTestFlowConfig.session} — and for the cookie header a caller's own `/oidc/token`
177
+ * request needs, via `session.cookieJar.cookieHeader()`.
178
+ */
179
+ readonly session: OAuthTestFlowSession;
36
180
  }
37
- export interface PerformFullOAuthFlowResult {
181
+ /**
182
+ * Result of {@link performFullOAuthFlow} — an {@link OAuthTestFlowResult} that ran to the `'token'`
183
+ * stage, so the callback and the tokens are guaranteed.
184
+ */
185
+ export interface PerformFullOAuthFlowResult extends OAuthTestFlowResult {
186
+ readonly callbackUrl: URL;
38
187
  readonly accessToken: string;
39
188
  readonly idToken: string;
189
+ readonly scope: string;
40
190
  }
41
191
  /**
42
- * Input for {@link performFullOAuthFlow}.
192
+ * Input for {@link performOAuthFlow}.
43
193
  */
44
- export interface PerformFullOAuthFlowInput {
194
+ export interface PerformOAuthFlowInput {
45
195
  readonly server: ReturnType<INestApplication['getHttpServer']>;
46
196
  readonly oidcClientService: OidcClientService;
47
197
  readonly nestApp: INestApplication;
48
- readonly uid: string;
198
+ /**
199
+ * Firebase user ID for whom the test ID token is minted and the flow is authorized.
200
+ *
201
+ * Only optional for a flow that stops at the `'auth'` stage, which never reaches the login
202
+ * interaction. Any later stage throws when it is missing.
203
+ */
204
+ readonly uid?: Maybe<string>;
49
205
  readonly config?: OAuthTestFlowConfig;
50
206
  }
207
+ /**
208
+ * Input for {@link performFullOAuthFlow}.
209
+ */
210
+ export interface PerformFullOAuthFlowInput extends PerformOAuthFlowInput {
211
+ readonly uid: string;
212
+ }
213
+ /**
214
+ * Performs the OAuth authorization code flow with PKCE, up to the configured
215
+ * {@link OAuthTestFlowConfig.stopAtStage}.
216
+ *
217
+ * Steps: create client → PKCE → auth redirect → login → consent → code exchange → token
218
+ *
219
+ * Stopping early is how a test asserts on an intermediate the completed flow discards — the raw
220
+ * `/oidc/auth` response, the consent screen's offered `scopes`, or a callback that came back carrying
221
+ * an `error` instead of a `code`. A stage before `'token'` never exchanges the code, so it never
222
+ * throws on such a callback; the caller asserts on {@link OAuthTestFlowResult.callbackUrl} instead,
223
+ * and can run its own `/oidc/token` request with {@link OAuthTestFlowResult.codeVerifier} and the
224
+ * session's cookie header.
225
+ *
226
+ * @param input - Bag of services and overrides needed to drive the flow.
227
+ * @param input.server - HTTP server returned by `nestApp.getHttpServer()` against which all supertest requests are issued.
228
+ * @param input.oidcClientService - Service used to create the OAuth client whose credentials drive the flow.
229
+ * @param input.nestApp - Initialized NestJS application; used to resolve {@link OidcAccountService} for project-id-derived ID tokens and default scopes.
230
+ * @param input.uid - Firebase user ID for whom the test ID token is minted; required for any stage past `'auth'`.
231
+ * @param input.config - Optional flow overrides (scopes, redirect URI, client name, token endpoint auth method, provider profiles, stop stage).
232
+ * @returns The intermediates the flow produced, plus the tokens when it ran to the `'token'` stage.
233
+ * @throws {Error} When a stage past `'auth'` is requested without a `uid`, or when the token exchange step fails (the response body and status are included in the message).
234
+ */
235
+ export declare function performOAuthFlow(input: PerformOAuthFlowInput): Promise<OAuthTestFlowResult>;
51
236
  /**
52
237
  * Performs the full OAuth authorization code flow with PKCE and returns tokens.
53
238
  *
54
239
  * Steps: create client → PKCE → auth redirect → login → consent → code exchange → token
55
240
  *
241
+ * Always runs to the `'token'` stage — {@link OAuthTestFlowConfig.stopAtStage} is ignored here, since
242
+ * this function's result guarantees tokens. Use {@link performOAuthFlow} to stop earlier.
243
+ *
56
244
  * @param input - Bag of services and overrides needed to drive the flow end-to-end.
57
245
  * @param input.server - HTTP server returned by `nestApp.getHttpServer()` against which all supertest requests are issued.
58
246
  * @param input.oidcClientService - Service used to create the OAuth client whose credentials drive the flow.
59
247
  * @param input.nestApp - Initialized NestJS application; used to resolve {@link OidcAccountService} for project-id-derived ID tokens and default scopes.
60
248
  * @param input.uid - Firebase user ID for whom the test ID token is minted and the OAuth flow is authorized.
61
249
  * @param input.config - Optional flow overrides (scopes, redirect URI, client name, token endpoint auth method, provider profiles).
62
- * @returns The exchanged access token and ID token from the OIDC `/token` endpoint.
250
+ * @returns The exchanged access token and ID token from the OIDC `/token` endpoint, plus the flow's intermediates.
63
251
  * @throws {Error} When the token exchange step fails (the response body and status are included in the message).
64
252
  */
65
253
  export declare function performFullOAuthFlow(input: PerformFullOAuthFlowInput): Promise<PerformFullOAuthFlowResult>;
254
+ /**
255
+ * Higher-level helper that resolves OIDC services from the NestJS DI container,
256
+ * rotates JWKS keys, and then performs the OAuth flow up to {@link OAuthTestFlowConfig.stopAtStage}.
257
+ *
258
+ * This avoids callers needing to import from `@dereekb/firebase-server/oidc` directly.
259
+ *
260
+ * @param nestApp - Initialized NestJS application from which {@link JwksService} and {@link OidcClientService} are resolved.
261
+ * @param uid - Firebase user ID for whom the OAuth flow is authorized; only omittable for the `'auth'` stage.
262
+ * @param config - Optional flow overrides (scopes, redirect URI, client name, token endpoint auth method, provider profiles, stop stage).
263
+ * @returns The result of {@link performOAuthFlow}.
264
+ */
265
+ export declare function setupAndPerformOAuthFlow(nestApp: INestApplication, uid: Maybe<string>, config?: OAuthTestFlowConfig): Promise<OAuthTestFlowResult>;
66
266
  /**
67
267
  * Higher-level helper that resolves OIDC services from the NestJS DI container,
68
268
  * rotates JWKS keys, and then performs the full OAuth flow.
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/twilio",
3
- "version": "14.2.0",
3
+ "version": "14.4.0",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "peerDependencies": {
7
- "@dereekb/date": "14.2.0",
8
- "@dereekb/firebase": "14.2.0",
9
- "@dereekb/firebase-server": "14.2.0",
10
- "@dereekb/model": "14.2.0",
11
- "@dereekb/nestjs": "14.2.0",
12
- "@dereekb/rxjs": "14.2.0",
13
- "@dereekb/util": "14.2.0"
7
+ "@dereekb/date": "14.4.0",
8
+ "@dereekb/firebase": "14.4.0",
9
+ "@dereekb/firebase-server": "14.4.0",
10
+ "@dereekb/model": "14.4.0",
11
+ "@dereekb/nestjs": "14.4.0",
12
+ "@dereekb/rxjs": "14.4.0",
13
+ "@dereekb/util": "14.4.0"
14
14
  },
15
15
  "exports": {
16
16
  "./package.json": "./package.json",
package/zoho/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/zoho",
3
- "version": "14.2.0",
3
+ "version": "14.4.0",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "peerDependencies": {
7
- "@dereekb/analytics": "14.2.0",
8
- "@dereekb/date": "14.2.0",
9
- "@dereekb/model": "14.2.0",
10
- "@dereekb/nestjs": "14.2.0",
11
- "@dereekb/rxjs": "14.2.0",
12
- "@dereekb/firebase": "14.2.0",
13
- "@dereekb/firebase-server": "14.2.0",
14
- "@dereekb/util": "14.2.0",
15
- "@dereekb/zoho": "14.2.0",
7
+ "@dereekb/analytics": "14.4.0",
8
+ "@dereekb/date": "14.4.0",
9
+ "@dereekb/model": "14.4.0",
10
+ "@dereekb/nestjs": "14.4.0",
11
+ "@dereekb/rxjs": "14.4.0",
12
+ "@dereekb/firebase": "14.4.0",
13
+ "@dereekb/firebase-server": "14.4.0",
14
+ "@dereekb/util": "14.4.0",
15
+ "@dereekb/zoho": "14.4.0",
16
16
  "@nestjs/common": "^12.0.1",
17
17
  "@nestjs/config": "^12.0.0",
18
18
  "express": "^5.2.1"