@dereekb/firebase-server 13.29.0 → 13.30.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/index.cjs.js CHANGED
@@ -7059,6 +7059,78 @@ exports.FirebaseServerAuthModule = __decorate([
7059
7059
  return claims[firebase.FIREBASE_SERVER_AUTH_CLAIMS_SETUP_PASSWORD_KEY] != null;
7060
7060
  }
7061
7061
 
7062
+ // MARK: Scope Reading
7063
+ /**
7064
+ * Reads the set of OIDC scopes carried by a model-api request's auth, or `undefined` for a non-OIDC
7065
+ * (regular Firebase ID-token) caller.
7066
+ *
7067
+ * The OIDC bearer-token middleware attaches the validated access-token claims at
7068
+ * `auth.oidcValidatedToken` (with the space-delimited `scope` string); a non-OIDC caller has neither
7069
+ * that field nor a `scope` on `auth.token`. Reading is defensive (the auth shape is only typed as
7070
+ * {@link FirebaseServerAuthData} here — the OIDC-specific `oidcValidatedToken` lives in the
7071
+ * `@dereekb/firebase-server/oidc` sub-package this core layer cannot import), delegating the actual
7072
+ * parse to the shared {@link oidcScopesFromScopeClaim} so there is no drift with `getOidcScopesFromRequest`.
7073
+ *
7074
+ * @param auth - The request auth data, or undefined for unauthenticated requests.
7075
+ * @returns The granted scope set, or `undefined` when the request carries no OIDC `scope` claim.
7076
+ */ function oidcScopesFromModelApiAuth(auth) {
7077
+ var _auth_oidcValidatedToken, _auth_token;
7078
+ var oidcScope = auth === null || auth === void 0 ? void 0 : (_auth_oidcValidatedToken = auth.oidcValidatedToken) === null || _auth_oidcValidatedToken === void 0 ? void 0 : _auth_oidcValidatedToken.scope;
7079
+ var tokenScope = auth === null || auth === void 0 ? void 0 : (_auth_token = auth.token) === null || _auth_token === void 0 ? void 0 : _auth_token.scope;
7080
+ var scope = oidcScope !== null && oidcScope !== void 0 ? oidcScope : tokenScope;
7081
+ return firebase.oidcScopesFromScopeClaim(scope);
7082
+ }
7083
+ /**
7084
+ * Enforces the OIDC scope requirement for a single model-api op, throwing a `403`
7085
+ * {@link CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE} error when the caller does not satisfy it.
7086
+ *
7087
+ * The single home of callModel OIDC scope enforcement. It reuses the shipped composition + evaluation
7088
+ * (`resolveEffectiveOidcScopeTerms` / `oidcScopeTermsSatisfied`) so enforcement and the MCP
7089
+ * tool-visibility filter never drift. Enforcement is AND-of-ORs across the per-verb `model.<call>`
7090
+ * scope and the effective GROUP term (per-function `requiredScope` > per-model requirement >
7091
+ * configured default).
7092
+ *
7093
+ * Bypasses (no-op) when `grantedScopes` is `undefined` — i.e. a non-OIDC caller — and short-circuits
7094
+ * without any check when the op resolves no requirement at all (a custom, non-CRUD verb with no
7095
+ * per-function/model/default term).
7096
+ *
7097
+ * @param input - The verb, model type, per-function scope, group config, and the caller's granted scopes.
7098
+ * @throws A `403` forbidden error (code {@link CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE}) when an OIDC
7099
+ * caller does not satisfy the effective requirement.
7100
+ */ function assertModelApiOidcScope(input) {
7101
+ var call = input.call, modelType = input.modelType, requiredScope = input.requiredScope, defaultRequiredScope = input.defaultRequiredScope, modelRequiredScopes = input.modelRequiredScopes, grantedScopes = input.grantedScopes;
7102
+ var terms = firebase.resolveEffectiveOidcScopeTerms({
7103
+ perVerbScope: firebase.callModelOidcScopeForCallType(call),
7104
+ requiredScope: requiredScope,
7105
+ modelRequirement: modelRequiredScopes === null || modelRequiredScopes === void 0 ? void 0 : modelRequiredScopes[modelType],
7106
+ call: call,
7107
+ defaultRequiredScope: defaultRequiredScope
7108
+ });
7109
+ if (terms.length > 0 && grantedScopes != null && !firebase.oidcScopeTermsSatisfied(terms, grantedScopes)) {
7110
+ var missingTerms = terms.filter(function(term) {
7111
+ return !firebase.oidcScopeTermSatisfied(term, grantedScopes);
7112
+ });
7113
+ throw forbiddenError({
7114
+ status: 403,
7115
+ code: firebase.CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE,
7116
+ message: "Missing required OIDC scope for callModel: ".concat(missingTerms.map(formatModelApiScopeTerm).join(', ')),
7117
+ data: {
7118
+ requiredScopes: missingTerms,
7119
+ call: call
7120
+ }
7121
+ });
7122
+ }
7123
+ }
7124
+ /**
7125
+ * Renders a scope term for the human-readable error message: a single scope as-is, an OR-group as its
7126
+ * alternatives joined by `|` (so a single-scope term reads as itself).
7127
+ *
7128
+ * @param term - The unsatisfied scope term.
7129
+ * @returns The display string for the term.
7130
+ */ function formatModelApiScopeTerm(term) {
7131
+ return typeof term === 'string' ? term : term.join('|');
7132
+ }
7133
+
7062
7134
  function _define_property$n(obj, key, value) {
7063
7135
  if (key in obj) {
7064
7136
  Object.defineProperty(obj, key, {
@@ -7335,6 +7407,15 @@ function _ts_generator$a(thisArg, body) {
7335
7407
  /**
7336
7408
  * Factory to create typed nest context from INestApplicationContext.
7337
7409
  */ _define_property$m(this, "makeNestContext", void 0);
7410
+ /**
7411
+ * Optional model-api-layer OIDC group-scope default. See {@link ModelApiOidcScopeConfig.defaultRequiredScope}.
7412
+ * This is the home for the callModel group-scope default; providing it here enforces it across
7413
+ * dispatch AND the `/get` reads.
7414
+ */ _define_property$m(this, "defaultRequiredScope", void 0);
7415
+ /**
7416
+ * Optional per-model OIDC group-scope overrides. See {@link ModelApiOidcScopeConfig.modelRequiredScopes}.
7417
+ * The only place a plain `/get` read (no per-function handler) can be scope-gated beyond `model.read`.
7418
+ */ _define_property$m(this, "modelRequiredScopes", void 0);
7338
7419
  };
7339
7420
  /**
7340
7421
  * Injection token for providing the NestJS application context to the dispatch service.
@@ -7369,8 +7450,25 @@ function _ts_generator$a(thisArg, body) {
7369
7450
  * @returns The handler's return value.
7370
7451
  */ function dispatch(params, auth, rawRequest) {
7371
7452
  return _async_to_generator$a(function() {
7372
- var callableRequest, appRequest, contextRequest;
7453
+ var call, modelType, specifier, apiDetails, requiredScope, callableRequest, appRequest, contextRequest;
7373
7454
  return _ts_generator$a(this, function(_state) {
7455
+ // Enforce OIDC scope BEFORE dispatching — the relocated home of the callModel scope check.
7456
+ // AND-of-ORs across the per-verb `model.<call>` scope and the effective group term (per-function
7457
+ // `requiredScope` > per-model requirement > module default), bypassing non-OIDC callers. A nullish
7458
+ // `call` is a malformed request the callModel chain rejects downstream (no model op to authorize).
7459
+ call = params.call, modelType = params.modelType, specifier = params.specifier;
7460
+ if (call != null) {
7461
+ apiDetails = this.config.callModelFn._apiDetails;
7462
+ requiredScope = apiDetails == null ? undefined : resolveRequiredScopeFromApiDetails(apiDetails, call, modelType, specifier);
7463
+ assertModelApiOidcScope({
7464
+ call: call,
7465
+ modelType: modelType,
7466
+ requiredScope: requiredScope,
7467
+ defaultRequiredScope: this.config.defaultRequiredScope,
7468
+ modelRequiredScopes: this.config.modelRequiredScopes,
7469
+ grantedScopes: oidcScopesFromModelApiAuth(auth)
7470
+ });
7471
+ }
7374
7472
  // Build a synthetic CallableRequest that the dispatch chain expects. Layer the
7375
7473
  // OIDC-validated claim subset over the base token so standard JWT claims
7376
7474
  // (`iat`, `auth_time`, `email`, …) survive — the callModel chain and any
@@ -7657,12 +7755,38 @@ function _ts_generator$9(thisArg, body) {
7657
7755
  function ModelApiGetService(config, nestApplication) {
7658
7756
  _class_call_check$e(this, ModelApiGetService);
7659
7757
  _define_property$l(this, "_nestContext", void 0);
7758
+ _define_property$l(this, "_defaultRequiredScope", void 0);
7759
+ _define_property$l(this, "_modelRequiredScopes", void 0);
7660
7760
  _define_property$l(this, "_identityByModelType", void 0);
7661
7761
  this._nestContext = config.makeNestContext(nestApplication);
7762
+ this._defaultRequiredScope = config.defaultRequiredScope;
7763
+ this._modelRequiredScopes = config.modelRequiredScopes;
7662
7764
  }
7663
7765
  _create_class$9(ModelApiGetService, [
7664
7766
  {
7665
7767
  /**
7768
+ * Enforces the OIDC read-scope requirement for a direct document read before it hits Firestore.
7769
+ *
7770
+ * A direct `/get` read is the `read` verb: it requires the per-verb `model.read` scope AND any
7771
+ * effective group term for the target model (per-model requirement > module default). This is the
7772
+ * ONLY gate on the direct-read path — it does not touch the callModel dispatch chain — so without it
7773
+ * an OIDC client scoped to a subset could read any model through `/get`. Non-OIDC callers bypass.
7774
+ *
7775
+ * @param modelType - The Firestore model type being read.
7776
+ * @param auth - The request's auth data (OIDC scopes are read from it).
7777
+ */ key: "_assertReadScope",
7778
+ value: function _assertReadScope(modelType, auth) {
7779
+ assertModelApiOidcScope({
7780
+ call: 'read',
7781
+ modelType: modelType,
7782
+ defaultRequiredScope: this._defaultRequiredScope,
7783
+ modelRequiredScopes: this._modelRequiredScopes,
7784
+ grantedScopes: oidcScopesFromModelApiAuth(auth)
7785
+ });
7786
+ }
7787
+ },
7788
+ {
7789
+ /**
7666
7790
  * Returns the registered {@link FirestoreModelIdentity} for the given `modelType` string, or
7667
7791
  * `undefined` when no model of that type is registered.
7668
7792
  *
@@ -7734,6 +7858,7 @@ function _ts_generator$9(thisArg, body) {
7734
7858
  return _ts_generator$9(this, function(_state) {
7735
7859
  switch(_state.label){
7736
7860
  case 0:
7861
+ this._assertReadScope(modelType, auth);
7737
7862
  authRef = this._makeAuthRef(auth);
7738
7863
  return [
7739
7864
  4,
@@ -7782,6 +7907,7 @@ function _ts_generator$9(thisArg, body) {
7782
7907
  return _async_to_generator$9(function() {
7783
7908
  var authRef;
7784
7909
  return _ts_generator$9(this, function(_state) {
7910
+ this._assertReadScope(modelType, auth);
7785
7911
  authRef = this._makeAuthRef(auth);
7786
7912
  return [
7787
7913
  2,
@@ -13185,6 +13311,7 @@ exports.assertHasSignedTosInRequest = assertHasSignedTosInRequest;
13185
13311
  exports.assertIsAdminInRequest = assertIsAdminInRequest;
13186
13312
  exports.assertIsAdminOrTargetUserInRequestData = assertIsAdminOrTargetUserInRequestData;
13187
13313
  exports.assertIsContextWithAuthData = assertIsContextWithAuthData;
13314
+ exports.assertModelApiOidcScope = assertModelApiOidcScope;
13188
13315
  exports.assertRequestRequiresAuthForFunction = assertRequestRequiresAuthForFunction;
13189
13316
  exports.assertSnapshotData = assertSnapshotData;
13190
13317
  exports.assertSnapshotDataWithKey = assertSnapshotDataWithKey;
@@ -13282,6 +13409,7 @@ exports.noRunNameSpecifiedForScheduledFunctionDevelopmentFunction = noRunNameSpe
13282
13409
  exports.noopFirebaseServerAnalyticsServiceListener = noopFirebaseServerAnalyticsServiceListener;
13283
13410
  exports.noopOnCallModelAnalyticsService = noopOnCallModelAnalyticsService;
13284
13411
  exports.notFoundError = notFoundError;
13412
+ exports.oidcScopesFromModelApiAuth = oidcScopesFromModelApiAuth;
13285
13413
  exports.onCallAnalyticsEmitterInstance = onCallAnalyticsEmitterInstance;
13286
13414
  exports.onCallCreateModel = onCallCreateModel;
13287
13415
  exports.onCallDeleteModel = onCallDeleteModel;
package/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { timingSafeEqual } from 'node:crypto';
2
- import { DBX_FIREBASE_SERVER_NO_AUTH_ERROR_CODE, UNAUTHENTICATED_ERROR_CODE, FORBIDDEN_ERROR_CODE, PERMISSION_DENIED_ERROR_CODE, NOT_FOUND_ERROR_CODE, MODEL_NOT_AVAILABLE_ERROR_CODE, BAD_REQUEST_ERROR_CODE, CONFLICT_ERROR_CODE, ALREADY_EXISTS_ERROR_CODE, UNAVAILABLE_ERROR_CODE, UNAVAILABLE_OR_DEACTIVATED_FUNCTION_ERROR_CODE, INTERNAL_SERVER_ERROR_CODE, FIREBASE_AUTH_USER_NOT_FOUND_ERROR, FIREBASE_SERVER_AUTH_CLAIMS_RESET_PASSWORD_KEY, FIREBASE_SERVER_AUTH_CLAIMS_RESET_LAST_COM_DATE_KEY, FIREBASE_SERVER_AUTH_CLAIMS_RESET_EXPIRES_AT_KEY, FIREBASE_AUTH_PHONE_NUMBER_ALREADY_EXISTS_ERROR, FIREBASE_AUTH_EMAIL_ALREADY_EXISTS_ERROR, FIREBASE_AUTH_INVALID_PHONE_NUMBER_ERROR, FIREBASE_SERVER_AUTH_CLAIMS_SETUP_PASSWORD_KEY, FIREBASE_SERVER_AUTH_CLAIMS_SETUP_LAST_COM_DATE_KEY, setIdAndKeyFromKeyIdRefOnDocumentData, DBX_FIREBASE_SERVER_PASSWORD_RESET_INVALID_CODE_ERROR_CODE, DBX_FIREBASE_SERVER_PASSWORD_RESET_NO_CONFIG_ERROR_CODE, DBX_FIREBASE_SERVER_PASSWORD_RESET_SEND_ONCE_ERROR_CODE, DBX_FIREBASE_SERVER_PASSWORD_RESET_THROTTLE_ERROR_CODE, FirestoreDocumentContextType, streamFromOnSnapshot, FIRESTORE_LIMIT_QUERY_CONSTRAINT_TYPE, FIRESTORE_LIMIT_TO_LAST_QUERY_CONSTRAINT_TYPE, FIRESTORE_ORDER_BY_QUERY_CONSTRAINT_TYPE, FIRESTORE_ORDER_BY_DOCUMENT_ID_QUERY_CONSTRAINT_TYPE, FIRESTORE_WHERE_QUERY_CONSTRAINT_TYPE, FIRESTORE_WHERE_DOCUMENT_ID_QUERY_CONSTRAINT_TYPE, FIRESTORE_OFFSET_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AT_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AT_VALUE_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AFTER_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_AT_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_AT_VALUE_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_BEFORE_QUERY_CONSTRAINT_TYPE, makeFirestoreQueryConstraintFunctionsDriver, firestoreContextFactory, firestoreField, optionalFirestoreField, DEFAULT_MODEL_FUNCTION_FIREBASE_CRUD_FUNCTION_SPECIFIER, UNKNOWN_MODEL_TYPE_ERROR_CODE, BAD_DOCUMENT_QUERY_CURSOR_ERROR_CODE, limit, startAfter, DEFAULT_ON_CALL_QUERY_MODEL_LIMIT, MAX_ON_CALL_QUERY_MODEL_LIMIT, ScheduledFunctionDevelopmentFunctionTypeEnum, SCHEDULED_FUNCTION_DEV_FUNCTION_SPECIFIER, storageListFilesResultFactory, assertStorageUploadOptionsStringFormat, firebaseStorageContextFactory, inContextFirebaseModelsServiceFactory, useFirebaseModelsService } from '@dereekb/firebase';
2
+ import { DBX_FIREBASE_SERVER_NO_AUTH_ERROR_CODE, UNAUTHENTICATED_ERROR_CODE, FORBIDDEN_ERROR_CODE, PERMISSION_DENIED_ERROR_CODE, NOT_FOUND_ERROR_CODE, MODEL_NOT_AVAILABLE_ERROR_CODE, BAD_REQUEST_ERROR_CODE, CONFLICT_ERROR_CODE, ALREADY_EXISTS_ERROR_CODE, UNAVAILABLE_ERROR_CODE, UNAVAILABLE_OR_DEACTIVATED_FUNCTION_ERROR_CODE, INTERNAL_SERVER_ERROR_CODE, FIREBASE_AUTH_USER_NOT_FOUND_ERROR, FIREBASE_SERVER_AUTH_CLAIMS_RESET_PASSWORD_KEY, FIREBASE_SERVER_AUTH_CLAIMS_RESET_LAST_COM_DATE_KEY, FIREBASE_SERVER_AUTH_CLAIMS_RESET_EXPIRES_AT_KEY, FIREBASE_AUTH_PHONE_NUMBER_ALREADY_EXISTS_ERROR, FIREBASE_AUTH_EMAIL_ALREADY_EXISTS_ERROR, FIREBASE_AUTH_INVALID_PHONE_NUMBER_ERROR, FIREBASE_SERVER_AUTH_CLAIMS_SETUP_PASSWORD_KEY, FIREBASE_SERVER_AUTH_CLAIMS_SETUP_LAST_COM_DATE_KEY, setIdAndKeyFromKeyIdRefOnDocumentData, DBX_FIREBASE_SERVER_PASSWORD_RESET_INVALID_CODE_ERROR_CODE, DBX_FIREBASE_SERVER_PASSWORD_RESET_NO_CONFIG_ERROR_CODE, DBX_FIREBASE_SERVER_PASSWORD_RESET_SEND_ONCE_ERROR_CODE, DBX_FIREBASE_SERVER_PASSWORD_RESET_THROTTLE_ERROR_CODE, FirestoreDocumentContextType, streamFromOnSnapshot, FIRESTORE_LIMIT_QUERY_CONSTRAINT_TYPE, FIRESTORE_LIMIT_TO_LAST_QUERY_CONSTRAINT_TYPE, FIRESTORE_ORDER_BY_QUERY_CONSTRAINT_TYPE, FIRESTORE_ORDER_BY_DOCUMENT_ID_QUERY_CONSTRAINT_TYPE, FIRESTORE_WHERE_QUERY_CONSTRAINT_TYPE, FIRESTORE_WHERE_DOCUMENT_ID_QUERY_CONSTRAINT_TYPE, FIRESTORE_OFFSET_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AT_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AT_VALUE_QUERY_CONSTRAINT_TYPE, FIRESTORE_START_AFTER_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_AT_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_AT_VALUE_QUERY_CONSTRAINT_TYPE, FIRESTORE_END_BEFORE_QUERY_CONSTRAINT_TYPE, makeFirestoreQueryConstraintFunctionsDriver, firestoreContextFactory, firestoreField, optionalFirestoreField, DEFAULT_MODEL_FUNCTION_FIREBASE_CRUD_FUNCTION_SPECIFIER, UNKNOWN_MODEL_TYPE_ERROR_CODE, BAD_DOCUMENT_QUERY_CURSOR_ERROR_CODE, limit, startAfter, DEFAULT_ON_CALL_QUERY_MODEL_LIMIT, MAX_ON_CALL_QUERY_MODEL_LIMIT, resolveEffectiveOidcScopeTerms, callModelOidcScopeForCallType, oidcScopeTermsSatisfied, oidcScopeTermSatisfied, CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE, oidcScopesFromScopeClaim, ScheduledFunctionDevelopmentFunctionTypeEnum, SCHEDULED_FUNCTION_DEV_FUNCTION_SPECIFIER, storageListFilesResultFactory, assertStorageUploadOptionsStringFormat, firebaseStorageContextFactory, inContextFirebaseModelsServiceFactory, useFirebaseModelsService } from '@dereekb/firebase';
3
3
  import { partialServerError, isServerError, randomNumberFactory, cachedGetter, filterNullAndUndefinedValues, asSet, AUTH_ADMIN_ROLE, AUTH_TOS_SIGNED_ROLE, forEachKeyValue, KeyValueTypleValueFilter, filterUndefinedValues, isThrottled, mapObjectMap, batch, objectToMap, serverError, asArray, containsAllValues, websiteUrlDetails, mergeObjects, cronExpressionRepeatingEveryNMinutes, mapIdentityFunction, slashPathName, toRelativeSlashPathStartType, fixMultiSlashesInSlashPath, SLASH_PATH_SEPARATOR, objectHasNoKeys, websiteUrlFromPaths, pushItemOrArrayItemsIntoArray, makeGetter, asGetter, build, performAsyncTasks } from '@dereekb/util';
4
4
  import { HttpsError } from 'firebase-functions/https';
5
5
  import { hoursToMs, minutesToMs, toISODateString } from '@dereekb/date';
@@ -7057,6 +7057,78 @@ FirebaseServerAuthModule = __decorate([
7057
7057
  return claims[FIREBASE_SERVER_AUTH_CLAIMS_SETUP_PASSWORD_KEY] != null;
7058
7058
  }
7059
7059
 
7060
+ // MARK: Scope Reading
7061
+ /**
7062
+ * Reads the set of OIDC scopes carried by a model-api request's auth, or `undefined` for a non-OIDC
7063
+ * (regular Firebase ID-token) caller.
7064
+ *
7065
+ * The OIDC bearer-token middleware attaches the validated access-token claims at
7066
+ * `auth.oidcValidatedToken` (with the space-delimited `scope` string); a non-OIDC caller has neither
7067
+ * that field nor a `scope` on `auth.token`. Reading is defensive (the auth shape is only typed as
7068
+ * {@link FirebaseServerAuthData} here — the OIDC-specific `oidcValidatedToken` lives in the
7069
+ * `@dereekb/firebase-server/oidc` sub-package this core layer cannot import), delegating the actual
7070
+ * parse to the shared {@link oidcScopesFromScopeClaim} so there is no drift with `getOidcScopesFromRequest`.
7071
+ *
7072
+ * @param auth - The request auth data, or undefined for unauthenticated requests.
7073
+ * @returns The granted scope set, or `undefined` when the request carries no OIDC `scope` claim.
7074
+ */ function oidcScopesFromModelApiAuth(auth) {
7075
+ var _auth_oidcValidatedToken, _auth_token;
7076
+ var oidcScope = auth === null || auth === void 0 ? void 0 : (_auth_oidcValidatedToken = auth.oidcValidatedToken) === null || _auth_oidcValidatedToken === void 0 ? void 0 : _auth_oidcValidatedToken.scope;
7077
+ var tokenScope = auth === null || auth === void 0 ? void 0 : (_auth_token = auth.token) === null || _auth_token === void 0 ? void 0 : _auth_token.scope;
7078
+ var scope = oidcScope !== null && oidcScope !== void 0 ? oidcScope : tokenScope;
7079
+ return oidcScopesFromScopeClaim(scope);
7080
+ }
7081
+ /**
7082
+ * Enforces the OIDC scope requirement for a single model-api op, throwing a `403`
7083
+ * {@link CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE} error when the caller does not satisfy it.
7084
+ *
7085
+ * The single home of callModel OIDC scope enforcement. It reuses the shipped composition + evaluation
7086
+ * (`resolveEffectiveOidcScopeTerms` / `oidcScopeTermsSatisfied`) so enforcement and the MCP
7087
+ * tool-visibility filter never drift. Enforcement is AND-of-ORs across the per-verb `model.<call>`
7088
+ * scope and the effective GROUP term (per-function `requiredScope` > per-model requirement >
7089
+ * configured default).
7090
+ *
7091
+ * Bypasses (no-op) when `grantedScopes` is `undefined` — i.e. a non-OIDC caller — and short-circuits
7092
+ * without any check when the op resolves no requirement at all (a custom, non-CRUD verb with no
7093
+ * per-function/model/default term).
7094
+ *
7095
+ * @param input - The verb, model type, per-function scope, group config, and the caller's granted scopes.
7096
+ * @throws A `403` forbidden error (code {@link CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE}) when an OIDC
7097
+ * caller does not satisfy the effective requirement.
7098
+ */ function assertModelApiOidcScope(input) {
7099
+ var call = input.call, modelType = input.modelType, requiredScope = input.requiredScope, defaultRequiredScope = input.defaultRequiredScope, modelRequiredScopes = input.modelRequiredScopes, grantedScopes = input.grantedScopes;
7100
+ var terms = resolveEffectiveOidcScopeTerms({
7101
+ perVerbScope: callModelOidcScopeForCallType(call),
7102
+ requiredScope: requiredScope,
7103
+ modelRequirement: modelRequiredScopes === null || modelRequiredScopes === void 0 ? void 0 : modelRequiredScopes[modelType],
7104
+ call: call,
7105
+ defaultRequiredScope: defaultRequiredScope
7106
+ });
7107
+ if (terms.length > 0 && grantedScopes != null && !oidcScopeTermsSatisfied(terms, grantedScopes)) {
7108
+ var missingTerms = terms.filter(function(term) {
7109
+ return !oidcScopeTermSatisfied(term, grantedScopes);
7110
+ });
7111
+ throw forbiddenError({
7112
+ status: 403,
7113
+ code: CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE,
7114
+ message: "Missing required OIDC scope for callModel: ".concat(missingTerms.map(formatModelApiScopeTerm).join(', ')),
7115
+ data: {
7116
+ requiredScopes: missingTerms,
7117
+ call: call
7118
+ }
7119
+ });
7120
+ }
7121
+ }
7122
+ /**
7123
+ * Renders a scope term for the human-readable error message: a single scope as-is, an OR-group as its
7124
+ * alternatives joined by `|` (so a single-scope term reads as itself).
7125
+ *
7126
+ * @param term - The unsatisfied scope term.
7127
+ * @returns The display string for the term.
7128
+ */ function formatModelApiScopeTerm(term) {
7129
+ return typeof term === 'string' ? term : term.join('|');
7130
+ }
7131
+
7060
7132
  function _define_property$n(obj, key, value) {
7061
7133
  if (key in obj) {
7062
7134
  Object.defineProperty(obj, key, {
@@ -7333,6 +7405,15 @@ function _ts_generator$a(thisArg, body) {
7333
7405
  /**
7334
7406
  * Factory to create typed nest context from INestApplicationContext.
7335
7407
  */ _define_property$m(this, "makeNestContext", void 0);
7408
+ /**
7409
+ * Optional model-api-layer OIDC group-scope default. See {@link ModelApiOidcScopeConfig.defaultRequiredScope}.
7410
+ * This is the home for the callModel group-scope default; providing it here enforces it across
7411
+ * dispatch AND the `/get` reads.
7412
+ */ _define_property$m(this, "defaultRequiredScope", void 0);
7413
+ /**
7414
+ * Optional per-model OIDC group-scope overrides. See {@link ModelApiOidcScopeConfig.modelRequiredScopes}.
7415
+ * The only place a plain `/get` read (no per-function handler) can be scope-gated beyond `model.read`.
7416
+ */ _define_property$m(this, "modelRequiredScopes", void 0);
7336
7417
  };
7337
7418
  /**
7338
7419
  * Injection token for providing the NestJS application context to the dispatch service.
@@ -7367,8 +7448,25 @@ function _ts_generator$a(thisArg, body) {
7367
7448
  * @returns The handler's return value.
7368
7449
  */ function dispatch(params, auth, rawRequest) {
7369
7450
  return _async_to_generator$a(function() {
7370
- var callableRequest, appRequest, contextRequest;
7451
+ var call, modelType, specifier, apiDetails, requiredScope, callableRequest, appRequest, contextRequest;
7371
7452
  return _ts_generator$a(this, function(_state) {
7453
+ // Enforce OIDC scope BEFORE dispatching — the relocated home of the callModel scope check.
7454
+ // AND-of-ORs across the per-verb `model.<call>` scope and the effective group term (per-function
7455
+ // `requiredScope` > per-model requirement > module default), bypassing non-OIDC callers. A nullish
7456
+ // `call` is a malformed request the callModel chain rejects downstream (no model op to authorize).
7457
+ call = params.call, modelType = params.modelType, specifier = params.specifier;
7458
+ if (call != null) {
7459
+ apiDetails = this.config.callModelFn._apiDetails;
7460
+ requiredScope = apiDetails == null ? undefined : resolveRequiredScopeFromApiDetails(apiDetails, call, modelType, specifier);
7461
+ assertModelApiOidcScope({
7462
+ call: call,
7463
+ modelType: modelType,
7464
+ requiredScope: requiredScope,
7465
+ defaultRequiredScope: this.config.defaultRequiredScope,
7466
+ modelRequiredScopes: this.config.modelRequiredScopes,
7467
+ grantedScopes: oidcScopesFromModelApiAuth(auth)
7468
+ });
7469
+ }
7372
7470
  // Build a synthetic CallableRequest that the dispatch chain expects. Layer the
7373
7471
  // OIDC-validated claim subset over the base token so standard JWT claims
7374
7472
  // (`iat`, `auth_time`, `email`, …) survive — the callModel chain and any
@@ -7655,12 +7753,38 @@ function _ts_generator$9(thisArg, body) {
7655
7753
  function ModelApiGetService(config, nestApplication) {
7656
7754
  _class_call_check$e(this, ModelApiGetService);
7657
7755
  _define_property$l(this, "_nestContext", void 0);
7756
+ _define_property$l(this, "_defaultRequiredScope", void 0);
7757
+ _define_property$l(this, "_modelRequiredScopes", void 0);
7658
7758
  _define_property$l(this, "_identityByModelType", void 0);
7659
7759
  this._nestContext = config.makeNestContext(nestApplication);
7760
+ this._defaultRequiredScope = config.defaultRequiredScope;
7761
+ this._modelRequiredScopes = config.modelRequiredScopes;
7660
7762
  }
7661
7763
  _create_class$9(ModelApiGetService, [
7662
7764
  {
7663
7765
  /**
7766
+ * Enforces the OIDC read-scope requirement for a direct document read before it hits Firestore.
7767
+ *
7768
+ * A direct `/get` read is the `read` verb: it requires the per-verb `model.read` scope AND any
7769
+ * effective group term for the target model (per-model requirement > module default). This is the
7770
+ * ONLY gate on the direct-read path — it does not touch the callModel dispatch chain — so without it
7771
+ * an OIDC client scoped to a subset could read any model through `/get`. Non-OIDC callers bypass.
7772
+ *
7773
+ * @param modelType - The Firestore model type being read.
7774
+ * @param auth - The request's auth data (OIDC scopes are read from it).
7775
+ */ key: "_assertReadScope",
7776
+ value: function _assertReadScope(modelType, auth) {
7777
+ assertModelApiOidcScope({
7778
+ call: 'read',
7779
+ modelType: modelType,
7780
+ defaultRequiredScope: this._defaultRequiredScope,
7781
+ modelRequiredScopes: this._modelRequiredScopes,
7782
+ grantedScopes: oidcScopesFromModelApiAuth(auth)
7783
+ });
7784
+ }
7785
+ },
7786
+ {
7787
+ /**
7664
7788
  * Returns the registered {@link FirestoreModelIdentity} for the given `modelType` string, or
7665
7789
  * `undefined` when no model of that type is registered.
7666
7790
  *
@@ -7732,6 +7856,7 @@ function _ts_generator$9(thisArg, body) {
7732
7856
  return _ts_generator$9(this, function(_state) {
7733
7857
  switch(_state.label){
7734
7858
  case 0:
7859
+ this._assertReadScope(modelType, auth);
7735
7860
  authRef = this._makeAuthRef(auth);
7736
7861
  return [
7737
7862
  4,
@@ -7780,6 +7905,7 @@ function _ts_generator$9(thisArg, body) {
7780
7905
  return _async_to_generator$9(function() {
7781
7906
  var authRef;
7782
7907
  return _ts_generator$9(this, function(_state) {
7908
+ this._assertReadScope(modelType, auth);
7783
7909
  authRef = this._makeAuthRef(auth);
7784
7910
  return [
7785
7911
  2,
@@ -13116,4 +13242,4 @@ function _define_property(obj, key, value) {
13116
13242
  }
13117
13243
  ();
13118
13244
 
13119
- export { AbstractFirebaseNestContext, AbstractFirebaseServerActionsContext, AbstractFirebaseServerAuthContext, AbstractFirebaseServerAuthService, AbstractFirebaseServerAuthUserContext, AbstractFirebaseServerNewUserService, AbstractFirebaseServerUserPasswordResetService, AbstractNestContext, AbstractServerFirebaseNestContext, ConfigureFirebaseAppCheckMiddlewareModule, ConfigureFirebaseWebhookMiddlewareModule, DEFAULT_FIREBASE_PASSWORD_NUMBER_GENERATOR, DEFAULT_RESET_CODE_EXPIRES_IN, DEFAULT_RESET_COM_THROTTLE_TIME, DEFAULT_SERVER_ASSETS_BASE_PATH, DEFAULT_SETUP_COM_THROTTLE_TIME, DefaultFirebaseServerEnvService, FIREBASE_APP_TOKEN, FIREBASE_AUTH_TOKEN, FIREBASE_FIRESTORE_CONTEXT_TOKEN, FIREBASE_FIRESTORE_TOKEN, FIREBASE_SERVER_ENV_TOKEN, FIREBASE_SERVER_VALIDATION_ERROR_CODE, FIREBASE_STORAGE_CONTEXT_FACTORY_CONFIG_TOKEN, FIREBASE_STORAGE_CONTEXT_TOKEN, FIREBASE_STORAGE_TOKEN, FIRESTORE_CLIENT_QUERY_CONSTRAINT_HANDLER_MAPPING, FirebaseAppCheckMiddleware, FirebaseAppCheckMiddlewareConfig, FirebaseNestServerRootModule, FirebaseRawBodyMiddleware, FirebaseServerAnalyticsSegmentListenerService, FirebaseServerAnalyticsSegmentModule, FirebaseServerAnalyticsService, FirebaseServerAnalyticsServiceListener, FirebaseServerAuthModule, FirebaseServerAuthNewUserSendSetupDetailsNoSetupConfigError, FirebaseServerAuthNewUserSendSetupDetailsSendOnceError, FirebaseServerAuthNewUserSendSetupDetailsThrottleError, FirebaseServerAuthPasswordResetInvalidCodeError, FirebaseServerAuthPasswordResetNoResetConfigError, FirebaseServerAuthPasswordResetSendOnceError, FirebaseServerAuthPasswordResetThrottleError, FirebaseServerAuthService, FirebaseServerAuthUserBadInputError, FirebaseServerAuthUserExistsError, FirebaseServerEnvService, FirebaseServerFirestoreContextModule, FirebaseServerFirestoreModule, FirebaseServerStorageContextModule, FirebaseServerStorageModule, FirebaseServerStorageService, GlobalRoutePrefixConfig, MAX_MODEL_ACCESS_MULTI_READ_KEYS, MODEL_API_NEST_APPLICATION_CONTEXT, ModelApiCallModelDispatchService, ModelApiController, ModelApiDispatchConfig, ModelApiGetService, NO_RUN_NAME_SPECIFIED_FOR_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_CODE, NoContentFirebaseServerUserPasswordResetService, NoSetupContentFirebaseServerNewUserService, ON_CALL_MODEL_ANALYTICS_HANDLER, ON_CALL_MODEL_ANALYTICS_SERVICE, OnCallModelAnalyticsService, PHONE_NUMBER_ALREADY_EXISTS_ERROR_CODE, SkipAppCheck, UNKNOWN_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_NAME_CODE, UNKNOWN_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_TYPE_CODE, _onCallWithCallTypeFunction, aggregateCrudModelApiDetails, aggregateModelApiDetails, aggregateSpecifierApiDetails, alreadyExistsError, appAnalyticsModuleMetadata, appFirestoreModuleMetadata, assertContextHasAuth, assertDocumentExists, assertHasRolesInRequest, assertHasSignedTosInRequest, assertIsAdminInRequest, assertIsAdminOrTargetUserInRequestData, assertIsContextWithAuthData, assertRequestRequiresAuthForFunction, assertSnapshotData, assertSnapshotDataWithKey, authServicePasswordResetInvalidCodeError, authServicePasswordResetNoConfigError, authServicePasswordResetSendOnceError, authServicePasswordResetThrottleError, badRequestError, blockingFunctionHandlerWithNestContextFactory, buildNestServerRootModule, callWithAnalytics, catchAndThrowPasswordResetServerErrors, cloudEventHandlerWithNestContextFactory, collectionRefForPath, createModelUnknownModelTypeError, decodeFirebaseServerUserPasswordResetOobCode, defaultFirebaseServerActionsTransformFactoryLogErrorFunction, defaultProvideFirebaseServerStorageServiceSimple, deleteModelUnknownModelTypeError, developmentUnknownSpecifierError, docRefForPath, documentModelNotAvailableError, encodeFirebaseServerUserPasswordResetOobCode, executeOnCallQuery, firebaseAuthTokenFromDecodedIdToken, firebaseServerActionsContext, firebaseServerActionsTransformContext, firebaseServerActionsTransformFactory, firebaseServerAppTokenProvider, firebaseServerAuthModuleMetadata, firebaseServerDevFunctions, firebaseServerEnvTokenProvider, firebaseServerEnvTokenProviders, firebaseServerErrorInfo, firebaseServerErrorInfoCodePair, firebaseServerErrorInfoServerErrorCodePair, firebaseServerErrorInfoServerErrorPair, firebaseServerFirestoreContextModuleMetadata, firebaseServerStorageDefaultBucketIdTokenProvider, firebaseServerStorageModuleMetadata, firebaseServerValidationError, firebaseServerValidationServerError, firestoreClientQueryConstraintFunctionsDriver, firestoreEncryptedField, firestoreServerIncrementUpdateToUpdateData, forbiddenError, getAuthUserOrUndefined, getModelApiDetails, googleCloudFileMetadataToStorageMetadata, googleCloudFirebaseStorageContextFactory, googleCloudFirebaseStorageDrivers, googleCloudFirestoreAccessorDriver, googleCloudFirestoreContextFactory, googleCloudFirestoreDrivers, googleCloudFirestoreQueryDriver, googleCloudStorageAccessorFile, googleCloudStorageAccessorFolder, googleCloudStorageBucketForStorageFilePath, googleCloudStorageFileForStorageFilePath, googleCloudStorageFirebaseStorageAccessorDriver, googleCloudStorageFromFirebaseAdminStorage, googleCloudStorageListFilesResultFactory, handleFirebaseAuthError, handleFirebaseError, hasAuthRolesInRequest, hasNewUserSetupPasswordInRequest, hasSignedTosInRequest, inAuthContext, injectNestApplicationContextIntoRequest, injectNestIntoRequest, internalServerError, invokeModelUnknownModelTypeError, isActualSpecifier, isAdminInRequest, isAdminOrTargetUserInRequestData, isContextWithAuthData, isFirebaseError, isFirebaseHttpsError, isOnCallCrudModelApiDetails, isOnCallHandlerApiDetails, isOnCallModelTypeApiDetails, isOnCallSpecifierApiDetails, makeBlockingFunctionWithHandler, makeOnScheduleHandlerWithNestApplicationRequest, makeScheduledFunctionDevelopmentFunction, modelAccessReadErrorFromUseMultipleModelsFailure, modelApiModuleMetadata, modelNotAvailableError, nestAppHasDevelopmentSchedulerEnabled, nestAppIsProductionEnvironment, nestFirebaseDoesNotExistError, nestFirebaseForbiddenPermissionError, nestServerInstance, noRunNameSpecifiedForScheduledFunctionDevelopmentFunction, noopFirebaseServerAnalyticsServiceListener, noopOnCallModelAnalyticsService, notFoundError, onCallAnalyticsEmitterInstance, onCallCreateModel, onCallDeleteModel, onCallDevelopmentFunction, onCallHandlerWithNestApplicationFactory, onCallHandlerWithNestContextFactory, onCallInvokeModel, onCallModel, onCallModelMissingCallTypeError, onCallModelUnknownCallTypeError, onCallQueryModel, onCallReadModel, onCallSpecifierHandler, onCallUpdateModel, onScheduleHandlerWithNestApplicationFactory, onScheduleHandlerWithNestContextFactory, optionalAuthContext, optionalFirestoreEncryptedField, permissionDeniedError, phoneNumberAlreadyExistsError, preconditionConflictError, provideAppFirestoreCollections, provideFirebaseServerAuthService, provideFirebaseServerStorageService, queryModelBadCursorError, queryModelUnknownModelTypeError, readApiDetails, readModelUnknownModelTypeError, resolveAdminOnlyValue, resolveAnalyticsFromApiDetails, resolveRequiredScopeFromApiDetails, setNestContextOnRequest, setNestContextOnScheduleRequest, taskQueueFunctionHandlerWithNestContextFactory, unauthenticatedContextHasNoAuthData, unauthenticatedContextHasNoUidError, unauthenticatedError, unavailableError, unavailableOrDeactivatedFunctionError, unknownModelCrudFunctionSpecifierError, unknownScheduledFunctionDevelopmentFunctionName, unknownScheduledFunctionDevelopmentFunctionType, updateModelUnknownModelTypeError, userContextFromUid, verifyAppCheckInRequest, withApiDetails };
13245
+ export { AbstractFirebaseNestContext, AbstractFirebaseServerActionsContext, AbstractFirebaseServerAuthContext, AbstractFirebaseServerAuthService, AbstractFirebaseServerAuthUserContext, AbstractFirebaseServerNewUserService, AbstractFirebaseServerUserPasswordResetService, AbstractNestContext, AbstractServerFirebaseNestContext, ConfigureFirebaseAppCheckMiddlewareModule, ConfigureFirebaseWebhookMiddlewareModule, DEFAULT_FIREBASE_PASSWORD_NUMBER_GENERATOR, DEFAULT_RESET_CODE_EXPIRES_IN, DEFAULT_RESET_COM_THROTTLE_TIME, DEFAULT_SERVER_ASSETS_BASE_PATH, DEFAULT_SETUP_COM_THROTTLE_TIME, DefaultFirebaseServerEnvService, FIREBASE_APP_TOKEN, FIREBASE_AUTH_TOKEN, FIREBASE_FIRESTORE_CONTEXT_TOKEN, FIREBASE_FIRESTORE_TOKEN, FIREBASE_SERVER_ENV_TOKEN, FIREBASE_SERVER_VALIDATION_ERROR_CODE, FIREBASE_STORAGE_CONTEXT_FACTORY_CONFIG_TOKEN, FIREBASE_STORAGE_CONTEXT_TOKEN, FIREBASE_STORAGE_TOKEN, FIRESTORE_CLIENT_QUERY_CONSTRAINT_HANDLER_MAPPING, FirebaseAppCheckMiddleware, FirebaseAppCheckMiddlewareConfig, FirebaseNestServerRootModule, FirebaseRawBodyMiddleware, FirebaseServerAnalyticsSegmentListenerService, FirebaseServerAnalyticsSegmentModule, FirebaseServerAnalyticsService, FirebaseServerAnalyticsServiceListener, FirebaseServerAuthModule, FirebaseServerAuthNewUserSendSetupDetailsNoSetupConfigError, FirebaseServerAuthNewUserSendSetupDetailsSendOnceError, FirebaseServerAuthNewUserSendSetupDetailsThrottleError, FirebaseServerAuthPasswordResetInvalidCodeError, FirebaseServerAuthPasswordResetNoResetConfigError, FirebaseServerAuthPasswordResetSendOnceError, FirebaseServerAuthPasswordResetThrottleError, FirebaseServerAuthService, FirebaseServerAuthUserBadInputError, FirebaseServerAuthUserExistsError, FirebaseServerEnvService, FirebaseServerFirestoreContextModule, FirebaseServerFirestoreModule, FirebaseServerStorageContextModule, FirebaseServerStorageModule, FirebaseServerStorageService, GlobalRoutePrefixConfig, MAX_MODEL_ACCESS_MULTI_READ_KEYS, MODEL_API_NEST_APPLICATION_CONTEXT, ModelApiCallModelDispatchService, ModelApiController, ModelApiDispatchConfig, ModelApiGetService, NO_RUN_NAME_SPECIFIED_FOR_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_CODE, NoContentFirebaseServerUserPasswordResetService, NoSetupContentFirebaseServerNewUserService, ON_CALL_MODEL_ANALYTICS_HANDLER, ON_CALL_MODEL_ANALYTICS_SERVICE, OnCallModelAnalyticsService, PHONE_NUMBER_ALREADY_EXISTS_ERROR_CODE, SkipAppCheck, UNKNOWN_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_NAME_CODE, UNKNOWN_SCHEDULED_FUNCTION_DEVELOPMENT_FUNCTION_TYPE_CODE, _onCallWithCallTypeFunction, aggregateCrudModelApiDetails, aggregateModelApiDetails, aggregateSpecifierApiDetails, alreadyExistsError, appAnalyticsModuleMetadata, appFirestoreModuleMetadata, assertContextHasAuth, assertDocumentExists, assertHasRolesInRequest, assertHasSignedTosInRequest, assertIsAdminInRequest, assertIsAdminOrTargetUserInRequestData, assertIsContextWithAuthData, assertModelApiOidcScope, assertRequestRequiresAuthForFunction, assertSnapshotData, assertSnapshotDataWithKey, authServicePasswordResetInvalidCodeError, authServicePasswordResetNoConfigError, authServicePasswordResetSendOnceError, authServicePasswordResetThrottleError, badRequestError, blockingFunctionHandlerWithNestContextFactory, buildNestServerRootModule, callWithAnalytics, catchAndThrowPasswordResetServerErrors, cloudEventHandlerWithNestContextFactory, collectionRefForPath, createModelUnknownModelTypeError, decodeFirebaseServerUserPasswordResetOobCode, defaultFirebaseServerActionsTransformFactoryLogErrorFunction, defaultProvideFirebaseServerStorageServiceSimple, deleteModelUnknownModelTypeError, developmentUnknownSpecifierError, docRefForPath, documentModelNotAvailableError, encodeFirebaseServerUserPasswordResetOobCode, executeOnCallQuery, firebaseAuthTokenFromDecodedIdToken, firebaseServerActionsContext, firebaseServerActionsTransformContext, firebaseServerActionsTransformFactory, firebaseServerAppTokenProvider, firebaseServerAuthModuleMetadata, firebaseServerDevFunctions, firebaseServerEnvTokenProvider, firebaseServerEnvTokenProviders, firebaseServerErrorInfo, firebaseServerErrorInfoCodePair, firebaseServerErrorInfoServerErrorCodePair, firebaseServerErrorInfoServerErrorPair, firebaseServerFirestoreContextModuleMetadata, firebaseServerStorageDefaultBucketIdTokenProvider, firebaseServerStorageModuleMetadata, firebaseServerValidationError, firebaseServerValidationServerError, firestoreClientQueryConstraintFunctionsDriver, firestoreEncryptedField, firestoreServerIncrementUpdateToUpdateData, forbiddenError, getAuthUserOrUndefined, getModelApiDetails, googleCloudFileMetadataToStorageMetadata, googleCloudFirebaseStorageContextFactory, googleCloudFirebaseStorageDrivers, googleCloudFirestoreAccessorDriver, googleCloudFirestoreContextFactory, googleCloudFirestoreDrivers, googleCloudFirestoreQueryDriver, googleCloudStorageAccessorFile, googleCloudStorageAccessorFolder, googleCloudStorageBucketForStorageFilePath, googleCloudStorageFileForStorageFilePath, googleCloudStorageFirebaseStorageAccessorDriver, googleCloudStorageFromFirebaseAdminStorage, googleCloudStorageListFilesResultFactory, handleFirebaseAuthError, handleFirebaseError, hasAuthRolesInRequest, hasNewUserSetupPasswordInRequest, hasSignedTosInRequest, inAuthContext, injectNestApplicationContextIntoRequest, injectNestIntoRequest, internalServerError, invokeModelUnknownModelTypeError, isActualSpecifier, isAdminInRequest, isAdminOrTargetUserInRequestData, isContextWithAuthData, isFirebaseError, isFirebaseHttpsError, isOnCallCrudModelApiDetails, isOnCallHandlerApiDetails, isOnCallModelTypeApiDetails, isOnCallSpecifierApiDetails, makeBlockingFunctionWithHandler, makeOnScheduleHandlerWithNestApplicationRequest, makeScheduledFunctionDevelopmentFunction, modelAccessReadErrorFromUseMultipleModelsFailure, modelApiModuleMetadata, modelNotAvailableError, nestAppHasDevelopmentSchedulerEnabled, nestAppIsProductionEnvironment, nestFirebaseDoesNotExistError, nestFirebaseForbiddenPermissionError, nestServerInstance, noRunNameSpecifiedForScheduledFunctionDevelopmentFunction, noopFirebaseServerAnalyticsServiceListener, noopOnCallModelAnalyticsService, notFoundError, oidcScopesFromModelApiAuth, onCallAnalyticsEmitterInstance, onCallCreateModel, onCallDeleteModel, onCallDevelopmentFunction, onCallHandlerWithNestApplicationFactory, onCallHandlerWithNestContextFactory, onCallInvokeModel, onCallModel, onCallModelMissingCallTypeError, onCallModelUnknownCallTypeError, onCallQueryModel, onCallReadModel, onCallSpecifierHandler, onCallUpdateModel, onScheduleHandlerWithNestApplicationFactory, onScheduleHandlerWithNestContextFactory, optionalAuthContext, optionalFirestoreEncryptedField, permissionDeniedError, phoneNumberAlreadyExistsError, preconditionConflictError, provideAppFirestoreCollections, provideFirebaseServerAuthService, provideFirebaseServerStorageService, queryModelBadCursorError, queryModelUnknownModelTypeError, readApiDetails, readModelUnknownModelTypeError, resolveAdminOnlyValue, resolveAnalyticsFromApiDetails, resolveRequiredScopeFromApiDetails, setNestContextOnRequest, setNestContextOnScheduleRequest, taskQueueFunctionHandlerWithNestContextFactory, unauthenticatedContextHasNoAuthData, unauthenticatedContextHasNoUidError, unauthenticatedError, unavailableError, unavailableOrDeactivatedFunctionError, unknownModelCrudFunctionSpecifierError, unknownScheduledFunctionDevelopmentFunctionName, unknownScheduledFunctionDevelopmentFunctionType, updateModelUnknownModelTypeError, userContextFromUid, verifyAppCheckInRequest, withApiDetails };
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/mailgun",
3
- "version": "13.29.0",
3
+ "version": "13.30.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.29.0",
6
- "@dereekb/firebase": "13.29.0",
7
- "@dereekb/firebase-server": "13.29.0",
8
- "@dereekb/date": "13.29.0",
9
- "@dereekb/nestjs": "13.29.0",
10
- "@dereekb/model": "13.29.0",
11
- "@dereekb/rxjs": "13.29.0",
12
- "@dereekb/util": "13.29.0"
5
+ "@dereekb/analytics": "13.30.0",
6
+ "@dereekb/firebase": "13.30.0",
7
+ "@dereekb/firebase-server": "13.30.0",
8
+ "@dereekb/date": "13.30.0",
9
+ "@dereekb/nestjs": "13.30.0",
10
+ "@dereekb/model": "13.30.0",
11
+ "@dereekb/rxjs": "13.30.0",
12
+ "@dereekb/util": "13.30.0"
13
13
  },
14
14
  "exports": {
15
15
  "./package.json": "./package.json",
package/mcp/index.cjs.js CHANGED
@@ -4,12 +4,12 @@ var common = require('@nestjs/common');
4
4
  var node_fs = require('node:fs');
5
5
  var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
6
6
  var types_js = require('@modelcontextprotocol/sdk/types.js');
7
+ var firebase = require('@dereekb/firebase');
7
8
  var oidc = require('@dereekb/firebase-server/oidc');
8
9
  var util = require('@dereekb/util');
9
10
  var firebaseServer = require('@dereekb/firebase-server');
10
11
  var node_util = require('node:util');
11
12
  var model = require('@dereekb/model');
12
- var firebase = require('@dereekb/firebase');
13
13
  var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
14
14
 
15
15
  function _class_call_check$5(instance, Constructor) {
@@ -167,6 +167,22 @@ function _define_property$9(obj, key, value) {
167
167
  * fills with a short justification for the call. The value is forwarded to analytics and stripped
168
168
  * from the dispatched handler body. See {@link McpReasonParameterConfig}.
169
169
  */ _define_property$9(this, "reasonParameter", void 0);
170
+ /**
171
+ * Default OIDC scope group term required on EVERY `callModel` op, mirrored into MCP tool-list
172
+ * visibility, unless a finer term overrides it (a per-function `requiredScope`, then a
173
+ * {@link McpModuleConfig.modelRequiredScopes} entry). Threaded into the same
174
+ * `resolveEffectiveOidcScopeTerms` composition the server-side model-api scope gate
175
+ * (`assertModelApiOidcScope`) uses, so a tool is advertised only when the caller could actually invoke it.
176
+ *
177
+ * Set this to the SAME value passed to `ModelApiDispatchConfig.defaultRequiredScope` to
178
+ * keep tool visibility and enforcement in lockstep.
179
+ */ _define_property$9(this, "defaultRequiredScope", void 0);
180
+ /**
181
+ * Per-model OIDC scope group-term overrides, keyed by {@link FirestoreModelType}, mirrored into MCP
182
+ * tool-list visibility. Set this to the SAME value passed to
183
+ * `ModelApiDispatchConfig.modelRequiredScopes` so per-model / verb-keyed restrictions that
184
+ * confine a subset client apply identically to the advertised tool list.
185
+ */ _define_property$9(this, "modelRequiredScopes", void 0);
170
186
  };
171
187
  /**
172
188
  * NestJS injection token for the optional {@link McpAuthRoleReader} provider.
@@ -1400,6 +1416,8 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1400
1416
  var seenNames = new Set();
1401
1417
  var manifest = context === null || context === void 0 ? void 0 : context.manifest;
1402
1418
  var naming = context === null || context === void 0 ? void 0 : context.naming;
1419
+ var defaultRequiredScope = context === null || context === void 0 ? void 0 : context.defaultRequiredScope;
1420
+ var modelRequiredScopes = context === null || context === void 0 ? void 0 : context.modelRequiredScopes;
1403
1421
  var candidates = planMcpToolCandidates(apiDetails, naming);
1404
1422
  var clashCounts = countVisibleAutoNameClashes(candidates);
1405
1423
  var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
@@ -1411,6 +1429,8 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1411
1429
  clashCounts: clashCounts,
1412
1430
  options: options,
1413
1431
  manifest: manifest,
1432
+ defaultRequiredScope: defaultRequiredScope,
1433
+ modelRequiredScopes: modelRequiredScopes,
1414
1434
  seenNames: seenNames,
1415
1435
  outTools: tools,
1416
1436
  outNeverVisibleTools: neverVisibleTools,
@@ -1582,7 +1602,7 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1582
1602
  */ function buildToolFromCandidate(context) {
1583
1603
  var _clashCounts_get, _ref;
1584
1604
  var _handlerDetails_mcp, _handlerDetails_mcp1;
1585
- var candidate = context.candidate, clashCounts = context.clashCounts, options = context.options, manifest = context.manifest, seenNames = context.seenNames, outTools = context.outTools, outNeverVisibleTools = context.outNeverVisibleTools, outSkipped = context.outSkipped, outWarnings = context.outWarnings;
1605
+ var candidate = context.candidate, clashCounts = context.clashCounts, options = context.options, manifest = context.manifest, defaultRequiredScope = context.defaultRequiredScope, modelRequiredScopes = context.modelRequiredScopes, seenNames = context.seenNames, outTools = context.outTools, outNeverVisibleTools = context.outNeverVisibleTools, outSkipped = context.outSkipped, outWarnings = context.outWarnings;
1586
1606
  var modelType = candidate.modelType, callType = candidate.callType, handlerDetails = candidate.handlerDetails, specifier = candidate.specifier, dispatch = candidate.dispatch, modelSegment = candidate.modelSegment, overrideName = candidate.overrideName, baseName = candidate.baseName, classified = candidate.classified, isVisible = candidate.isVisible;
1587
1607
  // A visible auto-named tool whose preferred name is produced by more than one visible tool is
1588
1608
  // re-derived with the abbreviated call type so both survive on the wire. Overrides and hidden tools
@@ -1632,13 +1652,18 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1632
1652
  });
1633
1653
  }
1634
1654
  }
1635
- // Additive scope set: the per-verb call-type scope (model.<call>) plus any per-function scope
1636
- // declared via withApiDetails({ requiredScope }). Nullish entries drop out, so a non-CRUD call
1637
- // type with no per-function scope yields an empty set (no scope gate).
1638
- var requiredScopes = util.filterMaybeArrayValues([
1639
- resolveRequiredScope(callType),
1640
- handlerDetails.requiredScope
1641
- ]);
1655
+ // AND-of-ORs scope terms: the per-verb call-type scope (model.<call>) plus the effective group term,
1656
+ // resolved by the same precedence the server model-api scope gate uses (per-function requiredScope > per-model
1657
+ // requirement > module default) via the shared resolveEffectiveOidcScopeTerms — so tool visibility
1658
+ // tracks callability. Empty when no term applies (a non-CRUD call type with no per-function/model/
1659
+ // default term → no scope gate).
1660
+ var requiredScopeTerms = firebase.resolveEffectiveOidcScopeTerms({
1661
+ perVerbScope: resolveRequiredScope(callType),
1662
+ requiredScope: handlerDetails.requiredScope,
1663
+ modelRequirement: modelRequiredScopes === null || modelRequiredScopes === void 0 ? void 0 : modelRequiredScopes[modelType],
1664
+ call: callType,
1665
+ defaultRequiredScope: defaultRequiredScope
1666
+ });
1642
1667
  var effectiveReadOnly = resolveEffectiveReadOnly((_handlerDetails_mcp = handlerDetails.mcp) === null || _handlerDetails_mcp === void 0 ? void 0 : _handlerDetails_mcp.readOnly, callType);
1643
1668
  var annotations = resolveMcpToolAnnotations(effectiveReadOnly);
1644
1669
  var description = applyWriteMarker(baseDescription, annotations);
@@ -1647,20 +1672,20 @@ function _unsupported_iterable_to_array$9(o, minLen) {
1647
1672
  filterMetadata = {
1648
1673
  visibilityKind: 'declarative',
1649
1674
  rule: classified.rule,
1650
- requiredScopes: requiredScopes,
1675
+ requiredScopeTerms: requiredScopeTerms,
1651
1676
  effectiveReadOnly: effectiveReadOnly
1652
1677
  };
1653
1678
  } else if (classified.visibilityKind === 'dynamic') {
1654
1679
  filterMetadata = {
1655
1680
  visibilityKind: 'dynamic',
1656
1681
  visibilityFn: classified.visibilityFn,
1657
- requiredScopes: requiredScopes,
1682
+ requiredScopeTerms: requiredScopeTerms,
1658
1683
  effectiveReadOnly: effectiveReadOnly
1659
1684
  };
1660
1685
  } else {
1661
1686
  filterMetadata = {
1662
1687
  visibilityKind: classified.visibilityKind,
1663
- requiredScopes: requiredScopes,
1688
+ requiredScopeTerms: requiredScopeTerms,
1664
1689
  effectiveReadOnly: effectiveReadOnly
1665
1690
  };
1666
1691
  }
@@ -6163,7 +6188,9 @@ var McpServerFactoryService_1;
6163
6188
  } else {
6164
6189
  result = generateMcpToolDefinitions(apiDetails, undefined, {
6165
6190
  manifest: manifest,
6166
- naming: this._resolveToolNamingOptions()
6191
+ naming: this._resolveToolNamingOptions(),
6192
+ defaultRequiredScope: this.mcpConfig.defaultRequiredScope,
6193
+ modelRequiredScopes: this.mcpConfig.modelRequiredScopes
6167
6194
  });
6168
6195
  }
6169
6196
  this._cachedTools = result;
@@ -6632,7 +6659,7 @@ var McpServerFactoryService_1;
6632
6659
  * Synthesizes the same `{ auth: { token } }` shape that `getOidcScopesFromRequest`
6633
6660
  * expects post-dispatch, so the upstream helper stays the single source of scope parsing.
6634
6661
  * Returns `undefined` for non-OIDC callers (no `oidcValidatedToken.scope`) — the filter
6635
- * loop treats that as "skip scope enforcement", matching `oidcCallModelScopePreAssert`.
6662
+ * loop treats that as "skip scope enforcement", matching the model-api scope gate (`assertModelApiOidcScope`).
6636
6663
  *
6637
6664
  * @param ctx - The per-request context carrying the validated auth payload.
6638
6665
  * @returns The set of granted OIDC scopes, or `undefined` when scope enforcement should be skipped.
@@ -6676,9 +6703,7 @@ var McpServerFactoryService_1;
6676
6703
  for(var _iterator = tools[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
6677
6704
  var tool = _step.value;
6678
6705
  var filterMetadata = tool.filterMetadata;
6679
- if (scopes != null && filterMetadata.requiredScopes != null && !filterMetadata.requiredScopes.every(function(scope) {
6680
- return scopes.has(scope);
6681
- })) {
6706
+ if (scopes != null && filterMetadata.requiredScopeTerms != null && !firebase.oidcScopeTermsSatisfied(filterMetadata.requiredScopeTerms, scopes)) {
6682
6707
  continue;
6683
6708
  }
6684
6709
  if (readOnlyMode && filterMetadata.effectiveReadOnly !== true) {