@dereekb/firebase-server 13.10.8 → 13.11.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,15 +1,15 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/mailgun",
3
- "version": "13.10.8",
3
+ "version": "13.11.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.10.8",
6
- "@dereekb/firebase": "13.10.8",
7
- "@dereekb/firebase-server": "13.10.8",
8
- "@dereekb/date": "13.10.8",
9
- "@dereekb/nestjs": "13.10.8",
10
- "@dereekb/model": "13.10.8",
11
- "@dereekb/rxjs": "13.10.8",
12
- "@dereekb/util": "13.10.8"
5
+ "@dereekb/analytics": "13.11.0",
6
+ "@dereekb/firebase": "13.11.0",
7
+ "@dereekb/firebase-server": "13.11.0",
8
+ "@dereekb/date": "13.11.0",
9
+ "@dereekb/nestjs": "13.11.0",
10
+ "@dereekb/model": "13.11.0",
11
+ "@dereekb/rxjs": "13.11.0",
12
+ "@dereekb/util": "13.11.0"
13
13
  },
14
14
  "exports": {
15
15
  "./package.json": "./package.json",
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/model",
3
- "version": "13.10.8",
3
+ "version": "13.11.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.10.8",
6
- "@dereekb/date": "13.10.8",
7
- "@dereekb/firebase": "13.10.8",
8
- "@dereekb/firebase-server": "13.10.8",
9
- "@dereekb/model": "13.10.8",
10
- "@dereekb/nestjs": "13.10.8",
11
- "@dereekb/rxjs": "13.10.8",
12
- "@dereekb/util": "13.10.8",
5
+ "@dereekb/analytics": "13.11.0",
6
+ "@dereekb/date": "13.11.0",
7
+ "@dereekb/firebase": "13.11.0",
8
+ "@dereekb/firebase-server": "13.11.0",
9
+ "@dereekb/model": "13.11.0",
10
+ "@dereekb/nestjs": "13.11.0",
11
+ "@dereekb/rxjs": "13.11.0",
12
+ "@dereekb/util": "13.11.0",
13
13
  "@nestjs/common": "^11.1.19",
14
14
  "@nestjs/config": "^4.0.4",
15
15
  "archiver": "^7.0.1",
package/oidc/index.cjs.js CHANGED
@@ -4076,6 +4076,33 @@ var _logger = new common.Logger('applyOidcAuthMiddleware');
4076
4076
  _logger.debug("Applied OAuth bearer token middleware for paths: ".concat(protectedPaths.join(', ')));
4077
4077
  }
4078
4078
 
4079
+ // MARK: Scopes
4080
+ /**
4081
+ * Reads the set of OIDC scopes carried by a callable request's bearer token.
4082
+ *
4083
+ * The `ModelApiCallModelDispatchService` builds a synthetic `CallableRequest` with
4084
+ * `auth.token = oidcValidatedToken ?? {}`, so the OIDC scope string lives at
4085
+ * `request.auth.token.scope` for OIDC callers and is `undefined` for non-OIDC
4086
+ * (regular Firebase ID-token) callers.
4087
+ *
4088
+ * Returning `undefined` for non-OIDC callers lets callers (e.g. `oidcCallModelScopePreAssert`)
4089
+ * skip scope enforcement instead of falsely treating the missing claim as "no scopes granted".
4090
+ *
4091
+ * The parameter is intentionally typed as `unknown` because `DecodedIdToken` (Firebase
4092
+ * Auth's typed token shape) does not declare a `scope` field, while OIDC bearer-token
4093
+ * callers attach one at runtime. Reading is done defensively.
4094
+ *
4095
+ * @param request - The callable request as seen inside an `onCallModel` handler.
4096
+ * @returns A `Set<string>` of granted scopes, or `undefined` when the request carries no OIDC `scope` claim.
4097
+ */ function getOidcScopesFromRequest(request) {
4098
+ var _request_auth_token, _request_auth;
4099
+ var scope = request === null || request === void 0 ? void 0 : (_request_auth = request.auth) === null || _request_auth === void 0 ? void 0 : (_request_auth_token = _request_auth.token) === null || _request_auth_token === void 0 ? void 0 : _request_auth_token.scope;
4100
+ var result = typeof scope === 'string' ? new Set(scope.split(' ').filter(function(value) {
4101
+ return value.length > 0;
4102
+ })) : undefined;
4103
+ return result;
4104
+ }
4105
+
4079
4106
  function asyncGeneratorStep$3(gen, resolve, reject, _next, _throw, key, arg) {
4080
4107
  try {
4081
4108
  var info = gen[key](arg);
@@ -5163,16 +5190,38 @@ function _ts_generator$1(thisArg, body) {
5163
5190
  * The provider's callback strips the controller prefix from the URL so that
5164
5191
  * the provider sees paths relative to its issuer (e.g., `/auth` instead of `/oidc/auth`).
5165
5192
  */ exports.OidcProviderController = /*#__PURE__*/ function() {
5166
- function OidcProviderController(oidcService) {
5193
+ function OidcProviderController(oidcService, oidcProviderConfigService) {
5167
5194
  _class_call_check$1(this, OidcProviderController);
5168
5195
  _define_property$2(this, "oidcService", void 0);
5196
+ _define_property$2(this, "oidcProviderConfigService", void 0);
5169
5197
  _define_property$2(this, "_callback", void 0);
5170
5198
  this.oidcService = oidcService;
5199
+ this.oidcProviderConfigService = oidcProviderConfigService;
5171
5200
  this._callback = this.oidcService.getProvider().then(function(p) {
5172
5201
  return p.callback();
5173
5202
  });
5174
5203
  }
5175
5204
  _create_class$1(OidcProviderController, [
5205
+ {
5206
+ /**
5207
+ * GET /oidc/login/client
5208
+ *
5209
+ * Convenience redirect from the API issuer path back to the frontend app's
5210
+ * OAuth login page. Lets a user who lands on the API host get bounced to the
5211
+ * client-side login UI. Any incoming query string is forwarded so flow params
5212
+ * (e.g., `uid`, `state`) survive the redirect, merged with any params already
5213
+ * baked into `appLoginUrl`.
5214
+ *
5215
+ * @param req - Inbound Express request; only `originalUrl` is used so any incoming query string can be forwarded to the redirect target.
5216
+ * @param res - Express response used to issue the 302 redirect to the configured `appLoginUrl`.
5217
+ */ key: "redirectToClientLogin",
5218
+ value: function redirectToClientLogin(req, res) {
5219
+ res.redirect(mergeQueryParamsFromOriginalUrl({
5220
+ baseUrl: this.oidcProviderConfigService.appLoginUrl,
5221
+ originalUrl: req.originalUrl
5222
+ }));
5223
+ }
5224
+ },
5176
5225
  {
5177
5226
  key: "handleOidcRequest",
5178
5227
  value: function handleOidcRequest(req, res) {
@@ -5201,6 +5250,11 @@ function _ts_generator$1(thisArg, body) {
5201
5250
  ]);
5202
5251
  return OidcProviderController;
5203
5252
  }();
5253
+ __decorate([
5254
+ common.Get('login/client'),
5255
+ __param(0, common.Req()),
5256
+ __param(1, common.Res())
5257
+ ], exports.OidcProviderController.prototype, "redirectToClientLogin", null);
5204
5258
  __decorate([
5205
5259
  common.All('{*path}'),
5206
5260
  __param(0, common.Req()),
@@ -5208,8 +5262,31 @@ __decorate([
5208
5262
  ], exports.OidcProviderController.prototype, "handleOidcRequest", null);
5209
5263
  exports.OidcProviderController = __decorate([
5210
5264
  common.Controller('oidc'),
5211
- __param(0, common.Inject(exports.OidcService))
5265
+ __param(0, common.Inject(exports.OidcService)),
5266
+ __param(1, common.Inject(exports.OidcProviderConfigService))
5212
5267
  ], exports.OidcProviderController);
5268
+ /**
5269
+ * Merges any query string present on `originalUrl` into `baseUrl`, preserving any params already
5270
+ * baked into `baseUrl`. Bare-string concatenation would produce malformed URLs (`?foo=1?bar=2`)
5271
+ * when `baseUrl` already contains a `?`.
5272
+ *
5273
+ * @param input - The base/original URL pair to merge.
5274
+ * @param input.baseUrl - The destination URL whose query string should be augmented with any incoming params.
5275
+ * @param input.originalUrl - The inbound request URL whose query string is appended onto `baseUrl`.
5276
+ * @returns The base URL with the original URL's query string appended using the appropriate `?`/`&` separator.
5277
+ */ function mergeQueryParamsFromOriginalUrl(input) {
5278
+ var queryIndex = input.originalUrl.indexOf('?');
5279
+ if (queryIndex < 0) {
5280
+ return input.baseUrl;
5281
+ }
5282
+ var incomingSearch = input.originalUrl.slice(queryIndex + 1);
5283
+ if (incomingSearch.length === 0) {
5284
+ return input.baseUrl;
5285
+ }
5286
+ var baseQueryIndex = input.baseUrl.indexOf('?');
5287
+ var separator = baseQueryIndex < 0 ? '?' : '&';
5288
+ return "".concat(input.baseUrl).concat(separator).concat(incomingSearch);
5289
+ }
5213
5290
 
5214
5291
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
5215
5292
  try {
@@ -5735,6 +5812,90 @@ function _unsupported_iterable_to_array(o, minLen) {
5735
5812
  };
5736
5813
  }
5737
5814
 
5815
+ /**
5816
+ * Prefix shared by every callModel OIDC scope (e.g., `model.create`).
5817
+ *
5818
+ * Kept stable so OAuth consent screens render consistent labels and so
5819
+ * future per-resource scopes (e.g., `model.create:profile`) compose cleanly.
5820
+ */ var CALL_MODEL_OIDC_SCOPE_PREFIX = 'model.';
5821
+ /**
5822
+ * Canonical CRUD scopes enforced on the `callModel` API.
5823
+ *
5824
+ * Each scope corresponds 1:1 to a {@link KnownOnCallFunctionType}; see
5825
+ * {@link CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE}.
5826
+ */ var CALL_MODEL_OIDC_SCOPES = [
5827
+ 'model.create',
5828
+ 'model.read',
5829
+ 'model.update',
5830
+ 'model.delete',
5831
+ 'model.query'
5832
+ ];
5833
+ /**
5834
+ * Maps each known CRUD call type to the scope an OIDC token must carry to invoke it.
5835
+ */ var CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE = {
5836
+ create: 'model.create',
5837
+ read: 'model.read',
5838
+ update: 'model.update',
5839
+ delete: 'model.delete',
5840
+ query: 'model.query'
5841
+ };
5842
+ /**
5843
+ * Resolves the OIDC scope that an OIDC-authenticated caller must hold to invoke
5844
+ * the given callModel `call` type.
5845
+ *
5846
+ * Returns `undefined` for non-CRUD (custom) call types so that scope enforcement
5847
+ * is opt-in for app-specific verbs — apps can still gate them via their own
5848
+ * `preAssert` if needed.
5849
+ *
5850
+ * @param call - The CRUD call type from {@link OnCallTypedModelParams.call}.
5851
+ * @returns The required scope, or `undefined` if `call` is not one of the known CRUD verbs.
5852
+ */ function callModelOidcScopeForCallType(call) {
5853
+ var result = call == null ? undefined : CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE[call];
5854
+ return result;
5855
+ }
5856
+ /**
5857
+ * Error code used when an OIDC-authenticated caller is missing the required
5858
+ * `model.*` scope for a callModel CRUD operation.
5859
+ */ var CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE = 'CALL_MODEL_MISSING_OIDC_SCOPE';
5860
+ /**
5861
+ * Builds a {@link AssertModelCrudRequestFunction} that rejects callModel requests
5862
+ * lacking the OIDC scope mapped from the call type.
5863
+ *
5864
+ * Bypasses the check (no-op) when the request is not OIDC-authenticated — i.e.
5865
+ * when {@link getOidcScopesFromRequest} returns `undefined` because there is no
5866
+ * `auth.token.scope` claim on the request. Regular Firebase ID-token callers are
5867
+ * unaffected and continue to be gated by auth roles.
5868
+ *
5869
+ * Bypasses the check for custom (non-CRUD) call types so app-specific verbs
5870
+ * remain unrestricted unless an app wires its own assertion.
5871
+ *
5872
+ * Wire as the `preAssert` of {@link onCallModel}.
5873
+ *
5874
+ * @returns The pre-assertion function ready to plug into `OnCallModelConfig.preAssert`.
5875
+ */ function oidcCallModelScopePreAssert() {
5876
+ var fn = function fn(context) {
5877
+ var requiredScope = callModelOidcScopeForCallType(context.call);
5878
+ var scopes = requiredScope == null ? undefined : getOidcScopesFromRequest(context.request);
5879
+ var isMissingScope = requiredScope != null && scopes != null && !scopes.has(requiredScope);
5880
+ if (isMissingScope) {
5881
+ throw firebaseServer.forbiddenError({
5882
+ status: 403,
5883
+ code: CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE,
5884
+ message: "Missing required OIDC scope for callModel: ".concat(requiredScope),
5885
+ data: {
5886
+ requiredScope: requiredScope,
5887
+ call: context.call
5888
+ }
5889
+ });
5890
+ }
5891
+ };
5892
+ return fn;
5893
+ }
5894
+
5895
+ exports.CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE = CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE;
5896
+ exports.CALL_MODEL_OIDC_SCOPES = CALL_MODEL_OIDC_SCOPES;
5897
+ exports.CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE = CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE;
5898
+ exports.CALL_MODEL_OIDC_SCOPE_PREFIX = CALL_MODEL_OIDC_SCOPE_PREFIX;
5738
5899
  exports.DEFAULT_APP_OAUTH_CONSENT_PATH_PART = DEFAULT_APP_OAUTH_CONSENT_PATH_PART;
5739
5900
  exports.DEFAULT_APP_OAUTH_INTERACTION_PATH = DEFAULT_APP_OAUTH_INTERACTION_PATH;
5740
5901
  exports.DEFAULT_APP_OAUTH_LOGIN_PATH_PART = DEFAULT_APP_OAUTH_LOGIN_PATH_PART;
@@ -5765,15 +5926,18 @@ exports.OidcServerFirestoreCollections = OidcServerFirestoreCollections;
5765
5926
  exports.activeJwksKeysQuery = activeJwksKeysQuery;
5766
5927
  exports.appOidcModelModuleMetadata = appOidcModelModuleMetadata;
5767
5928
  exports.applyOidcAuthMiddleware = applyOidcAuthMiddleware;
5929
+ exports.callModelOidcScopeForCallType = callModelOidcScopeForCallType;
5768
5930
  exports.createAdapterFactory = createAdapterFactory;
5769
5931
  exports.createOidcClientFactory = createOidcClientFactory;
5770
5932
  exports.deleteOidcClientFactory = deleteOidcClientFactory;
5933
+ exports.getOidcScopesFromRequest = getOidcScopesFromRequest;
5771
5934
  exports.jwksKeyCollectionReference = jwksKeyCollectionReference;
5772
5935
  exports.jwksKeyConverter = jwksKeyConverter;
5773
5936
  exports.jwksKeyFirestoreCollection = jwksKeyFirestoreCollection;
5774
5937
  exports.jwksKeyIdentity = jwksKeyIdentity;
5775
5938
  exports.jwksKeysWithStatusQuery = jwksKeysWithStatusQuery;
5776
5939
  exports.nonRetiredJwksKeysQuery = nonRetiredJwksKeysQuery;
5940
+ exports.oidcCallModelScopePreAssert = oidcCallModelScopePreAssert;
5777
5941
  exports.oidcFirestoreCollectionsFactory = oidcFirestoreCollectionsFactory;
5778
5942
  exports.oidcModelServerActions = oidcModelServerActions;
5779
5943
  exports.oidcModelServerActionsFactory = oidcModelServerActionsFactory;
package/oidc/index.esm.js CHANGED
@@ -3,7 +3,7 @@ import { generateKeyPairSync, randomBytes } from 'node:crypto';
3
3
  import { resolveEncryptionKey, encryptValue, decryptValue, createAesStringEncryptionProvider, isValidAES256GCMEncryptionSecret } from '@dereekb/nestjs';
4
4
  import { where, iterateFirestoreDocumentSnapshotPairs, firestoreModelIdentity, snapshotConverterFunctions, optionalFirestoreDate, firestoreDate, firestoreEnum, firestorePassThroughField, AbstractFirestoreDocument, createOidcClientParamsType, deleteOidcClientParamsType, rotateOidcClientSecretParamsType, updateOidcClientParamsType, firestoreModelKey, oidcEntryIdentity, OIDC_ENTRY_CLIENT_TYPE, oidcEntriesByUserCodeQuery, oidcEntriesByUidQuery, oidcEntriesByGrantIdQuery, oidcEntryFirestoreCollection } from '@dereekb/firebase';
5
5
  import { cachedGetter, unixDateTimeSecondsNumberToDate, unixDateTimeSecondsNumberForNow, selectiveFieldEncryptor, filterUndefinedValues, websiteUrlFromPaths, firstValue, hasHttpPrefix } from '@dereekb/util';
6
- import { firestoreEncryptedField, firebaseServerActionsContext, FirebaseServerEnvService, FIREBASE_FIRESTORE_CONTEXT_TOKEN, FirebaseServerFirestoreContextModule } from '@dereekb/firebase-server';
6
+ import { firestoreEncryptedField, firebaseServerActionsContext, FirebaseServerEnvService, FIREBASE_FIRESTORE_CONTEXT_TOKEN, FirebaseServerFirestoreContextModule, forbiddenError } from '@dereekb/firebase-server';
7
7
  import { nanoid } from 'nanoid';
8
8
  import { makeUrlSearchParamsString } from '@dereekb/util/fetch';
9
9
  import { ConfigService, ConfigModule } from '@nestjs/config';
@@ -4074,6 +4074,33 @@ var _logger = new Logger('applyOidcAuthMiddleware');
4074
4074
  _logger.debug("Applied OAuth bearer token middleware for paths: ".concat(protectedPaths.join(', ')));
4075
4075
  }
4076
4076
 
4077
+ // MARK: Scopes
4078
+ /**
4079
+ * Reads the set of OIDC scopes carried by a callable request's bearer token.
4080
+ *
4081
+ * The `ModelApiCallModelDispatchService` builds a synthetic `CallableRequest` with
4082
+ * `auth.token = oidcValidatedToken ?? {}`, so the OIDC scope string lives at
4083
+ * `request.auth.token.scope` for OIDC callers and is `undefined` for non-OIDC
4084
+ * (regular Firebase ID-token) callers.
4085
+ *
4086
+ * Returning `undefined` for non-OIDC callers lets callers (e.g. `oidcCallModelScopePreAssert`)
4087
+ * skip scope enforcement instead of falsely treating the missing claim as "no scopes granted".
4088
+ *
4089
+ * The parameter is intentionally typed as `unknown` because `DecodedIdToken` (Firebase
4090
+ * Auth's typed token shape) does not declare a `scope` field, while OIDC bearer-token
4091
+ * callers attach one at runtime. Reading is done defensively.
4092
+ *
4093
+ * @param request - The callable request as seen inside an `onCallModel` handler.
4094
+ * @returns A `Set<string>` of granted scopes, or `undefined` when the request carries no OIDC `scope` claim.
4095
+ */ function getOidcScopesFromRequest(request) {
4096
+ var _request_auth_token, _request_auth;
4097
+ var scope = request === null || request === void 0 ? void 0 : (_request_auth = request.auth) === null || _request_auth === void 0 ? void 0 : (_request_auth_token = _request_auth.token) === null || _request_auth_token === void 0 ? void 0 : _request_auth_token.scope;
4098
+ var result = typeof scope === 'string' ? new Set(scope.split(' ').filter(function(value) {
4099
+ return value.length > 0;
4100
+ })) : undefined;
4101
+ return result;
4102
+ }
4103
+
4077
4104
  function asyncGeneratorStep$3(gen, resolve, reject, _next, _throw, key, arg) {
4078
4105
  try {
4079
4106
  var info = gen[key](arg);
@@ -5161,16 +5188,38 @@ function _ts_generator$1(thisArg, body) {
5161
5188
  * The provider's callback strips the controller prefix from the URL so that
5162
5189
  * the provider sees paths relative to its issuer (e.g., `/auth` instead of `/oidc/auth`).
5163
5190
  */ var OidcProviderController = /*#__PURE__*/ function() {
5164
- function OidcProviderController(oidcService) {
5191
+ function OidcProviderController(oidcService, oidcProviderConfigService) {
5165
5192
  _class_call_check$1(this, OidcProviderController);
5166
5193
  _define_property$2(this, "oidcService", void 0);
5194
+ _define_property$2(this, "oidcProviderConfigService", void 0);
5167
5195
  _define_property$2(this, "_callback", void 0);
5168
5196
  this.oidcService = oidcService;
5197
+ this.oidcProviderConfigService = oidcProviderConfigService;
5169
5198
  this._callback = this.oidcService.getProvider().then(function(p) {
5170
5199
  return p.callback();
5171
5200
  });
5172
5201
  }
5173
5202
  _create_class$1(OidcProviderController, [
5203
+ {
5204
+ /**
5205
+ * GET /oidc/login/client
5206
+ *
5207
+ * Convenience redirect from the API issuer path back to the frontend app's
5208
+ * OAuth login page. Lets a user who lands on the API host get bounced to the
5209
+ * client-side login UI. Any incoming query string is forwarded so flow params
5210
+ * (e.g., `uid`, `state`) survive the redirect, merged with any params already
5211
+ * baked into `appLoginUrl`.
5212
+ *
5213
+ * @param req - Inbound Express request; only `originalUrl` is used so any incoming query string can be forwarded to the redirect target.
5214
+ * @param res - Express response used to issue the 302 redirect to the configured `appLoginUrl`.
5215
+ */ key: "redirectToClientLogin",
5216
+ value: function redirectToClientLogin(req, res) {
5217
+ res.redirect(mergeQueryParamsFromOriginalUrl({
5218
+ baseUrl: this.oidcProviderConfigService.appLoginUrl,
5219
+ originalUrl: req.originalUrl
5220
+ }));
5221
+ }
5222
+ },
5174
5223
  {
5175
5224
  key: "handleOidcRequest",
5176
5225
  value: function handleOidcRequest(req, res) {
@@ -5199,6 +5248,11 @@ function _ts_generator$1(thisArg, body) {
5199
5248
  ]);
5200
5249
  return OidcProviderController;
5201
5250
  }();
5251
+ __decorate([
5252
+ Get('login/client'),
5253
+ __param(0, Req()),
5254
+ __param(1, Res())
5255
+ ], OidcProviderController.prototype, "redirectToClientLogin", null);
5202
5256
  __decorate([
5203
5257
  All('{*path}'),
5204
5258
  __param(0, Req()),
@@ -5206,8 +5260,31 @@ __decorate([
5206
5260
  ], OidcProviderController.prototype, "handleOidcRequest", null);
5207
5261
  OidcProviderController = __decorate([
5208
5262
  Controller('oidc'),
5209
- __param(0, Inject(OidcService))
5263
+ __param(0, Inject(OidcService)),
5264
+ __param(1, Inject(OidcProviderConfigService))
5210
5265
  ], OidcProviderController);
5266
+ /**
5267
+ * Merges any query string present on `originalUrl` into `baseUrl`, preserving any params already
5268
+ * baked into `baseUrl`. Bare-string concatenation would produce malformed URLs (`?foo=1?bar=2`)
5269
+ * when `baseUrl` already contains a `?`.
5270
+ *
5271
+ * @param input - The base/original URL pair to merge.
5272
+ * @param input.baseUrl - The destination URL whose query string should be augmented with any incoming params.
5273
+ * @param input.originalUrl - The inbound request URL whose query string is appended onto `baseUrl`.
5274
+ * @returns The base URL with the original URL's query string appended using the appropriate `?`/`&` separator.
5275
+ */ function mergeQueryParamsFromOriginalUrl(input) {
5276
+ var queryIndex = input.originalUrl.indexOf('?');
5277
+ if (queryIndex < 0) {
5278
+ return input.baseUrl;
5279
+ }
5280
+ var incomingSearch = input.originalUrl.slice(queryIndex + 1);
5281
+ if (incomingSearch.length === 0) {
5282
+ return input.baseUrl;
5283
+ }
5284
+ var baseQueryIndex = input.baseUrl.indexOf('?');
5285
+ var separator = baseQueryIndex < 0 ? '?' : '&';
5286
+ return "".concat(input.baseUrl).concat(separator).concat(incomingSearch);
5287
+ }
5211
5288
 
5212
5289
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
5213
5290
  try {
@@ -5733,4 +5810,84 @@ function _unsupported_iterable_to_array(o, minLen) {
5733
5810
  };
5734
5811
  }
5735
5812
 
5736
- export { DEFAULT_APP_OAUTH_CONSENT_PATH_PART, DEFAULT_APP_OAUTH_INTERACTION_PATH, DEFAULT_APP_OAUTH_LOGIN_PATH_PART, DEFAULT_OIDC_CODE_CHALLENGE_METHODS, DEFAULT_OIDC_ID_TOKEN_SIGNING_ALG_VALUES, DEFAULT_OIDC_ISSUER_PATH, DEFAULT_OIDC_ROUTES, DEFAULT_OIDC_SUBJECT_TYPES, DEFAULT_OIDC_TOKEN_ENDPOINT_AUTH_METHODS, DEFAULT_OIDC_TOKEN_LIFETIMES, DEFAULT_ROTATED_KEY_MAX_AGE, FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE, JwksFirestoreCollections, JwksKeyDocument, JwksService, JwksServiceConfig, JwksServiceStorageConfig, OIDC_ENCRYPTED_PAYLOAD_FIELDS, OIDC_JWKS_ENCRYPTION_SECRET_ENV_KEY, OidcAccountService, OidcAccountServiceDelegate, OidcAccountServiceUserContext, OidcAuth, OidcAuthBearerTokenMiddleware, OidcAuthMiddlewareConfig, OidcClientService, OidcEncryptionService, OidcInteractionController, OidcInteractionService, OidcModelServerActions, OidcModuleConfig, OidcProviderConfigService, OidcProviderController, OidcServerFirestoreCollections, OidcService, OidcWellKnownController, activeJwksKeysQuery, appOidcModelModuleMetadata, applyOidcAuthMiddleware, createAdapterFactory, createOidcClientFactory, deleteOidcClientFactory, jwksKeyCollectionReference, jwksKeyConverter, jwksKeyFirestoreCollection, jwksKeyIdentity, jwksKeysWithStatusQuery, nonRetiredJwksKeysQuery, oidcFirestoreCollectionsFactory, oidcModelServerActions, oidcModelServerActionsFactory, oidcModuleConfigFactory, oidcModuleMetadata, rotateOidcClientSecretFactory, rotatedJwksKeysQuery, updateOidcClientFactory };
5813
+ /**
5814
+ * Prefix shared by every callModel OIDC scope (e.g., `model.create`).
5815
+ *
5816
+ * Kept stable so OAuth consent screens render consistent labels and so
5817
+ * future per-resource scopes (e.g., `model.create:profile`) compose cleanly.
5818
+ */ var CALL_MODEL_OIDC_SCOPE_PREFIX = 'model.';
5819
+ /**
5820
+ * Canonical CRUD scopes enforced on the `callModel` API.
5821
+ *
5822
+ * Each scope corresponds 1:1 to a {@link KnownOnCallFunctionType}; see
5823
+ * {@link CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE}.
5824
+ */ var CALL_MODEL_OIDC_SCOPES = [
5825
+ 'model.create',
5826
+ 'model.read',
5827
+ 'model.update',
5828
+ 'model.delete',
5829
+ 'model.query'
5830
+ ];
5831
+ /**
5832
+ * Maps each known CRUD call type to the scope an OIDC token must carry to invoke it.
5833
+ */ var CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE = {
5834
+ create: 'model.create',
5835
+ read: 'model.read',
5836
+ update: 'model.update',
5837
+ delete: 'model.delete',
5838
+ query: 'model.query'
5839
+ };
5840
+ /**
5841
+ * Resolves the OIDC scope that an OIDC-authenticated caller must hold to invoke
5842
+ * the given callModel `call` type.
5843
+ *
5844
+ * Returns `undefined` for non-CRUD (custom) call types so that scope enforcement
5845
+ * is opt-in for app-specific verbs — apps can still gate them via their own
5846
+ * `preAssert` if needed.
5847
+ *
5848
+ * @param call - The CRUD call type from {@link OnCallTypedModelParams.call}.
5849
+ * @returns The required scope, or `undefined` if `call` is not one of the known CRUD verbs.
5850
+ */ function callModelOidcScopeForCallType(call) {
5851
+ var result = call == null ? undefined : CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE[call];
5852
+ return result;
5853
+ }
5854
+ /**
5855
+ * Error code used when an OIDC-authenticated caller is missing the required
5856
+ * `model.*` scope for a callModel CRUD operation.
5857
+ */ var CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE = 'CALL_MODEL_MISSING_OIDC_SCOPE';
5858
+ /**
5859
+ * Builds a {@link AssertModelCrudRequestFunction} that rejects callModel requests
5860
+ * lacking the OIDC scope mapped from the call type.
5861
+ *
5862
+ * Bypasses the check (no-op) when the request is not OIDC-authenticated — i.e.
5863
+ * when {@link getOidcScopesFromRequest} returns `undefined` because there is no
5864
+ * `auth.token.scope` claim on the request. Regular Firebase ID-token callers are
5865
+ * unaffected and continue to be gated by auth roles.
5866
+ *
5867
+ * Bypasses the check for custom (non-CRUD) call types so app-specific verbs
5868
+ * remain unrestricted unless an app wires its own assertion.
5869
+ *
5870
+ * Wire as the `preAssert` of {@link onCallModel}.
5871
+ *
5872
+ * @returns The pre-assertion function ready to plug into `OnCallModelConfig.preAssert`.
5873
+ */ function oidcCallModelScopePreAssert() {
5874
+ var fn = function fn(context) {
5875
+ var requiredScope = callModelOidcScopeForCallType(context.call);
5876
+ var scopes = requiredScope == null ? undefined : getOidcScopesFromRequest(context.request);
5877
+ var isMissingScope = requiredScope != null && scopes != null && !scopes.has(requiredScope);
5878
+ if (isMissingScope) {
5879
+ throw forbiddenError({
5880
+ status: 403,
5881
+ code: CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE,
5882
+ message: "Missing required OIDC scope for callModel: ".concat(requiredScope),
5883
+ data: {
5884
+ requiredScope: requiredScope,
5885
+ call: context.call
5886
+ }
5887
+ });
5888
+ }
5889
+ };
5890
+ return fn;
5891
+ }
5892
+
5893
+ export { CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE, CALL_MODEL_OIDC_SCOPES, CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE, CALL_MODEL_OIDC_SCOPE_PREFIX, DEFAULT_APP_OAUTH_CONSENT_PATH_PART, DEFAULT_APP_OAUTH_INTERACTION_PATH, DEFAULT_APP_OAUTH_LOGIN_PATH_PART, DEFAULT_OIDC_CODE_CHALLENGE_METHODS, DEFAULT_OIDC_ID_TOKEN_SIGNING_ALG_VALUES, DEFAULT_OIDC_ISSUER_PATH, DEFAULT_OIDC_ROUTES, DEFAULT_OIDC_SUBJECT_TYPES, DEFAULT_OIDC_TOKEN_ENDPOINT_AUTH_METHODS, DEFAULT_OIDC_TOKEN_LIFETIMES, DEFAULT_ROTATED_KEY_MAX_AGE, FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE, JwksFirestoreCollections, JwksKeyDocument, JwksService, JwksServiceConfig, JwksServiceStorageConfig, OIDC_ENCRYPTED_PAYLOAD_FIELDS, OIDC_JWKS_ENCRYPTION_SECRET_ENV_KEY, OidcAccountService, OidcAccountServiceDelegate, OidcAccountServiceUserContext, OidcAuth, OidcAuthBearerTokenMiddleware, OidcAuthMiddlewareConfig, OidcClientService, OidcEncryptionService, OidcInteractionController, OidcInteractionService, OidcModelServerActions, OidcModuleConfig, OidcProviderConfigService, OidcProviderController, OidcServerFirestoreCollections, OidcService, OidcWellKnownController, activeJwksKeysQuery, appOidcModelModuleMetadata, applyOidcAuthMiddleware, callModelOidcScopeForCallType, createAdapterFactory, createOidcClientFactory, deleteOidcClientFactory, getOidcScopesFromRequest, jwksKeyCollectionReference, jwksKeyConverter, jwksKeyFirestoreCollection, jwksKeyIdentity, jwksKeysWithStatusQuery, nonRetiredJwksKeysQuery, oidcCallModelScopePreAssert, oidcFirestoreCollectionsFactory, oidcModelServerActions, oidcModelServerActionsFactory, oidcModuleConfigFactory, oidcModuleMetadata, rotateOidcClientSecretFactory, rotatedJwksKeysQuery, updateOidcClientFactory };
package/oidc/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/oidc",
3
- "version": "13.10.8",
3
+ "version": "13.11.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.10.8",
6
- "@dereekb/date": "13.10.8",
7
- "@dereekb/firebase": "13.10.8",
8
- "@dereekb/firebase-server": "13.10.8",
9
- "@dereekb/model": "13.10.8",
10
- "@dereekb/nestjs": "13.10.8",
11
- "@dereekb/rxjs": "13.10.8",
12
- "@dereekb/util": "13.10.8",
13
- "@dereekb/zoho": "13.10.8",
5
+ "@dereekb/analytics": "13.11.0",
6
+ "@dereekb/date": "13.11.0",
7
+ "@dereekb/firebase": "13.11.0",
8
+ "@dereekb/firebase-server": "13.11.0",
9
+ "@dereekb/model": "13.11.0",
10
+ "@dereekb/nestjs": "13.11.0",
11
+ "@dereekb/rxjs": "13.11.0",
12
+ "@dereekb/util": "13.11.0",
13
+ "@dereekb/zoho": "13.11.0",
14
14
  "@nestjs/common": "^11.1.19",
15
15
  "@nestjs/config": "^4.0.4",
16
16
  "express": "^5.2.1",
@@ -1,5 +1,6 @@
1
1
  import { type Request, type Response } from 'express';
2
2
  import { OidcService } from '../service/oidc.service';
3
+ import { OidcProviderConfigService } from '../service/oidc.config.service';
3
4
  /**
4
5
  * Catch-all controller that proxies requests to the oidc-provider callback.
5
6
  *
@@ -12,7 +13,21 @@ import { OidcService } from '../service/oidc.service';
12
13
  */
13
14
  export declare class OidcProviderController {
14
15
  private readonly oidcService;
16
+ private readonly oidcProviderConfigService;
15
17
  private _callback;
16
- constructor(oidcService: OidcService);
18
+ constructor(oidcService: OidcService, oidcProviderConfigService: OidcProviderConfigService);
19
+ /**
20
+ * GET /oidc/login/client
21
+ *
22
+ * Convenience redirect from the API issuer path back to the frontend app's
23
+ * OAuth login page. Lets a user who lands on the API host get bounced to the
24
+ * client-side login UI. Any incoming query string is forwarded so flow params
25
+ * (e.g., `uid`, `state`) survive the redirect, merged with any params already
26
+ * baked into `appLoginUrl`.
27
+ *
28
+ * @param req - Inbound Express request; only `originalUrl` is used so any incoming query string can be forwarded to the redirect target.
29
+ * @param res - Express response used to issue the 302 redirect to the configured `appLoginUrl`.
30
+ */
31
+ redirectToClientLogin(req: Request, res: Response): void;
17
32
  handleOidcRequest(req: Request, res: Response): Promise<void>;
18
33
  }
@@ -3,4 +3,5 @@ export * from './model';
3
3
  export * from './oidc.config';
4
4
  export * from './controller';
5
5
  export * from './oidc.module';
6
+ export * from './scope';
6
7
  export * from './service';
@@ -0,0 +1,59 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type KnownOnCallFunctionType, type OnCallFunctionType, type OnCallTypedModelParams } from '@dereekb/firebase';
3
+ import { type AssertModelCrudRequestFunction } from '@dereekb/firebase-server';
4
+ /**
5
+ * Prefix shared by every callModel OIDC scope (e.g., `model.create`).
6
+ *
7
+ * Kept stable so OAuth consent screens render consistent labels and so
8
+ * future per-resource scopes (e.g., `model.create:profile`) compose cleanly.
9
+ */
10
+ export declare const CALL_MODEL_OIDC_SCOPE_PREFIX = "model.";
11
+ /**
12
+ * Canonical CRUD scopes enforced on the `callModel` API.
13
+ *
14
+ * Each scope corresponds 1:1 to a {@link KnownOnCallFunctionType}; see
15
+ * {@link CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE}.
16
+ */
17
+ export declare const CALL_MODEL_OIDC_SCOPES: readonly ["model.create", "model.read", "model.update", "model.delete", "model.query"];
18
+ /**
19
+ * Union of the five canonical callModel CRUD scope strings.
20
+ */
21
+ export type CallModelOidcScope = (typeof CALL_MODEL_OIDC_SCOPES)[number];
22
+ /**
23
+ * Maps each known CRUD call type to the scope an OIDC token must carry to invoke it.
24
+ */
25
+ export declare const CALL_MODEL_OIDC_SCOPE_FOR_CALL_TYPE: Readonly<Record<KnownOnCallFunctionType, CallModelOidcScope>>;
26
+ /**
27
+ * Resolves the OIDC scope that an OIDC-authenticated caller must hold to invoke
28
+ * the given callModel `call` type.
29
+ *
30
+ * Returns `undefined` for non-CRUD (custom) call types so that scope enforcement
31
+ * is opt-in for app-specific verbs — apps can still gate them via their own
32
+ * `preAssert` if needed.
33
+ *
34
+ * @param call - The CRUD call type from {@link OnCallTypedModelParams.call}.
35
+ * @returns The required scope, or `undefined` if `call` is not one of the known CRUD verbs.
36
+ */
37
+ export declare function callModelOidcScopeForCallType(call: Maybe<OnCallFunctionType>): Maybe<CallModelOidcScope>;
38
+ /**
39
+ * Error code used when an OIDC-authenticated caller is missing the required
40
+ * `model.*` scope for a callModel CRUD operation.
41
+ */
42
+ export declare const CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE = "CALL_MODEL_MISSING_OIDC_SCOPE";
43
+ /**
44
+ * Builds a {@link AssertModelCrudRequestFunction} that rejects callModel requests
45
+ * lacking the OIDC scope mapped from the call type.
46
+ *
47
+ * Bypasses the check (no-op) when the request is not OIDC-authenticated — i.e.
48
+ * when {@link getOidcScopesFromRequest} returns `undefined` because there is no
49
+ * `auth.token.scope` claim on the request. Regular Firebase ID-token callers are
50
+ * unaffected and continue to be gated by auth roles.
51
+ *
52
+ * Bypasses the check for custom (non-CRUD) call types so app-specific verbs
53
+ * remain unrestricted unless an app wires its own assertion.
54
+ *
55
+ * Wire as the `preAssert` of {@link onCallModel}.
56
+ *
57
+ * @returns The pre-assertion function ready to plug into `OnCallModelConfig.preAssert`.
58
+ */
59
+ export declare function oidcCallModelScopePreAssert(): AssertModelCrudRequestFunction<unknown, OnCallTypedModelParams>;
@@ -1,3 +1,4 @@
1
+ import { type Maybe } from '@dereekb/util';
1
2
  import { type OidcEntryClientId } from '@dereekb/firebase';
2
3
  import { type FirebaseServerAuthData, type FirebaseServerAuthenticatedRequest } from '@dereekb/firebase-server';
3
4
  /**
@@ -24,3 +25,22 @@ export interface OidcAuthData extends FirebaseServerAuthData {
24
25
  * successful bearer token verification.
25
26
  */
26
27
  export type OidcAuthenticatedRequest = FirebaseServerAuthenticatedRequest<OidcAuthData>;
28
+ /**
29
+ * Reads the set of OIDC scopes carried by a callable request's bearer token.
30
+ *
31
+ * The `ModelApiCallModelDispatchService` builds a synthetic `CallableRequest` with
32
+ * `auth.token = oidcValidatedToken ?? {}`, so the OIDC scope string lives at
33
+ * `request.auth.token.scope` for OIDC callers and is `undefined` for non-OIDC
34
+ * (regular Firebase ID-token) callers.
35
+ *
36
+ * Returning `undefined` for non-OIDC callers lets callers (e.g. `oidcCallModelScopePreAssert`)
37
+ * skip scope enforcement instead of falsely treating the missing claim as "no scopes granted".
38
+ *
39
+ * The parameter is intentionally typed as `unknown` because `DecodedIdToken` (Firebase
40
+ * Auth's typed token shape) does not declare a `scope` field, while OIDC bearer-token
41
+ * callers attach one at runtime. Reading is done defensively.
42
+ *
43
+ * @param request - The callable request as seen inside an `onCallModel` handler.
44
+ * @returns A `Set<string>` of granted scopes, or `undefined` when the request carries no OIDC `scope` claim.
45
+ */
46
+ export declare function getOidcScopesFromRequest(request: unknown): Maybe<Set<string>>;