@dereekb/firebase-server 13.27.1 → 13.29.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/oidc/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/oidc",
3
- "version": "13.27.1",
3
+ "version": "13.29.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.27.1",
6
- "@dereekb/date": "13.27.1",
7
- "@dereekb/firebase": "13.27.1",
8
- "@dereekb/firebase-server": "13.27.1",
9
- "@dereekb/model": "13.27.1",
10
- "@dereekb/nestjs": "13.27.1",
11
- "@dereekb/rxjs": "13.27.1",
12
- "@dereekb/util": "13.27.1",
13
- "@dereekb/zoho": "13.27.1",
5
+ "@dereekb/analytics": "13.29.0",
6
+ "@dereekb/date": "13.29.0",
7
+ "@dereekb/firebase": "13.29.0",
8
+ "@dereekb/firebase-server": "13.29.0",
9
+ "@dereekb/model": "13.29.0",
10
+ "@dereekb/nestjs": "13.29.0",
11
+ "@dereekb/rxjs": "13.29.0",
12
+ "@dereekb/util": "13.29.0",
13
+ "@dereekb/zoho": "13.29.0",
14
14
  "@nestjs/common": "^11.1.19",
15
15
  "@nestjs/config": "^4.0.4",
16
16
  "express": "^5.2.1",
@@ -3,5 +3,6 @@ export * from './model';
3
3
  export * from './oidc.config';
4
4
  export * from './controller';
5
5
  export * from './oidc.module';
6
+ export * from './profile';
6
7
  export * from './scope';
7
8
  export * from './service';
@@ -1,6 +1,33 @@
1
1
  import { type INestApplication } from '@nestjs/common';
2
+ import { type Maybe } from '@dereekb/util';
3
+ import { type OidcCorsConfig } from '../oidc.config';
2
4
  /**
3
- * Enables CORS on the NestJS app for OIDC cross-origin requests originating from the frontend.
5
+ * Signature of the `origin` callback the `cors` package (used by NestJS
6
+ * `enableCors`) invokes per request. Returning a string sets that exact
7
+ * `Access-Control-Allow-Origin`; returning `false` omits the header entirely.
8
+ */
9
+ export type OidcCorsOriginDelegate = (requestOrigin: Maybe<string>, callback: (err: Maybe<Error>, origin?: string | boolean) => void) => void;
10
+ /**
11
+ * Builds the `cors`-package origin delegate implementing the composed OIDC CORS policy.
12
+ *
13
+ * - An allowlisted origin (`appUrl` or any {@link OidcCorsConfig.allowOrigins} entry) is
14
+ * reflected exactly, which pre-empts oidc-provider's own CORS handling.
15
+ * - Otherwise, when {@link OidcCorsConfig.clientBased} is enabled, the header is OMITTED so
16
+ * oidc-provider runs its `clientBasedCORS` check on client routes (`/token`) and its builtin
17
+ * reflect on client-less routes (discovery/JWKS).
18
+ * - Otherwise (client-based off), `appUrl` is returned unconditionally so oidc-provider stays
19
+ * pre-empted and there is never a reflect-any.
20
+ *
21
+ * Exported for unit testing without a running Nest application.
22
+ *
23
+ * @param appUrl - The authoritative frontend origin (always allowed).
24
+ * @param cors - The unified CORS configuration.
25
+ * @returns A `cors`-package origin delegate.
26
+ */
27
+ export declare function oidcCorsOriginDelegate(appUrl: string, cors: OidcCorsConfig): OidcCorsOriginDelegate;
28
+ /**
29
+ * Enables CORS on the NestJS app for OIDC cross-origin requests originating from the frontend
30
+ * and any additional relying-party origins.
4
31
  *
5
32
  * When the OIDC issuer lives on a different origin than the frontend app (e.g. the frontend is
6
33
  * `https://app.example.com` and the OIDC server is at `https://api.example.com`), the frontend's
@@ -8,16 +35,22 @@ import { type INestApplication } from '@nestjs/common';
8
35
  * and `/interaction/{uid}/consent`. Because the POST body is `application/json`, the browser
9
36
  * sends a CORS preflight. Without an `Access-Control-Allow-Origin` reply the preflight fails,
10
37
  * the POST never reaches the server, and the OAuth flow stalls before `finishInteractionByUid`
11
- * can run.
38
+ * can run. Browser-based OIDC relying parties (public PKCE clients) likewise need CORS on the
39
+ * discovery GET and the token POST.
40
+ *
41
+ * `appUrl` (from `FirebaseServerEnvService.appUrl`) is always allowed. Additional behavior is
42
+ * driven by the optional {@link OidcModuleConfig.cors} configuration:
43
+ * - {@link OidcCorsConfig.allowOrigins} adds an explicit allowlist of relying-party origins.
44
+ * - {@link OidcCorsConfig.clientBased} defers non-allowlisted origins to oidc-provider's
45
+ * `clientBasedCORS`, which trusts each requesting client's registered `redirect_uris`.
12
46
  *
13
- * Origin is sourced from `FirebaseServerEnvService.appUrl`, which is the authoritative frontend
14
- * origin per environment. Single-origin deployments (e.g. local dev where the frontend and the
15
- * OIDC server share `http://localhost:9010`) still benefit: their cross-origin requests are
16
- * actually same-origin and CORS is a no-op.
47
+ * When `cors` is unset, this preserves the legacy single-origin behavior exactly:
48
+ * `Access-Control-Allow-Origin: <appUrl>` on every response, which pre-empts oidc-provider's
49
+ * own CORS handling.
17
50
  *
18
51
  * Credentials are intentionally NOT enabled (`Access-Control-Allow-Credentials` stays off). The
19
- * `/interaction/{uid}/*` POSTs carry the Firebase ID token in the JSON body, not in cookies, so
20
- * we keep the CORS surface minimal.
52
+ * `/interaction/{uid}/*` POSTs and the token exchange carry their bearer material in the request
53
+ * body, not in cookies, so we keep the CORS surface minimal.
21
54
  *
22
55
  * @param nestApp - The NestJS application to configure.
23
56
  *
@@ -1,6 +1,6 @@
1
1
  import type { Configuration } from 'oidc-provider';
2
2
  import { type WebsitePath, type SlashPath, type Seconds } from '@dereekb/util';
3
- import { type OidcScope, type OidcTokenEndpointAuthMethod } from '@dereekb/firebase';
3
+ import { type OidcProviderProfile, type OidcScope, type OidcTokenEndpointAuthMethod } from '@dereekb/firebase';
4
4
  import { type JwksServiceConfig } from './service/oidc.jwks.service';
5
5
  import { type JwksKeyConverterConfig } from './model';
6
6
  /**
@@ -104,6 +104,19 @@ export interface OidcProviderConfig<S extends OidcScope = OidcScope> {
104
104
  * All other grants keep oidc-provider's default rotation behavior.
105
105
  */
106
106
  readonly nonRotatingScopes?: readonly string[];
107
+ /**
108
+ * Named provider profiles that unlock (and optionally force-require) scopes for the OIDC clients
109
+ * they are assigned to. Any scope referenced by a profile is treated as restricted: a client may
110
+ * only obtain it when one of its assigned profiles (persisted as `dbx_provider_profiles` client
111
+ * metadata) unlocks it. A profile scope marked `require: 'required'` is force-required — surfaced
112
+ * as a required scope at consent and hard-rejected if not granted.
113
+ *
114
+ * Keeps the generic package app-agnostic: apps declare their profiles statically and list them
115
+ * here, rather than the package hard-coding any profile or scope name. This gate is independent of
116
+ * {@link adminOnlyScopes} (which gates by the resolving user's admin status); a scope may be
117
+ * subject to both.
118
+ */
119
+ readonly providerProfiles?: readonly OidcProviderProfile<S>[];
107
120
  }
108
121
  /**
109
122
  * Returns the space-delimited list of every scope declared on `providerConfig.claims`.
@@ -184,6 +197,44 @@ export declare const DEFAULT_MAX_SERVICE_TOKEN_LOGIN_DURATION_SECONDS: number;
184
197
  * Default token lifetimes: 15 min access tokens, 30-day refresh tokens, 30-day sessions/grants, 60 s auth codes.
185
198
  */
186
199
  export declare const DEFAULT_OIDC_TOKEN_LIFETIMES: OidcTokenLifetimes;
200
+ /**
201
+ * Unified CORS configuration for the cross-origin browser OIDC endpoints (the
202
+ * discovery GET and the token POST). Supports two independent, composable modes;
203
+ * both, either, or neither may be used.
204
+ *
205
+ * When the whole {@link OidcModuleConfig.cors} object is unset, the Express-level
206
+ * {@link applyOidcCorsMiddleware} preserves the pre-existing behavior: it emits
207
+ * `Access-Control-Allow-Origin: <appUrl>` unconditionally on every response, which
208
+ * pre-empts oidc-provider's own CORS handling.
209
+ */
210
+ export interface OidcCorsConfig {
211
+ /**
212
+ * Explicit allowlist of additional browser origins (beyond `appUrl`, which is
213
+ * ALWAYS allowed) permitted to make cross-origin requests to the OIDC endpoints.
214
+ *
215
+ * Each entry must be a bare origin (scheme + host + optional port, no path or
216
+ * trailing slash), e.g. `'https://lms.app.example.com'`. When a request's `Origin`
217
+ * matches, the Express CORS layer reflects that exact origin
218
+ * (`Access-Control-Allow-Origin: <origin>`), pre-empting oidc-provider's own CORS.
219
+ */
220
+ readonly allowOrigins?: readonly string[];
221
+ /**
222
+ * When `true`, enables oidc-provider's `clientBasedCORS` on client-assigned routes
223
+ * (notably `POST /token`). For a request whose `Origin` is NOT in the explicit
224
+ * {@link allowOrigins} list, the Express layer OMITS `Access-Control-Allow-Origin`
225
+ * so oidc-provider runs its per-client check: the request is allowed iff the `Origin`
226
+ * matches the origin of one of the requesting client's registered `redirect_uris`.
227
+ * Discovery and JWKS (no client context) fall back to oidc-provider's builtin
228
+ * reflect (public metadata).
229
+ *
230
+ * When `false`/unset, non-allowlisted origins receive `Access-Control-Allow-Origin:
231
+ * <appUrl>` from the Express layer (pre-empting oidc-provider), so there is no
232
+ * reflect-any.
233
+ *
234
+ * Defaults to `false`.
235
+ */
236
+ readonly clientBased?: boolean;
237
+ }
187
238
  /**
188
239
  * Configuration for the OIDC module.
189
240
  *
@@ -411,6 +462,15 @@ export declare abstract class OidcModuleConfig {
411
462
  * Defaults to `false`.
412
463
  */
413
464
  readonly trustProxyInNonProduction?: boolean;
465
+ /**
466
+ * Unified CORS configuration for the cross-origin browser OIDC endpoints
467
+ * (discovery GET, token POST). When unset, the Express CORS layer preserves the
468
+ * pre-existing behavior: `Access-Control-Allow-Origin: <appUrl>` on every
469
+ * response, which pre-empts oidc-provider's own CORS handling.
470
+ *
471
+ * @see OidcCorsConfig
472
+ */
473
+ readonly cors?: OidcCorsConfig;
414
474
  /**
415
475
  * Validates that all required fields are present on the config.
416
476
  *
@@ -138,7 +138,7 @@ export declare function oidcFirestoreCollectionsFactory(firestoreContext: Firest
138
138
  * Subset of {@link OidcModuleConfig} that consumers may override via
139
139
  * `oidcModuleMetadata`'s `config` or `configFactory`.
140
140
  */
141
- export type OidcModuleMetadataOverrides = Partial<Pick<OidcModuleConfig, 'issuer' | 'suppressBodyParserWarning' | 'renderError' | 'protectedPaths' | 'appOAuthInteractionPath' | 'appOAuthLoginUrlPart' | 'appOAuthConsentUrlPart' | 'tokenEndpointAuthMethods' | 'registrationEnabled' | 'trustProxy' | 'trustProxyInNonProduction' | 'tokenLifetimes' | 'maxRequestedLoginDuration' | 'minRequestedLoginDuration' | 'defaultRequestedLoginDuration' | 'resourceServers' | 'resourceMetadataUrl' | 'configureMcpResourceServer'>>;
141
+ export type OidcModuleMetadataOverrides = Partial<Pick<OidcModuleConfig, 'issuer' | 'suppressBodyParserWarning' | 'renderError' | 'protectedPaths' | 'appOAuthInteractionPath' | 'appOAuthLoginUrlPart' | 'appOAuthConsentUrlPart' | 'tokenEndpointAuthMethods' | 'registrationEnabled' | 'trustProxy' | 'trustProxyInNonProduction' | 'tokenLifetimes' | 'maxRequestedLoginDuration' | 'minRequestedLoginDuration' | 'defaultRequestedLoginDuration' | 'resourceServers' | 'resourceMetadataUrl' | 'configureMcpResourceServer' | 'cors'>>;
142
142
  export interface ProvideAppOidcModuleMetadataConfig extends Pick<ModuleMetadata, 'imports' | 'exports' | 'providers'> {
143
143
  /**
144
144
  * Module that exports the required dependencies for this module.
@@ -0,0 +1,29 @@
1
+ import { type OidcProviderProfile, type OidcScope, type OidcProviderProfileKey } from '@dereekb/firebase';
2
+ /**
3
+ * Custom oidc-provider client metadata field holding the {@link OidcProviderProfile} keys assigned
4
+ * to a client. Registered via `extraClientMetadata` and persisted on the client's `OidcEntry` payload.
5
+ */
6
+ export declare const DBX_FIREBASE_SERVER_OIDC_PROVIDER_PROFILES_CLIENT_METADATA = "dbx_provider_profiles";
7
+ /**
8
+ * The scopes a client with the given assigned profile keys is allowed/required to obtain, resolved
9
+ * against the provider-profile registry.
10
+ */
11
+ export interface OidcClientProviderProfileScopes<S extends OidcScope = OidcScope> {
12
+ /**
13
+ * Scopes unlocked for the client (the union of its assigned profiles' scopes).
14
+ */
15
+ readonly unlocked: Set<S>;
16
+ /**
17
+ * Scopes the client's profiles force-require (`require: 'required'`).
18
+ */
19
+ readonly required: Set<S>;
20
+ }
21
+ /**
22
+ * Resolves the unlocked + required scope sets for a client from the provider-profile registry and
23
+ * the profile keys assigned to that client.
24
+ *
25
+ * @param providerProfiles - The provider-profile registry (from {@link OidcProviderConfig.providerProfiles}).
26
+ * @param clientProfileKeys - The profile keys assigned to the client (its `dbx_provider_profiles`).
27
+ * @returns The unlocked and required scope sets for the client.
28
+ */
29
+ export declare function oidcClientProviderProfileScopes<S extends OidcScope = OidcScope>(providerProfiles: readonly OidcProviderProfile<S>[] | undefined, clientProfileKeys: readonly OidcProviderProfileKey[] | undefined): OidcClientProviderProfileScopes<S>;
@@ -2,15 +2,20 @@ import { type OnCallTypedModelParams } from '@dereekb/firebase';
2
2
  import { type AssertModelCrudRequestFunction } from '@dereekb/firebase-server';
3
3
  /**
4
4
  * Builds a {@link AssertModelCrudRequestFunction} that rejects callModel requests
5
- * lacking the OIDC scope mapped from the call type.
5
+ * lacking the required OIDC scope(s) for the call.
6
+ *
7
+ * Enforcement is additive: an OIDC caller must hold BOTH the per-verb scope mapped
8
+ * from the call type (`model.<call>`) AND any per-function scope a handler declared
9
+ * via `withApiDetails({ requiredScope })` — surfaced on `context.requiredScope`.
6
10
  *
7
11
  * Bypasses the check (no-op) when the request is not OIDC-authenticated — i.e.
8
12
  * when {@link getOidcScopesFromRequest} returns `undefined` because there is no
9
13
  * `auth.token.scope` claim on the request. Regular Firebase ID-token callers are
10
14
  * unaffected and continue to be gated by auth roles.
11
15
  *
12
- * Bypasses the check for custom (non-CRUD) call types so app-specific verbs
13
- * remain unrestricted unless an app wires its own assertion.
16
+ * Bypasses the check for custom (non-CRUD) call types that declare no per-function
17
+ * scope, so app-specific verbs remain unrestricted unless an app wires its own
18
+ * assertion or declares a `requiredScope`.
14
19
  *
15
20
  * Wire as the `preAssert` of {@link onCallModel}.
16
21
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server",
3
- "version": "13.27.1",
3
+ "version": "13.29.0",
4
4
  "sideEffects": false,
5
5
  "exports": {
6
6
  "./test": {
@@ -57,15 +57,15 @@
57
57
  "types": "./src/index.d.ts",
58
58
  "peerDependencies": {
59
59
  "@cantoo/pdf-lib": "^2.6.5",
60
- "@dereekb/analytics": "13.27.1",
61
- "@dereekb/date": "13.27.1",
62
- "@dereekb/dbx-core": "13.27.1",
63
- "@dereekb/firebase": "13.27.1",
64
- "@dereekb/model": "13.27.1",
65
- "@dereekb/nestjs": "13.27.1",
66
- "@dereekb/rxjs": "13.27.1",
67
- "@dereekb/util": "13.27.1",
68
- "@dereekb/zoho": "13.27.1",
60
+ "@dereekb/analytics": "13.29.0",
61
+ "@dereekb/date": "13.29.0",
62
+ "@dereekb/dbx-core": "13.29.0",
63
+ "@dereekb/firebase": "13.29.0",
64
+ "@dereekb/model": "13.29.0",
65
+ "@dereekb/nestjs": "13.29.0",
66
+ "@dereekb/rxjs": "13.29.0",
67
+ "@dereekb/util": "13.29.0",
68
+ "@dereekb/zoho": "13.29.0",
69
69
  "@google-cloud/firestore": "^7.11.6",
70
70
  "@google-cloud/storage": "^7.19.0",
71
71
  "@modelcontextprotocol/sdk": "1.29.0",
@@ -1,5 +1,5 @@
1
1
  import { type AuthRole, type Maybe, type PromiseOrValue } from '@dereekb/util';
2
- import { type OnCallFunctionType, type OnCallTypedModelParams, type FirestoreModelType, type ModelFirebaseCrudFunctionSpecifier } from '@dereekb/firebase';
2
+ import { type OnCallFunctionType, type OnCallTypedModelParams, type FirestoreModelType, type ModelFirebaseCrudFunctionSpecifier, type OidcScope } from '@dereekb/firebase';
3
3
  import { type FirebaseServerAuthData } from '../controller/auth.context.server';
4
4
  import { type OnCallModelFunctionAnalyticsDetails } from './analytics.details';
5
5
  /**
@@ -302,6 +302,16 @@ export interface OnCallModelFunctionApiDetails<R = unknown, M = unknown> {
302
302
  * When provided, the dispatch chain will call lifecycle hooks around handler execution.
303
303
  */
304
304
  readonly analytics?: OnCallModelFunctionAnalyticsDetails;
305
+ /**
306
+ * Optional per-function OIDC scope an OIDC-authenticated caller must additionally hold to
307
+ * invoke this handler, on top of the per-verb `model.<call>` scope.
308
+ *
309
+ * Composes additively with the CRUD-verb scope enforced by `oidcCallModelScopePreAssert`: an
310
+ * OIDC caller still needs `model.<call>` AND this scope. Non-OIDC callers (Firebase ID tokens)
311
+ * bypass scope enforcement entirely and are unaffected. Also folded into the MCP tool-list
312
+ * visibility filter, so a tool the caller lacks this scope for is neither listed nor callable.
313
+ */
314
+ readonly requiredScope?: OidcScope;
305
315
  }
306
316
  /**
307
317
  * Ref interface for objects (functions) that carry handler-level _apiDetails.
@@ -564,6 +574,20 @@ export declare function getModelApiDetails(callModelFn: Maybe<OnCallApiDetailsRe
564
574
  * @returns The analytics details for the resolved handler, or undefined.
565
575
  */
566
576
  export declare function resolveAnalyticsFromApiDetails(apiDetails: OnCallModelApiDetails, call: OnCallFunctionType, modelType: FirestoreModelType, specifier?: ModelFirebaseCrudFunctionSpecifier): Maybe<OnCallModelFunctionAnalyticsDetails>;
577
+ /**
578
+ * Resolves the leaf-level per-function required OIDC scope from the aggregated _apiDetails tree.
579
+ *
580
+ * Walks: call -> modelType -> specifier (if specifier-level), then reads the `requiredScope`
581
+ * field from the handler-level {@link OnCallModelFunctionApiDetails}. Mirrors
582
+ * {@link resolveAnalyticsFromApiDetails}.
583
+ *
584
+ * @param apiDetails - The top-level aggregated API details.
585
+ * @param call - The CRUD operation type to look up.
586
+ * @param modelType - The Firestore model type to look up.
587
+ * @param specifier - Optional specifier key for variant handlers.
588
+ * @returns The per-function required scope for the resolved handler, or undefined.
589
+ */
590
+ export declare function resolveRequiredScopeFromApiDetails(apiDetails: OnCallModelApiDetails, call: OnCallFunctionType, modelType: FirestoreModelType, specifier?: ModelFirebaseCrudFunctionSpecifier): Maybe<OidcScope>;
567
591
  /**
568
592
  * @deprecated Use {@link OnCallModelTypeApiDetails} instead.
569
593
  */
@@ -1,6 +1,6 @@
1
1
  import { type Maybe } from '@dereekb/util';
2
2
  import { type NestContextCallableRequest } from '../function/nest';
3
- import { type OnCallFunctionType, type FirestoreModelType } from '@dereekb/firebase';
3
+ import { type OnCallFunctionType, type FirestoreModelType, type OidcScope } from '@dereekb/firebase';
4
4
  /**
5
5
  * Discriminator for the category of CRUD operation being asserted.
6
6
  *
@@ -34,6 +34,14 @@ export interface AssertModelCrudRequestFunctionContext<N, I = unknown> {
34
34
  * The optional sub-operation specifier (e.g., 'username', 'fromUpload').
35
35
  */
36
36
  readonly specifier: Maybe<string>;
37
+ /**
38
+ * The per-function required OIDC scope declared via `withApiDetails({ requiredScope })`, when present.
39
+ *
40
+ * Resolved from the handler's `_apiDetails` at dispatch and handed to the pre-assert so
41
+ * `oidcCallModelScopePreAssert` can require it additively alongside the per-verb `model.<call>`
42
+ * scope. `undefined` when the handler declares no extra scope.
43
+ */
44
+ readonly requiredScope?: Maybe<OidcScope>;
37
45
  }
38
46
  /**
39
47
  * Pre-assertion hook invoked before a model CRUD handler executes.
package/test/index.cjs.js CHANGED
@@ -4922,18 +4922,24 @@ function _ts_generator$1(thisArg, body) {
4922
4922
  }
4923
4923
  /**
4924
4924
  * Resolves the scopes string from config, or falls back to all registered scopes
4925
- * from the provider config — excluding any `adminOnlyScopes`.
4925
+ * from the provider config — excluding any `adminOnlyScopes` or provider-profile-gated scopes.
4926
4926
  *
4927
4927
  * Admin-only scopes (e.g. `token.service`) are dropped from the default "all scopes" request because
4928
4928
  * the provider hard-rejects them for non-admin users, and this default flow logs in an arbitrary
4929
4929
  * (often non-admin) user. Tests that need an admin-only scope pass it explicitly via `config.scopes`.
4930
4930
  *
4931
+ * Provider-profile-gated scopes (e.g. `lms`, `reports`) are dropped for the same reason: they are
4932
+ * unlocked only for a client an admin has assigned the corresponding profile to, and this flow creates
4933
+ * a fresh client with no profiles — so the consent unlock gate hard-rejects them (finishing the
4934
+ * interaction with `access_denied` and no `code`). Tests that need a gated scope create a client with
4935
+ * the profile and pass the scope explicitly via `config.scopes`.
4936
+ *
4931
4937
  * @param nestApp - Initialized NestJS application used to resolve {@link OidcAccountService} when no scopes override is given.
4932
4938
  * @param config - Optional flow config; when `config.scopes` is set, it is returned verbatim.
4933
4939
  * @returns The space-separated scope string to pass to the `/oidc/auth` endpoint.
4934
4940
  */ function resolveScopes(nestApp, config) {
4935
4941
  return _async_to_generator$1(function() {
4936
- var result, _providerConfig_adminOnlyScopes, accountService, providerConfig, adminOnlyScopes;
4942
+ var result, _providerConfig_adminOnlyScopes, _providerConfig_providerProfiles, accountService, providerConfig, adminOnlyScopes, profileGatedScopes;
4937
4943
  return _ts_generator$1(this, function(_state) {
4938
4944
  if (config === null || config === void 0 ? void 0 : config.scopes) {
4939
4945
  result = config.scopes;
@@ -4941,8 +4947,9 @@ function _ts_generator$1(thisArg, body) {
4941
4947
  accountService = nestApp.get(oidc.OidcAccountService);
4942
4948
  providerConfig = accountService.providerConfig;
4943
4949
  adminOnlyScopes = new Set((_providerConfig_adminOnlyScopes = providerConfig.adminOnlyScopes) !== null && _providerConfig_adminOnlyScopes !== void 0 ? _providerConfig_adminOnlyScopes : []);
4950
+ profileGatedScopes = firebase.scopesForOidcProviderProfiles((_providerConfig_providerProfiles = providerConfig.providerProfiles) !== null && _providerConfig_providerProfiles !== void 0 ? _providerConfig_providerProfiles : []);
4944
4951
  result = Object.keys(providerConfig.claims).filter(function(scope) {
4945
- return !adminOnlyScopes.has(scope);
4952
+ return !adminOnlyScopes.has(scope) && !profileGatedScopes.has(scope);
4946
4953
  }).join(' ');
4947
4954
  }
4948
4955
  return [
package/test/index.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { randomEmailFactory, randomPhoneNumberFactory, mapGetter, incrementingNumberFactory, asGetter, getValueFromGetter, cachedGetter, asArray, mapObjectMap, unixDateTimeSecondsNumberForNow } from '@dereekb/util';
2
2
  import { useTestContextFixture, AbstractChildTestContextFixture, testContextBuilder, AbstractTestContextFixture, useTestFunctionFixture, useTestFunctionMapFixture, ExpectedErrorOfSpecificTypeError } from '@dereekb/util/test';
3
3
  import { decode } from 'jsonwebtoken';
4
- import { flatFirestoreModelKey, twoWayFlatFirestoreModelKey, firestoreContextFactory, firebaseStorageContextFactory } from '@dereekb/firebase';
4
+ import { flatFirestoreModelKey, twoWayFlatFirestoreModelKey, firestoreContextFactory, firebaseStorageContextFactory, scopesForOidcProviderProfiles } from '@dereekb/firebase';
5
5
  import admin from 'firebase-admin';
6
6
  import functions from 'firebase-functions-test';
7
7
  import { makeTestingFirestoreDrivers, TestFirestoreInstance, TestFirestoreContextFixture, makeTestingFirebaseStorageDrivers, TestFirebaseStorageInstance, TestFirebaseStorageContextFixture, mockItemFirestoreCollection, testWithMockItemCollectionFixture, testWithMockItemStorageFixture } from '@dereekb/firebase/test';
@@ -4921,18 +4921,24 @@ function _ts_generator$1(thisArg, body) {
4921
4921
  }
4922
4922
  /**
4923
4923
  * Resolves the scopes string from config, or falls back to all registered scopes
4924
- * from the provider config — excluding any `adminOnlyScopes`.
4924
+ * from the provider config — excluding any `adminOnlyScopes` or provider-profile-gated scopes.
4925
4925
  *
4926
4926
  * Admin-only scopes (e.g. `token.service`) are dropped from the default "all scopes" request because
4927
4927
  * the provider hard-rejects them for non-admin users, and this default flow logs in an arbitrary
4928
4928
  * (often non-admin) user. Tests that need an admin-only scope pass it explicitly via `config.scopes`.
4929
4929
  *
4930
+ * Provider-profile-gated scopes (e.g. `lms`, `reports`) are dropped for the same reason: they are
4931
+ * unlocked only for a client an admin has assigned the corresponding profile to, and this flow creates
4932
+ * a fresh client with no profiles — so the consent unlock gate hard-rejects them (finishing the
4933
+ * interaction with `access_denied` and no `code`). Tests that need a gated scope create a client with
4934
+ * the profile and pass the scope explicitly via `config.scopes`.
4935
+ *
4930
4936
  * @param nestApp - Initialized NestJS application used to resolve {@link OidcAccountService} when no scopes override is given.
4931
4937
  * @param config - Optional flow config; when `config.scopes` is set, it is returned verbatim.
4932
4938
  * @returns The space-separated scope string to pass to the `/oidc/auth` endpoint.
4933
4939
  */ function resolveScopes(nestApp, config) {
4934
4940
  return _async_to_generator$1(function() {
4935
- var result, _providerConfig_adminOnlyScopes, accountService, providerConfig, adminOnlyScopes;
4941
+ var result, _providerConfig_adminOnlyScopes, _providerConfig_providerProfiles, accountService, providerConfig, adminOnlyScopes, profileGatedScopes;
4936
4942
  return _ts_generator$1(this, function(_state) {
4937
4943
  if (config === null || config === void 0 ? void 0 : config.scopes) {
4938
4944
  result = config.scopes;
@@ -4940,8 +4946,9 @@ function _ts_generator$1(thisArg, body) {
4940
4946
  accountService = nestApp.get(OidcAccountService);
4941
4947
  providerConfig = accountService.providerConfig;
4942
4948
  adminOnlyScopes = new Set((_providerConfig_adminOnlyScopes = providerConfig.adminOnlyScopes) !== null && _providerConfig_adminOnlyScopes !== void 0 ? _providerConfig_adminOnlyScopes : []);
4949
+ profileGatedScopes = scopesForOidcProviderProfiles((_providerConfig_providerProfiles = providerConfig.providerProfiles) !== null && _providerConfig_providerProfiles !== void 0 ? _providerConfig_providerProfiles : []);
4943
4950
  result = Object.keys(providerConfig.claims).filter(function(scope) {
4944
- return !adminOnlyScopes.has(scope);
4951
+ return !adminOnlyScopes.has(scope) && !profileGatedScopes.has(scope);
4945
4952
  }).join(' ');
4946
4953
  }
4947
4954
  return [
package/test/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/test",
3
- "version": "13.27.1",
3
+ "version": "13.29.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.27.1",
6
- "@dereekb/date": "13.27.1",
7
- "@dereekb/firebase": "13.27.1",
8
- "@dereekb/firebase-server": "13.27.1",
9
- "@dereekb/firebase-server/oidc": "13.27.1",
10
- "@dereekb/model": "13.27.1",
11
- "@dereekb/nestjs": "13.27.1",
12
- "@dereekb/rxjs": "13.27.1",
13
- "@dereekb/util": "13.27.1",
5
+ "@dereekb/analytics": "13.29.0",
6
+ "@dereekb/date": "13.29.0",
7
+ "@dereekb/firebase": "13.29.0",
8
+ "@dereekb/firebase-server": "13.29.0",
9
+ "@dereekb/firebase-server/oidc": "13.29.0",
10
+ "@dereekb/model": "13.29.0",
11
+ "@dereekb/nestjs": "13.29.0",
12
+ "@dereekb/rxjs": "13.29.0",
13
+ "@dereekb/util": "13.29.0",
14
14
  "@google-cloud/firestore": "^7.11.6",
15
15
  "@google-cloud/storage": "^7.19.0",
16
16
  "@nestjs/common": "^11.1.19",
@@ -23,7 +23,7 @@
23
23
  "supertest": "^7.2.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@dereekb/nestjs": "13.27.1"
26
+ "@dereekb/nestjs": "13.29.0"
27
27
  },
28
28
  "exports": {
29
29
  "./package.json": "./package.json",
@@ -1,5 +1,5 @@
1
1
  import { type INestApplication } from '@nestjs/common';
2
- import type { OidcTokenEndpointAuthMethod } from '@dereekb/firebase';
2
+ import { type OidcTokenEndpointAuthMethod } from '@dereekb/firebase';
3
3
  import { OidcClientService } from '@dereekb/firebase-server/oidc';
4
4
  /**
5
5
  * Configuration for {@link performFullOAuthFlow}.
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/twilio",
3
- "version": "13.27.1",
3
+ "version": "13.29.0",
4
4
  "peerDependencies": {
5
- "@dereekb/date": "13.27.1",
6
- "@dereekb/firebase": "13.27.1",
7
- "@dereekb/firebase-server": "13.27.1",
8
- "@dereekb/model": "13.27.1",
9
- "@dereekb/nestjs": "13.27.1",
10
- "@dereekb/rxjs": "13.27.1",
11
- "@dereekb/util": "13.27.1"
5
+ "@dereekb/date": "13.29.0",
6
+ "@dereekb/firebase": "13.29.0",
7
+ "@dereekb/firebase-server": "13.29.0",
8
+ "@dereekb/model": "13.29.0",
9
+ "@dereekb/nestjs": "13.29.0",
10
+ "@dereekb/rxjs": "13.29.0",
11
+ "@dereekb/util": "13.29.0"
12
12
  },
13
13
  "exports": {
14
14
  "./package.json": "./package.json",
package/zoho/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/zoho",
3
- "version": "13.27.1",
3
+ "version": "13.29.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.27.1",
6
- "@dereekb/date": "13.27.1",
7
- "@dereekb/model": "13.27.1",
8
- "@dereekb/nestjs": "13.27.1",
9
- "@dereekb/rxjs": "13.27.1",
10
- "@dereekb/firebase": "13.27.1",
11
- "@dereekb/util": "13.27.1",
12
- "@dereekb/zoho": "13.27.1"
5
+ "@dereekb/analytics": "13.29.0",
6
+ "@dereekb/date": "13.29.0",
7
+ "@dereekb/model": "13.29.0",
8
+ "@dereekb/nestjs": "13.29.0",
9
+ "@dereekb/rxjs": "13.29.0",
10
+ "@dereekb/firebase": "13.29.0",
11
+ "@dereekb/util": "13.29.0",
12
+ "@dereekb/zoho": "13.29.0"
13
13
  },
14
14
  "exports": {
15
15
  "./package.json": "./package.json",