@dereekb/firebase-server 13.37.0 → 13.38.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/mcp/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/mcp",
3
- "version": "13.37.0",
3
+ "version": "13.38.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.37.0",
6
- "@dereekb/date": "13.37.0",
7
- "@dereekb/firebase": "13.37.0",
8
- "@dereekb/firebase-server": "13.37.0",
9
- "@dereekb/firebase-server/oidc": "13.37.0",
10
- "@dereekb/model": "13.37.0",
11
- "@dereekb/nestjs": "13.37.0",
12
- "@dereekb/rxjs": "13.37.0",
13
- "@dereekb/util": "13.37.0",
14
- "@dereekb/zoho": "13.37.0",
5
+ "@dereekb/analytics": "13.38.0",
6
+ "@dereekb/date": "13.38.0",
7
+ "@dereekb/firebase": "13.38.0",
8
+ "@dereekb/firebase-server": "13.38.0",
9
+ "@dereekb/firebase-server/oidc": "13.38.0",
10
+ "@dereekb/model": "13.38.0",
11
+ "@dereekb/nestjs": "13.38.0",
12
+ "@dereekb/rxjs": "13.38.0",
13
+ "@dereekb/util": "13.38.0",
14
+ "@dereekb/zoho": "13.38.0",
15
15
  "@modelcontextprotocol/node": "2.0.0",
16
16
  "@modelcontextprotocol/server": "2.0.0",
17
17
  "@nestjs/common": "^11.1.19",
@@ -1,4 +1,5 @@
1
- import { type AuthClaims, type AuthRoleSet } from '@dereekb/util';
1
+ import { type AuthClaims, type AuthRoleSet, type Maybe, type PromiseOrValue } from '@dereekb/util';
2
+ import { type FirebaseServerAuthData } from '@dereekb/firebase-server';
2
3
  import { type FirestoreModelType, type OidcModelScopeRequirement, type OidcScope, type OidcScopeTerm } from '@dereekb/firebase';
3
4
  /**
4
5
  * Default path the MCP Streamable HTTP transport is mounted at.
@@ -238,3 +239,23 @@ export type McpAuthRoleReader = (claims: AuthClaims) => AuthRoleSet;
238
239
  * NestJS injection token for the optional {@link McpAuthRoleReader} provider.
239
240
  */
240
241
  export declare const MCP_AUTH_ROLE_READER = "MCP_AUTH_ROLE_READER";
242
+ /**
243
+ * Signature for the optional predicate that authorizes `model-roles` calls which target another
244
+ * user's uid.
245
+ *
246
+ * `model-roles` resolves permissions for the calling user by default, which is always safe. Passing
247
+ * a `uid` asks the server "what can *that* user do here?" — an answer that leaks the target's
248
+ * effective access, so it is gated behind this app-supplied predicate rather than being open.
249
+ *
250
+ * Receives the calling request's auth data (`undefined` for an unauthenticated request) and returns
251
+ * true if that caller may resolve roles for arbitrary uids. Typically an admin check, e.g.
252
+ * `(auth) => authRoleClaimsService.toRoles(auth?.token ?? {}).has('admin')`.
253
+ *
254
+ * When no predicate is provided the `uid` parameter fails closed for every caller — the tool is
255
+ * still registered and still answers for the caller themselves.
256
+ */
257
+ export type McpModelRolesTargetUidPredicate = (auth: Maybe<FirebaseServerAuthData>) => PromiseOrValue<boolean>;
258
+ /**
259
+ * NestJS injection token for the optional {@link McpModelRolesTargetUidPredicate} provider.
260
+ */
261
+ export declare const MCP_MODEL_ROLES_TARGET_UID_PREDICATE = "MCP_MODEL_ROLES_TARGET_UID_PREDICATE";
@@ -7,6 +7,7 @@ export * from './mcp.server.factory';
7
7
  export * from './mcp.tool-generator';
8
8
  export * from './mcp.visibility';
9
9
  export * from './tools/mcp.tool.model-get';
10
+ export * from './tools/mcp.tool.model-roles';
10
11
  export * from './tools/mcp.tool.model-info';
11
12
  export * from './tools/mcp.tool.model-decode';
12
13
  export * from './tools/mcp.tool.enum-info';
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { type Request } from 'express';
3
3
  import { ModelApiCallModelDispatchService, ModelApiGetService, FirebaseServerStorageService, type FirebaseServerAuthData } from '@dereekb/firebase-server';
4
- import { McpModuleConfig, type McpAuthRoleReader } from '../mcp.config';
4
+ import { McpModuleConfig, type McpAuthRoleReader, type McpModelRolesTargetUidPredicate } from '../mcp.config';
5
5
  import { type McpAnalyticsService } from './analytics/mcp.analytics.handler';
6
6
  /**
7
7
  * Optional per-request context passed when invoking the MCP server through a
@@ -28,6 +28,7 @@ export declare class McpServerFactoryService {
28
28
  private readonly modelApiGetService?;
29
29
  private readonly roleReader?;
30
30
  private readonly storageService?;
31
+ private readonly modelRolesTargetUidPredicate?;
31
32
  private readonly _logger;
32
33
  private _cachedTools;
33
34
  private _cachedStaticTools;
@@ -42,7 +43,7 @@ export declare class McpServerFactoryService {
42
43
  private _warnedMissingRoleReader;
43
44
  private _resolvedReasonConfig?;
44
45
  private readonly _analyticsService;
45
- constructor(mcpConfig: McpModuleConfig, dispatchService: ModelApiCallModelDispatchService, modelApiGetService?: ModelApiGetService | undefined, roleReader?: McpAuthRoleReader | undefined, analyticsService?: McpAnalyticsService, storageService?: FirebaseServerStorageService | undefined);
46
+ constructor(mcpConfig: McpModuleConfig, dispatchService: ModelApiCallModelDispatchService, modelApiGetService?: ModelApiGetService | undefined, roleReader?: McpAuthRoleReader | undefined, analyticsService?: McpAnalyticsService, storageService?: FirebaseServerStorageService | undefined, modelRolesTargetUidPredicate?: McpModelRolesTargetUidPredicate | undefined);
46
47
  /**
47
48
  * Builds a configured MCP server with tool listing + dispatch handlers wired up.
48
49
  *
@@ -0,0 +1,95 @@
1
+ import { type Maybe, type PromiseOrValue } from '@dereekb/util';
2
+ import { type FirebaseAuthUserId, type FirestoreModelIdentity, type FirestoreModelKey, type FirestoreModelType } from '@dereekb/firebase';
3
+ import { type ModelAccessMultiRoleMapResult, type FirebaseServerAuthData } from '@dereekb/firebase-server';
4
+ import { type McpToolDefinition } from '../mcp.tool-generator';
5
+ /**
6
+ * Reserved tool name for the built-in `model-roles` static tool.
7
+ */
8
+ export declare const MODEL_ROLES_TOOL_NAME = "model-roles";
9
+ /**
10
+ * Synthetic call type used in the tool's dispatch identity. Distinct from `model-get`'s `get` so
11
+ * visibility predicates can target the two independently.
12
+ */
13
+ export declare const MODEL_ROLES_DISPATCH_CALL = "roles";
14
+ /**
15
+ * Synthetic model type used in the tool's dispatch identity. Mirrors {@link MODEL_GET_DISPATCH_MODEL_TYPE}
16
+ * — the tool isn't bound to one model type, so the literal "model" stands in.
17
+ */
18
+ export declare const MODEL_ROLES_DISPATCH_MODEL_TYPE = "model";
19
+ /**
20
+ * Maximum number of keys accepted per `model-roles` call. Role resolution runs the model's real
21
+ * permission delegate per key (which may itself read parent documents), so this is deliberately
22
+ * lower than the `model-get` batch size.
23
+ */
24
+ export declare const MCP_MODEL_ROLES_MAX_KEYS = 25;
25
+ /**
26
+ * Resolves granted role maps for a batch of keys. Signature matches
27
+ * `ModelApiGetService.readRoleMaps` so the service method can be passed directly.
28
+ */
29
+ export type McpModelRolesReadRoleMaps = (params: {
30
+ readonly modelType: FirestoreModelType;
31
+ readonly keys: FirestoreModelKey[];
32
+ readonly auth: Maybe<FirebaseServerAuthData>;
33
+ readonly targetUid?: Maybe<FirebaseAuthUserId>;
34
+ }) => Promise<ModelAccessMultiRoleMapResult>;
35
+ /**
36
+ * Lookup that resolves a `modelType` to its registered {@link FirestoreModelIdentity}, so bare ids
37
+ * can be promoted to full keys. Mirrors `McpModelGetResolveIdentity`.
38
+ */
39
+ export type McpModelRolesResolveIdentity = (modelType: FirestoreModelType, auth: Maybe<FirebaseServerAuthData>) => Maybe<FirestoreModelIdentity>;
40
+ /**
41
+ * Predicate authorizing a caller to resolve roles for a uid other than their own. Mirrors
42
+ * `McpModelRolesTargetUidPredicate` from the module config; redeclared here so this module does not
43
+ * depend on the config module.
44
+ */
45
+ export type McpModelRolesTargetUidCheck = (auth: Maybe<FirebaseServerAuthData>) => PromiseOrValue<boolean>;
46
+ /**
47
+ * Constructor dependencies for {@link createModelRolesTool}.
48
+ */
49
+ export interface CreateModelRolesToolDeps {
50
+ /**
51
+ * Resolves the granted role maps for a batch of keys.
52
+ */
53
+ readonly readRoleMaps: McpModelRolesReadRoleMaps;
54
+ /**
55
+ * Resolves the registered identity for a model type so bare ids can be promoted into full keys.
56
+ */
57
+ readonly resolveIdentity: McpModelRolesResolveIdentity;
58
+ /**
59
+ * Authorizes use of the `uid` parameter. Omitted means the parameter fails closed for everyone;
60
+ * the tool still answers for the calling user.
61
+ */
62
+ readonly canTargetOtherUids?: Maybe<McpModelRolesTargetUidCheck>;
63
+ }
64
+ /**
65
+ * Shape of the `model-roles` tool input.
66
+ */
67
+ export interface ModelRolesToolInput {
68
+ readonly modelType: string;
69
+ readonly keys: ReadonlyArray<string>;
70
+ readonly uid?: string;
71
+ }
72
+ /**
73
+ * Builds the built-in `model-roles` MCP tool definition.
74
+ *
75
+ * Answers "what is this user actually allowed to do with this document?" by running the same
76
+ * `roleMapForModel()` delegate the permission-checked read/write paths use, and returning the
77
+ * resolved roles instead of consuming them. Because the model's real delegate runs, derived and
78
+ * cascading roles appear exactly as the API grants them — there is no second implementation of the
79
+ * rules to drift.
80
+ *
81
+ * Two things it disambiguates that a plain read cannot:
82
+ * - **Missing vs. forbidden.** A key that resolves to a missing document returns
83
+ * `exists: false, roles: []` rather than an error, so "the document is not there" reads
84
+ * differently from "it is there and you have no access" (`exists: true, roles: []`).
85
+ * - **Full access.** Admin short-circuits that grant the full-access marker come back as
86
+ * `fullAccess: true` rather than as an opaque enumerated set.
87
+ *
88
+ * By default roles are resolved for the calling user. Passing `uid` resolves them for another user
89
+ * and is gated behind {@link CreateModelRolesToolDeps.canTargetOtherUids}.
90
+ *
91
+ * @param deps - Role-map reader, identity resolver, and the target-uid authorization predicate.
92
+ * @returns A statically-registered {@link McpToolDefinition} ready to be appended to the MCP
93
+ * server factory's tool registry.
94
+ */
95
+ export declare function createModelRolesTool(deps: CreateModelRolesToolDeps): McpToolDefinition;
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/model",
3
- "version": "13.37.0",
3
+ "version": "13.38.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.37.0",
6
- "@dereekb/date": "13.37.0",
7
- "@dereekb/firebase": "13.37.0",
8
- "@dereekb/firebase-server": "13.37.0",
9
- "@dereekb/model": "13.37.0",
10
- "@dereekb/nestjs": "13.37.0",
11
- "@dereekb/rxjs": "13.37.0",
12
- "@dereekb/util": "13.37.0",
5
+ "@dereekb/analytics": "13.38.0",
6
+ "@dereekb/date": "13.38.0",
7
+ "@dereekb/firebase": "13.38.0",
8
+ "@dereekb/firebase-server": "13.38.0",
9
+ "@dereekb/model": "13.38.0",
10
+ "@dereekb/nestjs": "13.38.0",
11
+ "@dereekb/rxjs": "13.38.0",
12
+ "@dereekb/util": "13.38.0",
13
13
  "@nestjs/common": "^11.1.19",
14
14
  "@nestjs/config": "^4.0.4",
15
15
  "archiver": "^7.0.1",
package/oidc/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/oidc",
3
- "version": "13.37.0",
3
+ "version": "13.38.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.37.0",
6
- "@dereekb/date": "13.37.0",
7
- "@dereekb/firebase": "13.37.0",
8
- "@dereekb/firebase-server": "13.37.0",
9
- "@dereekb/model": "13.37.0",
10
- "@dereekb/nestjs": "13.37.0",
11
- "@dereekb/rxjs": "13.37.0",
12
- "@dereekb/util": "13.37.0",
13
- "@dereekb/zoho": "13.37.0",
5
+ "@dereekb/analytics": "13.38.0",
6
+ "@dereekb/date": "13.38.0",
7
+ "@dereekb/firebase": "13.38.0",
8
+ "@dereekb/firebase-server": "13.38.0",
9
+ "@dereekb/model": "13.38.0",
10
+ "@dereekb/nestjs": "13.38.0",
11
+ "@dereekb/rxjs": "13.38.0",
12
+ "@dereekb/util": "13.38.0",
13
+ "@dereekb/zoho": "13.38.0",
14
14
  "@nestjs/common": "^11.1.19",
15
15
  "@nestjs/config": "^4.0.4",
16
16
  "express": "^5.2.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server",
3
- "version": "13.37.0",
3
+ "version": "13.38.0",
4
4
  "sideEffects": false,
5
5
  "exports": {
6
6
  "./test": {
@@ -69,17 +69,17 @@
69
69
  "types": "./src/index.d.ts",
70
70
  "peerDependencies": {
71
71
  "@cantoo/pdf-lib": "^2.6.5",
72
- "@dereekb/analytics": "13.37.0",
73
- "@dereekb/calcom": "13.37.0",
74
- "@dereekb/date": "13.37.0",
75
- "@dereekb/dbx-core": "13.37.0",
76
- "@dereekb/discord": "13.37.0",
77
- "@dereekb/firebase": "13.37.0",
78
- "@dereekb/model": "13.37.0",
79
- "@dereekb/nestjs": "13.37.0",
80
- "@dereekb/rxjs": "13.37.0",
81
- "@dereekb/util": "13.37.0",
82
- "@dereekb/zoho": "13.37.0",
72
+ "@dereekb/analytics": "13.38.0",
73
+ "@dereekb/calcom": "13.38.0",
74
+ "@dereekb/date": "13.38.0",
75
+ "@dereekb/dbx-core": "13.38.0",
76
+ "@dereekb/discord": "13.38.0",
77
+ "@dereekb/firebase": "13.38.0",
78
+ "@dereekb/model": "13.38.0",
79
+ "@dereekb/nestjs": "13.38.0",
80
+ "@dereekb/rxjs": "13.38.0",
81
+ "@dereekb/util": "13.38.0",
82
+ "@dereekb/zoho": "13.38.0",
83
83
  "@google-cloud/firestore": "^7.11.6",
84
84
  "@google-cloud/storage": "^7.19.0",
85
85
  "@modelcontextprotocol/node": "2.0.0",
@@ -14,15 +14,39 @@ export interface FirebaseServerEnvServiceRef<S extends FirebaseServerEnvService
14
14
  */
15
15
  export declare abstract class FirebaseServerEnvService {
16
16
  /**
17
- * Whether the server is running in a test/CI environment.
17
+ * Whether the server is running under a test runner / in CI.
18
+ *
19
+ * NOT the complement of {@link isProduction}. There are three distinct situations, and a local
20
+ * emulator run (`nx run <app>:serve`) is the third one — neither testing nor deployed:
21
+ *
22
+ * | situation | isTestingEnv | isProduction |
23
+ * | -------------------------- | ------------ | ------------ |
24
+ * | test / CI run | true | false |
25
+ * | local emulator `serve` | false | false |
26
+ * | deployed (staging or prod) | false | true |
27
+ *
28
+ * Use this only to branch on "a test runner is driving this" — seeded fixtures, relaxed timeouts,
29
+ * assertions. To decide whether real Firebase infrastructure is reachable, use {@link isProduction}:
30
+ * `!isTestingEnv` wrongly includes local emulator runs, which is a common source of calls to live
31
+ * backends (App Check, IAM signing, service-account discovery) that fail with no emulator behind them.
18
32
  */
19
33
  abstract readonly isTestingEnv: boolean;
20
34
  /**
21
- * Whether the server is running in production mode. (This may be true in both prod or a staging running as production).
35
+ * Whether the server is DEPLOYED — true for every deployed environment, including staging.
36
+ *
37
+ * This is the gate for "real Firebase infrastructure is reachable": services with no emulator
38
+ * (App Check token minting, IAM signing, GCE metadata service-account discovery) may only be
39
+ * called when this is true. It is false for both a test run and a local emulator `serve`, which is
40
+ * what makes it — and not `!isTestingEnv` — the correct check for those calls.
41
+ *
42
+ * Use {@link isStaging} to distinguish staging from true production.
22
43
  */
23
44
  abstract readonly isProduction: boolean;
24
45
  /**
25
- * Whether the server is running in a staging environment. isProduction is also typically true when this is true.
46
+ * Whether the server is running in a staging environment.
47
+ *
48
+ * Narrows within a deployed environment: {@link isProduction} is also true when this is true, since
49
+ * staging runs as production against real infrastructure.
26
50
  */
27
51
  abstract readonly isStaging: boolean;
28
52
  /**
@@ -0,0 +1,63 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type OidcScope, type OidcScopeTerm } from '@dereekb/firebase';
3
+ import { type FirebaseServerAuthData } from './auth.context.server';
4
+ /**
5
+ * Error code thrown by {@link assertEndpointOidcScope} when an OIDC caller is missing the scope a
6
+ * non-model endpoint requires.
7
+ *
8
+ * Distinct from the callModel-layer code so a client can tell "you cannot call this endpoint" apart
9
+ * from "you cannot make this model call".
10
+ */
11
+ export declare const MISSING_ENDPOINT_OIDC_SCOPE_ERROR_CODE = "MISSING_ENDPOINT_OIDC_SCOPE_ERROR";
12
+ /**
13
+ * Reads the set of OIDC scopes carried by an authenticated request, or `undefined` for a non-OIDC
14
+ * (regular Firebase ID-token) caller.
15
+ *
16
+ * The OIDC bearer-token middleware attaches the validated access-token claims at
17
+ * `auth.oidcValidatedToken` (with the space-delimited `scope` string); a non-OIDC caller has neither
18
+ * that field nor a `scope` on `auth.token`. Reading is defensive — the auth shape is only typed as
19
+ * {@link FirebaseServerAuthData} here, since the OIDC-specific `oidcValidatedToken` lives in the
20
+ * `@dereekb/firebase-server/oidc` sub-package this core layer cannot import — and the actual parse is
21
+ * delegated to the shared {@link oidcScopesFromScopeClaim} so there is no drift with
22
+ * `getOidcScopesFromRequest`.
23
+ *
24
+ * @param auth - The request auth data, or undefined for unauthenticated requests.
25
+ * @returns The granted scope set, or `undefined` when the request carries no OIDC `scope` claim.
26
+ */
27
+ export declare function oidcScopesFromRequestAuth(auth: Maybe<FirebaseServerAuthData>): Maybe<Set<OidcScope>>;
28
+ /**
29
+ * Inputs to {@link assertEndpointOidcScope}.
30
+ */
31
+ export interface AssertEndpointOidcScopeInput {
32
+ /**
33
+ * The scope term the endpoint requires — a single scope, or an OR-group satisfied by holding any
34
+ * one of its scopes. `undefined`/`null` imposes no requirement.
35
+ */
36
+ readonly requiredScope: Maybe<OidcScopeTerm>;
37
+ /**
38
+ * The scopes the caller was granted, or `undefined` for a non-OIDC caller (bypasses enforcement).
39
+ */
40
+ readonly grantedScopes: Maybe<ReadonlySet<OidcScope>>;
41
+ /**
42
+ * Human-readable endpoint identifier used in the thrown error message (e.g. `/api/session/firestore`).
43
+ */
44
+ readonly endpoint: string;
45
+ }
46
+ /**
47
+ * Enforces a single OIDC scope requirement for a plain (non-callModel) endpoint, throwing a `403`
48
+ * when an OIDC caller does not hold it.
49
+ *
50
+ * The non-model counterpart to `assertModelApiOidcScope`, which is unusable outside the model API
51
+ * because it requires a `{ call, modelType }` pair. Evaluation reuses the same shipped
52
+ * {@link oidcScopeTermSatisfied} primitive so a term means the same thing on both surfaces.
53
+ *
54
+ * Bypasses (no-op) when `grantedScopes` is `undefined` — i.e. a non-OIDC caller carrying a plain
55
+ * Firebase ID token, which has no `scope` claim to enforce against. **This is why scope-gating alone
56
+ * is never a sufficient gate**: an endpoint that must reject non-admins needs its own admin check,
57
+ * with the scope acting as defence in depth.
58
+ *
59
+ * @param input - The required term, the caller's granted scopes, and the endpoint name for the message.
60
+ * @throws A `403` forbidden error (code {@link MISSING_ENDPOINT_OIDC_SCOPE_ERROR_CODE}) when an OIDC
61
+ * caller does not satisfy the requirement.
62
+ */
63
+ export declare function assertEndpointOidcScope(input: AssertEndpointOidcScopeInput): void;
@@ -1,2 +1,4 @@
1
1
  export * from './auth.context.server';
2
+ export * from './api.scope';
2
3
  export * from './model';
4
+ export * from './session';
@@ -1,5 +1,6 @@
1
1
  import { type Maybe } from '@dereekb/util';
2
- import { type FirestoreModelIdentity, type FirestoreModelKey, type FirestoreModelType } from '@dereekb/firebase';
2
+ import { type GrantedRoleMap } from '@dereekb/model';
3
+ import { type FirebaseAuthUserId, type FirestoreModelIdentity, type FirestoreModelKey, type FirestoreModelType } from '@dereekb/firebase';
3
4
  import { type INestApplicationContext } from '@nestjs/common';
4
5
  import { ModelApiDispatchConfig } from './model.api.dispatch';
5
6
  import { type FirebaseServerAuthData } from '../auth.context.server';
@@ -7,6 +8,13 @@ import { type FirebaseServerAuthData } from '../auth.context.server';
7
8
  * Maximum number of keys allowed in a multi-read request.
8
9
  */
9
10
  export declare const MAX_MODEL_ACCESS_MULTI_READ_KEYS = 50;
11
+ /**
12
+ * Error code returned when a read targets a model the app declared SERVER-ONLY.
13
+ *
14
+ * Deliberately distinct from a permission error: nothing the caller can be granted will make this
15
+ * read succeed, so the message needs to say WHY rather than implying a missing role.
16
+ */
17
+ export declare const MODEL_IS_SERVER_ONLY_ERROR_CODE = "MODEL_IS_SERVER_ONLY";
10
18
  /**
11
19
  * Result of a single document access read.
12
20
  */
@@ -29,6 +37,90 @@ export interface ModelAccessReadError {
29
37
  readonly message: string;
30
38
  readonly code?: string;
31
39
  }
40
+ /**
41
+ * The resolved permission state for a single model key, as computed by that model's
42
+ * `roleMapForModel()` delegate for a specific user.
43
+ */
44
+ export interface ModelAccessRoleMapResult {
45
+ readonly key: FirestoreModelKey;
46
+ /**
47
+ * Whether the underlying document exists.
48
+ *
49
+ * Roles are only computed for documents that exist — `FirebaseModelPermissionServiceInstance`
50
+ * gates on `isUsableOutputForRoles(output) => output.exists`, so a missing document always
51
+ * resolves to an empty role set. Surfacing existence separately is what lets a caller tell
52
+ * "the document is not there" apart from "the document is there and you may not touch it";
53
+ * both otherwise present as `roles: []`.
54
+ */
55
+ readonly exists: boolean;
56
+ /**
57
+ * True when the model granted the full-access marker ({@link FULL_ACCESS_ROLE_KEY}) rather than
58
+ * an enumerated role set — the shape admin short-circuits like `fullAccessRoleMap()` produce.
59
+ * When true, {@link roles} is empty and every role is implicitly granted.
60
+ */
61
+ readonly fullAccess: boolean;
62
+ /**
63
+ * The granted role names, sorted. Empty when {@link fullAccess} is true or nothing was granted.
64
+ */
65
+ readonly roles: string[];
66
+ }
67
+ /**
68
+ * Result of a multi-key role-map resolution.
69
+ */
70
+ export interface ModelAccessMultiRoleMapResult {
71
+ /**
72
+ * The uid the roles were resolved for — the caller's own uid unless the request targeted
73
+ * another user. `undefined` for an unauthenticated resolution.
74
+ */
75
+ readonly uid?: string;
76
+ /**
77
+ * True when {@link uid} is someone other than the calling user.
78
+ */
79
+ readonly targeted: boolean;
80
+ readonly results: ModelAccessRoleMapResult[];
81
+ readonly errors: ModelAccessReadError[];
82
+ }
83
+ /**
84
+ * Input for {@link ModelApiGetService.readRoleMaps}.
85
+ */
86
+ export interface ModelAccessRoleMapParams {
87
+ readonly modelType: FirestoreModelType;
88
+ readonly keys: FirestoreModelKey[];
89
+ /**
90
+ * The calling request's auth data.
91
+ */
92
+ readonly auth: Maybe<FirebaseServerAuthData>;
93
+ /**
94
+ * Resolve roles as this user instead of the caller. Callers are responsible for authorizing
95
+ * this before passing it — the service performs no permission check of its own on the target.
96
+ */
97
+ readonly targetUid?: Maybe<FirebaseAuthUserId>;
98
+ }
99
+ /**
100
+ * Input for {@link modelAccessRoleMapResultFromGrantedRoles}.
101
+ */
102
+ export interface ModelAccessRoleMapResultParams {
103
+ readonly key: FirestoreModelKey;
104
+ /**
105
+ * The resolved `ContextGrantedModelRoles` for the key. Typed structurally (rather than importing
106
+ * the generic) so the mapper stays testable with a plain object.
107
+ */
108
+ readonly granted: {
109
+ readonly data?: Maybe<{
110
+ readonly exists?: boolean;
111
+ }>;
112
+ readonly roleMap: GrantedRoleMap<string>;
113
+ };
114
+ }
115
+ /**
116
+ * Maps a resolved `ContextGrantedModelRoles` into the flat {@link ModelAccessRoleMapResult} wire
117
+ * shape — collapsing the full-access marker into a boolean, dropping the structural marker keys, and
118
+ * sorting the enumerated role keys so output is stable across calls.
119
+ *
120
+ * @param input - The key and its resolved granted-roles result.
121
+ * @returns The flattened per-key permission state.
122
+ */
123
+ export declare function modelAccessRoleMapResultFromGrantedRoles(input: ModelAccessRoleMapResultParams): ModelAccessRoleMapResult;
32
124
  /**
33
125
  * Shape of a single failed-key entry from `useMultipleModels({ throwOnFirstError: false })`.
34
126
  * Exposed so the mapper below stays testable without a live nest context.
@@ -77,6 +169,29 @@ export declare class ModelApiGetService {
77
169
  * @param auth - The request's auth data (OIDC scopes are read from it).
78
170
  */
79
171
  private _assertReadScope;
172
+ /**
173
+ * Refuses a read of a model the app declared SERVER-ONLY, BEFORE `useModel` runs.
174
+ *
175
+ * The model API authorizes through `roleMapForModel` under the Admin SDK, which bypasses
176
+ * `firestore.rules` entirely. For a model the rules file deliberately leaves unmatched (or denies
177
+ * outright), those two systems disagree and the model API is the one handing out the document.
178
+ * Gating here — ahead of the role map, and on the ONE service both `/model/<type>/get` and the
179
+ * `model-get` MCP tool share — is what makes the two read paths agree.
180
+ *
181
+ * @param modelType - The Firestore model type being read.
182
+ * @param auth - The request's auth data; used to build the context the service lookup needs.
183
+ * @throws {HttpsError} `MODEL_IS_SERVER_ONLY` when the model opted in via `serverOnly`.
184
+ */
185
+ private _assertNotServerOnly;
186
+ /**
187
+ * Reads the `serverOnly` flag off the registered model service, tolerating a type whose service
188
+ * cannot be constructed (which `readDocument` then reports as an unknown model).
189
+ *
190
+ * @param modelType - The Firestore model type to inspect.
191
+ * @param auth - The request's auth data.
192
+ * @returns `true` when the model declared itself server-only.
193
+ */
194
+ private _isServerOnlyModel;
80
195
  /**
81
196
  * Returns the registered {@link FirestoreModelIdentity} for the given `modelType` string, or
82
197
  * `undefined` when no model of that type is registered.
@@ -114,6 +229,36 @@ export declare class ModelApiGetService {
114
229
  * @returns Results and errors for each requested key.
115
230
  */
116
231
  readDocuments(modelType: FirestoreModelType, keys: FirestoreModelKey[], auth: Maybe<FirebaseServerAuthData>): Promise<ModelAccessMultiReadResult>;
232
+ /**
233
+ * Resolves the granted role map for one or more keys of the same model type — i.e. "what is this
234
+ * user actually allowed to do with this document?".
235
+ *
236
+ * This is the same computation the permission-checked read path runs (`roleMapForModel()` on the
237
+ * model's registered service factory), but the resolved roles are returned instead of being
238
+ * consumed to allow/deny an operation. Because it runs the model's real delegate, derived and
239
+ * cascading roles are included exactly as the API would grant them.
240
+ *
241
+ * Per-key failures are captured in `errors` rather than thrown, matching {@link readDocuments}.
242
+ * A key that resolves to a missing document is NOT an error — it comes back with
243
+ * `exists: false, roles: []`, which is what distinguishes "not there" from "no access".
244
+ *
245
+ * @param params - Model type, keys, calling auth, and an optional target uid.
246
+ * @returns The per-key permission state plus the uid the roles were resolved for.
247
+ */
248
+ readRoleMaps(params: ModelAccessRoleMapParams): Promise<ModelAccessMultiRoleMapResult>;
249
+ /**
250
+ * Builds a synthetic {@link AuthData} for an arbitrary uid so role resolution can run *as* that
251
+ * user rather than as the caller.
252
+ *
253
+ * The target's custom claims are read from their Firebase Auth record and spread into the
254
+ * synthetic token, mirroring how Firebase merges custom claims into a real decoded ID token —
255
+ * so claim-reading permission delegates behave identically to a live request from that user.
256
+ *
257
+ * @param uid - The uid to resolve as.
258
+ * @returns An auth ref carrying the target user's claims.
259
+ * @throws {Error} If no Firebase Auth user exists for the uid.
260
+ */
261
+ private _makeAuthRefForUid;
117
262
  /**
118
263
  * Builds an {@link AuthDataRef} compatible with `useModel()` from the HTTP request auth.
119
264
  *
@@ -40,8 +40,9 @@ export interface ModelApiOidcScopeConfig {
40
40
  * `auth.oidcValidatedToken` (with the space-delimited `scope` string); a non-OIDC caller has neither
41
41
  * that field nor a `scope` on `auth.token`. Reading is defensive (the auth shape is only typed as
42
42
  * {@link FirebaseServerAuthData} here — the OIDC-specific `oidcValidatedToken` lives in the
43
- * `@dereekb/firebase-server/oidc` sub-package this core layer cannot import), delegating the actual
44
- * parse to the shared {@link oidcScopesFromScopeClaim} so there is no drift with `getOidcScopesFromRequest`.
43
+ * `@dereekb/firebase-server/oidc` sub-package this core layer cannot import), delegating entirely to the
44
+ * shared {@link oidcScopesFromRequestAuth} so there is no drift with `getOidcScopesFromRequest` or with
45
+ * the non-model endpoints that read scopes the same way.
45
46
  *
46
47
  * @param auth - The request auth data, or undefined for unauthenticated requests.
47
48
  * @returns The granted scope set, or `undefined` when the request carries no OIDC `scope` claim.
@@ -0,0 +1,4 @@
1
+ export * from './session.api.config';
2
+ export * from './session.api.service';
3
+ export * from './session.api.controller';
4
+ export * from './session.api.module';