@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.
@@ -0,0 +1,104 @@
1
+ import { type Maybe, type Milliseconds, type PromiseOrValue } from '@dereekb/util';
2
+ import { type OidcScopeTerm } from '@dereekb/firebase';
3
+ import { type FirebaseServerAuthData } from '../auth.context.server';
4
+ /**
5
+ * Route prefix the session API controller is mounted at. Under the `/api` global route prefix the
6
+ * routes become `/api/session/*`.
7
+ */
8
+ export declare const SESSION_API_ROUTE_PREFIX = "session";
9
+ /**
10
+ * Path (relative to the API base URL) of the direct-Firestore session endpoint.
11
+ *
12
+ * The `@dereekb/dbx-cli` client posts to `<apiBaseUrl>${FIRESTORE_SESSION_API_PATH}`.
13
+ */
14
+ export declare const FIRESTORE_SESSION_API_PATH = "/session/firestore";
15
+ /**
16
+ * Path prefix apps must add to their OIDC `protectedPaths` so the bearer-token middleware
17
+ * authenticates the session endpoint (today typically `['/api/model', '/mcp']`).
18
+ *
19
+ * Without this the endpoint is reachable unauthenticated — `req.auth` would be `undefined` and the
20
+ * request rejected as unauthenticated, but the gate belongs at the middleware, not the handler.
21
+ */
22
+ export declare const FIREBASE_SERVER_SESSION_API_PROTECTED_PATH = "/api/session";
23
+ /**
24
+ * Default lifetime requested for the minted App Check token. Matches the Admin SDK's own default.
25
+ */
26
+ export declare const DEFAULT_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS: Milliseconds;
27
+ /**
28
+ * Minimum lifetime the Admin SDK accepts for an App Check token (30 minutes).
29
+ */
30
+ export declare const MIN_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS: Milliseconds;
31
+ /**
32
+ * Maximum lifetime the Admin SDK accepts for an App Check token (7 days).
33
+ *
34
+ * Deliberately NOT the default: this endpoint mints a valid web-app App Check attestation for
35
+ * whoever calls it, so the shortest workable lifetime is the right one.
36
+ */
37
+ export declare const MAX_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS: Milliseconds;
38
+ /**
39
+ * Window a Firebase Auth custom token may be exchanged for an ID token within (1 hour, fixed by
40
+ * Firebase). The exchanged ID token then lives its own hour from sign-in.
41
+ */
42
+ export declare const FIREBASE_CUSTOM_TOKEN_EXCHANGE_WINDOW_MILLIS: Milliseconds;
43
+ /**
44
+ * Signature for the predicate that authorizes a caller to open a direct-Firestore session.
45
+ *
46
+ * Receives the calling request's auth data (`undefined` for an unauthenticated request) and returns
47
+ * true when that caller may be handed a custom token + App Check attestation. Typically an admin
48
+ * check, e.g. `(auth) => authRoleClaimsService.toRoles(auth?.token ?? {}).has('admin')`.
49
+ *
50
+ * This is the LOAD-BEARING gate. `assertIsAdminInRequest` cannot be used here — it needs a
51
+ * `NestContextCallableRequestWithOptionalAuth` with `.nest` attached, and a plain Nest controller
52
+ * only has an Express request carrying `req.auth` — so the check is delegated to the app, keeping
53
+ * `@dereekb/firebase-server` app-agnostic about what "admin" means.
54
+ *
55
+ * When no predicate is provided the endpoint fails closed for EVERY caller.
56
+ */
57
+ export type FirestoreSessionAdminPredicate = (auth: Maybe<FirebaseServerAuthData>) => PromiseOrValue<boolean>;
58
+ /**
59
+ * NestJS injection token for the {@link FirestoreSessionAdminPredicate} provider.
60
+ */
61
+ export declare const FIRESTORE_SESSION_ADMIN_PREDICATE = "FIRESTORE_SESSION_ADMIN_PREDICATE";
62
+ /**
63
+ * Optional configuration for the session API module, supplied by the app via its dependency module.
64
+ *
65
+ * Everything here is optional — with no config provided the endpoint still mints custom tokens, just
66
+ * without an App Check attestation (correct for a project that does not enforce App Check, and for
67
+ * emulator-backed local development).
68
+ */
69
+ export declare abstract class SessionApiModuleConfig {
70
+ /**
71
+ * The Firebase `appId` of the registered **web** app to mint App Check tokens for. This is the same
72
+ * `appId` the browser client initializes with — the attestation must match an app the project's
73
+ * App Check enforcement recognizes.
74
+ *
75
+ * When unset, no App Check token is minted and the response's `appCheckToken` is omitted.
76
+ */
77
+ readonly appCheckAppId?: string;
78
+ /**
79
+ * Lifetime to request for the minted App Check token. Clamped to
80
+ * [{@link MIN_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS}, {@link MAX_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS}].
81
+ *
82
+ * Defaults to {@link DEFAULT_FIRESTORE_SESSION_APP_CHECK_TTL_MILLIS}.
83
+ */
84
+ readonly appCheckTokenTtlMillis?: Milliseconds;
85
+ /**
86
+ * OIDC scope term an OIDC caller must hold to open a session. Defaults to
87
+ * {@link FIRESTORE_SESSION_OIDC_SCOPE}. Pass `null` to disable scope enforcement entirely (the admin
88
+ * predicate remains the real gate either way).
89
+ */
90
+ readonly requiredScope?: Maybe<OidcScopeTerm>;
91
+ }
92
+ /**
93
+ * Resolves the effective App Check TTL from the module config, clamped to the Admin SDK's accepted range.
94
+ *
95
+ * @param ttlMillis - The configured TTL, or `undefined` to use the default.
96
+ * @returns The TTL to request, in milliseconds.
97
+ * @__NO_SIDE_EFFECTS__
98
+ */
99
+ export declare function firestoreSessionAppCheckTtlMillis(ttlMillis: Maybe<Milliseconds>): Milliseconds;
100
+ /**
101
+ * The default {@link SessionApiModuleConfig.requiredScope}, re-exported for apps that want to widen it
102
+ * into an OR-group rather than replace it.
103
+ */
104
+ export declare const DEFAULT_FIRESTORE_SESSION_REQUIRED_OIDC_SCOPE: OidcScopeTerm;
@@ -0,0 +1,26 @@
1
+ import { type Request } from 'express';
2
+ import { FirestoreSessionApiService, type FirestoreSessionResult } from './session.api.service';
3
+ /**
4
+ * REST controller that hands an authenticated admin caller the credentials needed to talk to
5
+ * Firestore directly, rather than only through the model HTTP API.
6
+ *
7
+ * Mounted at `session` — under the `/api` global prefix the route becomes `GET /api/session/firestore`.
8
+ *
9
+ * Auth comes from the global OIDC bearer middleware, so `'/api/session'` MUST be listed in the OIDC
10
+ * module's `protectedPaths` (see `FIREBASE_SERVER_SESSION_API_PROTECTED_PATH`). Follows `McpController`:
11
+ * there are no NestJS guards in this codebase — protection is path-prefix middleware plus the
12
+ * per-endpoint checks in {@link FirestoreSessionApiService}.
13
+ */
14
+ export declare class SessionApiController {
15
+ private readonly sessionService;
16
+ constructor(sessionService: FirestoreSessionApiService);
17
+ /**
18
+ * Mints a short-lived Firebase Auth custom token (+ App Check attestation, when configured) for the
19
+ * calling user.
20
+ *
21
+ * @param req - The Express request carrying auth credentials on `req.auth`.
22
+ * @returns The {@link FirestoreSessionResult} credential bundle.
23
+ */
24
+ getFirestoreSession(req: Request): Promise<FirestoreSessionResult>;
25
+ private _toHttpException;
26
+ }
@@ -0,0 +1,49 @@
1
+ import { type ModuleMetadata } from '@nestjs/common';
2
+ import { type ClassType } from '@dereekb/util';
3
+ /**
4
+ * Configuration for {@link sessionApiModuleMetadata}.
5
+ */
6
+ export interface SessionApiModuleMetadataConfig extends Pick<ModuleMetadata, 'imports' | 'exports' | 'providers'> {
7
+ /**
8
+ * Module that exports the session endpoint's dependencies.
9
+ *
10
+ * Should provide:
11
+ * - `FIRESTORE_SESSION_ADMIN_PREDICATE` — the app's admin check. Without it the endpoint rejects
12
+ * EVERY caller (fail-closed), and logs a warning at boot.
13
+ * - {@link SessionApiModuleConfig} — the registered web app's `appId` to mint App Check tokens for,
14
+ * plus optional TTL / scope overrides. Without it sessions carry no App Check attestation.
15
+ *
16
+ * `FIREBASE_APP_TOKEN` is not listed because `nestServerInstance` provides it globally.
17
+ */
18
+ readonly dependencyModule: ClassType;
19
+ }
20
+ /**
21
+ * Generates NestJS module metadata for the direct-Firestore session API.
22
+ *
23
+ * Mirrors the convention used by `modelApiModuleMetadata` and `mcpModuleMetadata`: the consumer
24
+ * provides a dependency module exposing the required tokens, and this factory wires the controller +
25
+ * service.
26
+ *
27
+ * Remember to add `FIREBASE_SERVER_SESSION_API_PROTECTED_PATH` (`'/api/session'`) to the OIDC module's
28
+ * `protectedPaths` — the controller relies on the bearer middleware having populated `req.auth`.
29
+ *
30
+ * @param metadataConfig - Configuration including the dependency module.
31
+ * @returns NestJS module metadata exposing the session controller + service.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * @Module({
36
+ * imports: [DemoApiAuthModule],
37
+ * providers: [
38
+ * { provide: SessionApiModuleConfig, useValue: { appCheckAppId: environment.firebase.appId } },
39
+ * { provide: FIRESTORE_SESSION_ADMIN_PREDICATE, useValue: (auth) => DEMO_AUTH_CLAIMS_SERVICE.toRoles(auth?.token ?? {}).has('admin') }
40
+ * ],
41
+ * exports: [SessionApiModuleConfig, FIRESTORE_SESSION_ADMIN_PREDICATE]
42
+ * })
43
+ * export class DemoSessionApiDependencyModule {}
44
+ *
45
+ * @Module(sessionApiModuleMetadata({ dependencyModule: DemoSessionApiDependencyModule }))
46
+ * export class DemoSessionApiModule {}
47
+ * ```
48
+ */
49
+ export declare function sessionApiModuleMetadata(metadataConfig: SessionApiModuleMetadataConfig): ModuleMetadata;
@@ -0,0 +1,71 @@
1
+ import type * as admin from 'firebase-admin';
2
+ import { type ISO8601DateString, type Maybe } from '@dereekb/util';
3
+ import { type FirebaseAuthUserId } from '@dereekb/firebase';
4
+ import { type FirebaseServerAuthData } from '../auth.context.server';
5
+ import { type FirestoreSessionAdminPredicate, SessionApiModuleConfig } from './session.api.config';
6
+ /**
7
+ * Error code thrown when the caller is not authorized to open a direct-Firestore session.
8
+ */
9
+ export declare const FIRESTORE_SESSION_FORBIDDEN_ERROR_CODE = "FIRESTORE_SESSION_FORBIDDEN_ERROR";
10
+ /**
11
+ * A short-lived credential bundle that lets a headless client connect directly to Firestore as the
12
+ * authenticated user, through the app's security rules.
13
+ */
14
+ export interface FirestoreSessionResult {
15
+ /**
16
+ * The uid the session was minted for — the calling user's real Firebase Auth uid.
17
+ */
18
+ readonly uid: FirebaseAuthUserId;
19
+ /**
20
+ * A Firebase Auth custom token to exchange via `signInWithCustomToken`. The user's stored
21
+ * `setCustomUserClaims` claims are spread at the TOP LEVEL of the exchanged ID token, so security
22
+ * rules reading `request.auth.token.<claim>` behave exactly as they do for the browser app.
23
+ */
24
+ readonly customToken: string;
25
+ /**
26
+ * An App Check attestation minted for the project's registered web app, for clients that cannot run
27
+ * a browser-only attestation provider (reCAPTCHA v3). Feed it to `initializeAppCheck` through a
28
+ * `CustomProvider`.
29
+ *
30
+ * Omitted when the app did not configure {@link SessionApiModuleConfig.appCheckAppId}.
31
+ */
32
+ readonly appCheckToken?: string;
33
+ /**
34
+ * When the session as a whole stops being usable — the earliest expiry among its credentials.
35
+ * A long-running client should re-fetch rather than assume one session covers the whole job.
36
+ */
37
+ readonly expiresAt: ISO8601DateString;
38
+ }
39
+ /**
40
+ * Mints the direct-Firestore session credential bundle returned by `SessionApiController`.
41
+ *
42
+ * ## Security
43
+ *
44
+ * This service hands out a valid web-app App Check token and a custom token for the caller's own uid.
45
+ * Two gates apply, in order:
46
+ *
47
+ * 1. The app-supplied {@link FirestoreSessionAdminPredicate} — the load-bearing check. **Fails closed**
48
+ * when the app provides no predicate.
49
+ * 2. The OIDC scope requirement (default {@link FIRESTORE_SESSION_OIDC_SCOPE}) — defence in depth only.
50
+ * It cannot stand alone: a non-OIDC caller carries no `scope` claim and every enforcement site in
51
+ * this codebase treats that as "skip".
52
+ *
53
+ * The custom token is ALWAYS minted for `auth.uid`; there is no way to ask for someone else's session,
54
+ * so a granted session is exactly as privileged as the caller already is under Firestore rules.
55
+ */
56
+ export declare class FirestoreSessionApiService {
57
+ private readonly _logger;
58
+ private readonly _app;
59
+ private readonly _config;
60
+ private readonly _adminPredicate;
61
+ constructor(app: admin.app.App, config?: SessionApiModuleConfig, adminPredicate?: FirestoreSessionAdminPredicate);
62
+ /**
63
+ * Mints a direct-Firestore session for the calling user after enforcing the admin predicate and the
64
+ * OIDC scope requirement.
65
+ *
66
+ * @param auth - The authenticated request's auth data (`req.auth`).
67
+ * @returns The credential bundle the client needs to connect to Firestore as this user.
68
+ * @throws {HttpsError} A `401` when the request carries no uid, or a `403` when either gate rejects the caller.
69
+ */
70
+ createFirestoreSession(auth: Maybe<FirebaseServerAuthData>): Promise<FirestoreSessionResult>;
71
+ }
package/test/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/test",
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",
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
14
  "@google-cloud/firestore": "^7.11.6",
15
15
  "@google-cloud/storage": "^7.19.0",
16
16
  "@nestjs/common": "^11.1.19",
@@ -23,7 +23,7 @@
23
23
  "supertest": "^7.2.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@dereekb/nestjs": "13.37.0"
26
+ "@dereekb/nestjs": "13.38.0"
27
27
  },
28
28
  "exports": {
29
29
  "./package.json": "./package.json",
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/twilio",
3
- "version": "13.37.0",
3
+ "version": "13.38.0",
4
4
  "peerDependencies": {
5
- "@dereekb/date": "13.37.0",
6
- "@dereekb/firebase": "13.37.0",
7
- "@dereekb/firebase-server": "13.37.0",
8
- "@dereekb/model": "13.37.0",
9
- "@dereekb/nestjs": "13.37.0",
10
- "@dereekb/rxjs": "13.37.0",
11
- "@dereekb/util": "13.37.0"
5
+ "@dereekb/date": "13.38.0",
6
+ "@dereekb/firebase": "13.38.0",
7
+ "@dereekb/firebase-server": "13.38.0",
8
+ "@dereekb/model": "13.38.0",
9
+ "@dereekb/nestjs": "13.38.0",
10
+ "@dereekb/rxjs": "13.38.0",
11
+ "@dereekb/util": "13.38.0"
12
12
  },
13
13
  "exports": {
14
14
  "./package.json": "./package.json",
package/zoho/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/zoho",
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/model": "13.37.0",
8
- "@dereekb/nestjs": "13.37.0",
9
- "@dereekb/rxjs": "13.37.0",
10
- "@dereekb/firebase": "13.37.0",
11
- "@dereekb/firebase-server": "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/model": "13.38.0",
8
+ "@dereekb/nestjs": "13.38.0",
9
+ "@dereekb/rxjs": "13.38.0",
10
+ "@dereekb/firebase": "13.38.0",
11
+ "@dereekb/firebase-server": "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"