@dereekb/firebase-server 14.4.0 → 14.5.1

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.
Files changed (39) hide show
  1. package/calcom/package.json +11 -10
  2. package/discord/package.json +14 -10
  3. package/index.esm.js +1295 -317
  4. package/mailgun/package.json +9 -9
  5. package/mcp/index.esm.js +483 -83
  6. package/mcp/package.json +12 -12
  7. package/mcp/src/lib/mcp.config.d.ts +73 -3
  8. package/mcp/src/lib/service/index.d.ts +1 -0
  9. package/mcp/src/lib/service/mcp.server.factory.d.ts +3 -2
  10. package/mcp/src/lib/service/mcp.tool-generator.d.ts +9 -0
  11. package/mcp/src/lib/service/tools/mcp.tool.cli-token.d.ts +62 -0
  12. package/model/package.json +13 -10
  13. package/oidc/index.esm.js +3186 -1456
  14. package/oidc/package.json +11 -11
  15. package/oidc/src/lib/controller/index.d.ts +3 -0
  16. package/oidc/src/lib/controller/oidc.cli-token.config.d.ts +323 -0
  17. package/oidc/src/lib/controller/oidc.cli-token.controller.d.ts +45 -0
  18. package/oidc/src/lib/controller/oidc.cli-token.service.d.ts +138 -0
  19. package/oidc/src/lib/controller/oidc.interaction.controller.d.ts +38 -0
  20. package/oidc/src/lib/middleware/oauth-auth.module.d.ts +8 -0
  21. package/oidc/src/lib/oidc.config.d.ts +9 -0
  22. package/oidc/src/lib/oidc.module.d.ts +1 -1
  23. package/oidc/src/lib/service/index.d.ts +1 -0
  24. package/oidc/src/lib/service/oidc.config.service.d.ts +8 -3
  25. package/oidc/src/lib/service/oidc.download-signer.d.ts +17 -0
  26. package/oidc/src/lib/service/oidc.jwt-signing.service.d.ts +35 -0
  27. package/package.json +13 -13
  28. package/src/lib/nest/controller/download/download.api.config.d.ts +203 -0
  29. package/src/lib/nest/controller/download/download.api.controller.d.ts +25 -0
  30. package/src/lib/nest/controller/download/download.api.module.d.ts +47 -0
  31. package/src/lib/nest/controller/download/download.api.service.d.ts +111 -0
  32. package/src/lib/nest/controller/download/index.d.ts +4 -0
  33. package/src/lib/nest/controller/index.d.ts +2 -0
  34. package/src/lib/nest/controller/request.ip.d.ts +60 -0
  35. package/test/index.esm.js +1 -0
  36. package/test/package.json +12 -12
  37. package/test/src/lib/oidc/oidc.test.flow.d.ts +10 -0
  38. package/twilio/package.json +8 -8
  39. package/zoho/package.json +14 -10
package/oidc/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/oidc",
3
- "version": "14.4.0",
3
+ "version": "14.5.1",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "peerDependencies": {
7
- "@dereekb/analytics": "14.4.0",
8
- "@dereekb/date": "14.4.0",
9
- "@dereekb/firebase": "14.4.0",
10
- "@dereekb/firebase-server": "14.4.0",
11
- "@dereekb/model": "14.4.0",
12
- "@dereekb/nestjs": "14.4.0",
13
- "@dereekb/oauth-resource": "14.4.0",
14
- "@dereekb/rxjs": "14.4.0",
15
- "@dereekb/util": "14.4.0",
16
- "@dereekb/zoho": "14.4.0",
7
+ "@dereekb/analytics": "14.5.1",
8
+ "@dereekb/date": "14.5.1",
9
+ "@dereekb/firebase": "14.5.1",
10
+ "@dereekb/firebase-server": "14.5.1",
11
+ "@dereekb/model": "14.5.1",
12
+ "@dereekb/nestjs": "14.5.1",
13
+ "@dereekb/oauth-resource": "14.5.1",
14
+ "@dereekb/rxjs": "14.5.1",
15
+ "@dereekb/util": "14.5.1",
16
+ "@dereekb/zoho": "14.5.1",
17
17
  "@nestjs/common": "^12.0.1",
18
18
  "@nestjs/config": "^12.0.0",
19
19
  "express": "^5.2.1",
@@ -1,3 +1,6 @@
1
+ export * from './oidc.cli-token.config';
2
+ export * from './oidc.cli-token.service';
3
+ export * from './oidc.cli-token.controller';
1
4
  export * from './oidc.interaction.controller';
2
5
  export * from './oidc.provider.controller';
3
6
  export * from './oidc.wellknown.controller';
@@ -0,0 +1,323 @@
1
+ import { type ISO8601DateString, type Maybe, type PromiseOrValue, type Seconds, type WebsiteUrl } from '@dereekb/util';
2
+ import { type FirebaseAuthUserId, type OidcScope, type OidcScopeTerm } from '@dereekb/firebase';
3
+ import { type FirebaseServerAuthData } from '@dereekb/firebase-server';
4
+ /**
5
+ * Path (relative to the OIDC issuer) of the CLI-token mint endpoint.
6
+ *
7
+ * Under the `oidc` controller prefix the route is `POST /oidc/cli-token`.
8
+ */
9
+ export declare const CLI_TOKEN_MINT_PATH_PART = "cli-token";
10
+ /**
11
+ * Path (relative to the OIDC issuer) of the one-time claim redemption endpoint.
12
+ *
13
+ * Under the `oidc` controller prefix the route is `POST /oidc/cli-token/claim`.
14
+ */
15
+ export declare const CLI_TOKEN_CLAIM_PATH_PART = "cli-token/claim";
16
+ /**
17
+ * Full path of the mint endpoint, as an app must list it in the OIDC module's `protectedPaths`.
18
+ *
19
+ * The mint reads `req.auth`, which only the bearer-token middleware populates. Note this is the
20
+ * MINT path specifically — {@link FIREBASE_SERVER_CLI_TOKEN_CLAIM_PATH} must NOT be protected, since
21
+ * the claim code IS the credential there and a bearer middleware would 401 the machine redeeming it.
22
+ *
23
+ * The OIDC routes are excluded from the global `/api` prefix (see
24
+ * `FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE`), so the path carries no `/api` prefix.
25
+ */
26
+ export declare const FIREBASE_SERVER_CLI_TOKEN_API_PROTECTED_PATH = "/oidc/cli-token";
27
+ /**
28
+ * Full path of the unauthenticated claim endpoint.
29
+ *
30
+ * Deliberately a SUFFIX of {@link FIREBASE_SERVER_CLI_TOKEN_API_PROTECTED_PATH}'s prefix, so an app
31
+ * that protects `/oidc/cli-token` by prefix would also protect this one. The middleware config
32
+ * therefore has to exclude it — see `OidcAuthMiddlewareConfig.unprotectedPaths`.
33
+ */
34
+ export declare const FIREBASE_SERVER_CLI_TOKEN_CLAIM_PATH = "/oidc/cli-token/claim";
35
+ /**
36
+ * Hard ceiling on a minted CLI credential's lifetime, in seconds (one hour).
37
+ *
38
+ * NEVER configurable upward: the credential is minted non-interactively by whatever already holds
39
+ * the session, and the child grant does not cascade-revoke with its parent (see
40
+ * {@link OidcCliTokenService}), so the short ceiling IS the mitigation.
41
+ */
42
+ export declare const MAX_CLI_TOKEN_TTL_SECONDS: Seconds;
43
+ /**
44
+ * Default lifetime requested for a minted CLI credential when the caller asks for none.
45
+ */
46
+ export declare const DEFAULT_CLI_TOKEN_TTL_SECONDS: Seconds;
47
+ /**
48
+ * Window a one-time claim code may be redeemed within, in seconds.
49
+ *
50
+ * Short by design: the code only has to survive the hop from the minting agent to the machine
51
+ * running the CLI, which is an immediate copy-paste or an automated follow-up command — not
52
+ * something a human sits on. The credential it unwraps lives its own (also short) life from the
53
+ * mint, so this window is purely the exposure of the code in transit and is kept near its floor.
54
+ */
55
+ export declare const CLI_TOKEN_CLAIM_TTL_SECONDS: Seconds;
56
+ /**
57
+ * Number of random bytes behind a claim code. 32 bytes base64url-encodes to 43 characters.
58
+ *
59
+ * Applies to the opaque-random code form. A JWT-shaped code (see {@link CliTokenMintResult.claimCode})
60
+ * would size itself from its payload instead, and should spend at least this much entropy on a `jti`.
61
+ */
62
+ export declare const CLI_TOKEN_CLAIM_CODE_BYTES = 32;
63
+ /**
64
+ * Scopes a minted CLI credential NEVER inherits, no matter what the minting session holds.
65
+ *
66
+ * - `token.cli` — no chaining: a handoff credential must not be able to mint another one.
67
+ * - `token.service` — no escalation: that scope is the 365-day, non-rotating tier, and a one-hour
68
+ * credential must not be able to trade itself up into one.
69
+ *
70
+ * Everything else the caller holds (including `session.firestore`) inherits normally — the child is
71
+ * exactly as privileged as its parent, minus these two.
72
+ */
73
+ export declare const CLI_TOKEN_DENIED_INHERITED_OIDC_SCOPES: readonly OidcScope[];
74
+ /**
75
+ * Scope always unioned into a minted credential's grant, so the refresh-token grant is coherent.
76
+ */
77
+ export declare const CLI_TOKEN_REQUIRED_OIDC_SCOPE: OidcScope;
78
+ /**
79
+ * The default {@link CliTokenApiModuleConfig.requiredScope}, re-exported for apps that want to widen
80
+ * it into an OR-group rather than replace it.
81
+ */
82
+ export declare const DEFAULT_CLI_TOKEN_REQUIRED_OIDC_SCOPE: OidcScopeTerm;
83
+ /**
84
+ * Signature for the predicate that authorizes a caller to mint a CLI credential.
85
+ *
86
+ * Receives the calling request's auth data (`undefined` for an unauthenticated request) and returns
87
+ * true when that caller may mint. Typically an admin check, e.g.
88
+ * `(auth) => authRoleClaimsService.toRoles(auth?.token ?? {}).has('admin')`.
89
+ *
90
+ * This is the LOAD-BEARING gate — the `token.cli` scope check beside it is defence in depth only,
91
+ * because a non-OIDC caller carries no `scope` claim and every enforcement site treats that as
92
+ * "skip". When no predicate is provided the endpoint fails closed for EVERY caller.
93
+ */
94
+ export type CliTokenAdminPredicate = (auth: Maybe<FirebaseServerAuthData>) => PromiseOrValue<boolean>;
95
+ /**
96
+ * NestJS injection token for the {@link CliTokenAdminPredicate} provider.
97
+ */
98
+ export declare const CLI_TOKEN_ADMIN_PREDICATE = "CLI_TOKEN_ADMIN_PREDICATE";
99
+ /**
100
+ * Resolver form of {@link CliTokenApiModuleConfig.cliClientId}.
101
+ *
102
+ * Invoked once per mint rather than read at boot, so an app may discover — or lazily provision — its
103
+ * CLI client after the DI graph was built. Returning a nullish value leaves the endpoint disabled for
104
+ * that call, exactly as an empty literal does.
105
+ *
106
+ * @returns The CLI client's `client_id`, or nullish when none is available.
107
+ */
108
+ export type CliTokenClientIdResolver = () => Promise<Maybe<string>> | Maybe<string>;
109
+ /**
110
+ * Resolves the configured CLI `client_id`, invoking the resolver form when one was supplied.
111
+ *
112
+ * Takes the configured VALUE rather than the whole config so it can be exercised — and reused —
113
+ * without standing up a module config. An empty result is normalized to `undefined`, so a blank
114
+ * literal disables the endpoint exactly as an absent one does.
115
+ *
116
+ * Memoization is deliberately NOT done here: the resolver is invoked once per mint so an app may
117
+ * provision its CLI client lazily, which means an app that must see a stable id has to memoize its
118
+ * own resolver (e.g. with `cachedGetter`).
119
+ *
120
+ * @param cliClientId - The configured value, in either the literal or the resolver form.
121
+ * @returns The resolved `client_id`, or `undefined` when the mint is not configured.
122
+ */
123
+ export declare function resolveCliTokenClientId(cliClientId: Maybe<string | CliTokenClientIdResolver>): Promise<Maybe<string>>;
124
+ /**
125
+ * Configuration for the CLI-token mint endpoint, supplied by the app via its dependency module.
126
+ *
127
+ * Without a config the endpoint is DISABLED: there is no safe default for `cliClientId`, and minting
128
+ * against the wrong client would hand the CLI a credential its own `client_id` cannot refresh.
129
+ */
130
+ export declare abstract class CliTokenApiModuleConfig {
131
+ /**
132
+ * The registered OAuth `client_id` of the app's CLI client — the same client the CLI's
133
+ * `auth setup` was configured with.
134
+ *
135
+ * REQUIRED. When absent (or unresolvable at the provider) the endpoint is disabled.
136
+ *
137
+ * May be a {@link CliTokenClientIdResolver} instead of a literal id, for an app that discovers or
138
+ * provisions its CLI client at runtime rather than reading a configured value. The resolver is
139
+ * invoked per mint, so it can return a value that did not exist when the DI graph was built.
140
+ */
141
+ readonly cliClientId: string | CliTokenClientIdResolver;
142
+ /**
143
+ * OIDC scope term an OIDC caller must hold to mint. Defaults to
144
+ * {@link DEFAULT_CLI_TOKEN_REQUIRED_OIDC_SCOPE}. Pass `null` to disable scope enforcement entirely
145
+ * (the admin predicate remains the real gate either way).
146
+ */
147
+ readonly requiredScope?: Maybe<Maybe<OidcScopeTerm>>;
148
+ /**
149
+ * Lifetime requested for a minted credential when the caller names none. Clamped to
150
+ * {@link MAX_CLI_TOKEN_TTL_SECONDS}. Defaults to {@link DEFAULT_CLI_TOKEN_TTL_SECONDS}.
151
+ */
152
+ readonly defaultTtlSeconds?: Seconds;
153
+ /**
154
+ * Whether a claim code may only be redeemed from the same IP that minted it. Defaults to FALSE.
155
+ *
156
+ * Defence in depth on top of the code's own controls (unguessable, single-use, short-lived): a code
157
+ * that leaks out of a transcript is useless from anywhere but the minting host.
158
+ *
159
+ * **Off by default because it is incompatible with the feature's primary use case.** A cloud-hosted
160
+ * agent provisioning a *different* machine mints and redeems from different addresses by
161
+ * construction, and enforcing a match would make that handoff impossible. Enable it only where the
162
+ * minting session and the redeeming machine are genuinely the same host.
163
+ *
164
+ * Two further limits worth knowing before relying on it. Behind NAT every host on the network shares
165
+ * one egress address, so the check passes for any of them — it is weakest exactly where several
166
+ * machines could race for the code. And the claim route is unauthenticated and proxied, so the
167
+ * observed address is only as trustworthy as the proxy that set `X-Forwarded-For`; where nothing
168
+ * rewrites that header it is caller-supplied and the binding is decorative.
169
+ */
170
+ readonly bindClaimToMintIp?: boolean;
171
+ /**
172
+ * The app's API base URL, echoed into the handoff bundle so a machine with no prior `auth setup`
173
+ * can bootstrap an env from the bundle alone.
174
+ */
175
+ readonly apiBaseUrl?: WebsiteUrl;
176
+ /**
177
+ * The CLI env name this deployment should be stored under, echoed into the handoff bundle.
178
+ *
179
+ * The SERVER is the only party that knows which deployment it is. Without this the redeeming CLI
180
+ * can only fall back to whatever env is already active locally, which has two bad outcomes: on a
181
+ * bare machine there is no active env at all and the redeem fails, and on a configured machine a
182
+ * code minted here can silently repoint an env named for a DIFFERENT deployment at this one.
183
+ *
184
+ * Name it after the deployment (`local`, `staging`, `prod`), not after the app.
185
+ */
186
+ readonly envName?: string;
187
+ }
188
+ /**
189
+ * Result of a successful mint. Carries ONLY the one-time claim code — never the refresh token, which
190
+ * would otherwise land in an MCP transcript and a shell history.
191
+ */
192
+ export interface CliTokenMintResult {
193
+ /**
194
+ * The one-time code to redeem at `POST /oidc/cli-token/claim`.
195
+ *
196
+ * OPAQUE to its holder: the CLI passes it back verbatim and reads nothing out of it. Today it is
197
+ * {@link CLI_TOKEN_CLAIM_CODE_BYTES} random bytes, base64url-encoded — a pointer at the stored
198
+ * claim, carrying no data of its own.
199
+ *
200
+ * It may instead become an encoded (or signed) JWT if the code ever has to carry information the
201
+ * redeeming machine needs BEFORE it can redeem — an issuer or API base URL it would otherwise have
202
+ * no way to learn. Nothing downstream assumes the random form: `cliTokenClaimDocumentId` normalizes
203
+ * the value into a document id, and base64url (which a compact JWS also uses) already satisfies it.
204
+ *
205
+ * Two properties must survive such a change. The code has to stay unguessable, so a JWT form must
206
+ * still carry its own random `jti` rather than being derivable from the mint's inputs; and a failed
207
+ * redemption must stay undifferentiated, so a self-describing code must not let a caller distinguish
208
+ * "expired" from "unknown" locally and turn the unauthenticated claim route into an oracle.
209
+ */
210
+ readonly claimCode: string;
211
+ /**
212
+ * When the CLAIM CODE stops being redeemable.
213
+ */
214
+ readonly claimExpiresAt: ISO8601DateString;
215
+ /**
216
+ * When the minted CREDENTIAL itself expires — always within {@link MAX_CLI_TOKEN_TTL_SECONDS}.
217
+ */
218
+ readonly expiresAt: ISO8601DateString;
219
+ /**
220
+ * The space-delimited scopes the minted credential carries.
221
+ */
222
+ readonly scope: string;
223
+ }
224
+ /**
225
+ * The credential bundle handed back by `POST /oidc/cli-token/claim`. Everything a bare machine needs
226
+ * to create a CLI env and be logged in.
227
+ */
228
+ export interface CliTokenHandoffBundle {
229
+ /**
230
+ * The uid the credential was minted for — the minting caller's own uid.
231
+ */
232
+ readonly uid: FirebaseAuthUserId;
233
+ /**
234
+ * The OIDC issuer the credential is valid at.
235
+ */
236
+ readonly issuer: WebsiteUrl;
237
+ /**
238
+ * The app's API base URL, when the app configured one. Lets a machine with no prior `auth setup`
239
+ * create the env from this bundle alone.
240
+ */
241
+ readonly apiBaseUrl?: WebsiteUrl;
242
+ /**
243
+ * The CLI env name this deployment should be stored under, when the app configured one via
244
+ * {@link CliTokenApiModuleConfig.envName}.
245
+ *
246
+ * Advisory: an explicit `--env` on the redeeming side still wins. It exists so the rendered
247
+ * one-line handoff command works on a machine with no config, and so a redeem cannot silently
248
+ * repoint an env named for another deployment.
249
+ */
250
+ readonly envName?: string;
251
+ /**
252
+ * The CLI OAuth client the refresh token is bound to. The CLI must present THIS `client_id` when
253
+ * refreshing — a refresh token is client-bound.
254
+ */
255
+ readonly clientId: string;
256
+ /**
257
+ * The refresh token itself. The only place it is ever returned.
258
+ */
259
+ readonly refreshToken: string;
260
+ /**
261
+ * The space-delimited scopes the credential carries.
262
+ */
263
+ readonly scope: string;
264
+ /**
265
+ * When the credential expires.
266
+ */
267
+ readonly expiresAt: ISO8601DateString;
268
+ }
269
+ /**
270
+ * Inputs to {@link resolveCliTokenScopes}.
271
+ */
272
+ export interface ResolveCliTokenScopesInput {
273
+ /**
274
+ * The scopes the MINTING caller holds. The minted credential can never exceed this set.
275
+ */
276
+ readonly callerScopes: Maybe<ReadonlySet<OidcScope>>;
277
+ /**
278
+ * An optional subset the caller asked for. May only NARROW `callerScopes` — any scope named here
279
+ * that the caller does not hold is dropped.
280
+ */
281
+ readonly requestedScopes?: Maybe<readonly OidcScope[]>;
282
+ }
283
+ /**
284
+ * Resolves the scope set a minted CLI credential carries.
285
+ *
286
+ * `granted = callerScopes ∩ (requestedSubset ?? callerScopes) − {token.cli, token.service} ∪ {offline_access}`
287
+ *
288
+ * The intersection is what makes the mint non-amplifying: the caller may narrow, never widen. The
289
+ * deny-list is the only asymmetry, and it exists so a one-hour credential cannot manufacture a
290
+ * longer-lived or further-minting one.
291
+ *
292
+ * @param input - The caller's scopes and the optional requested subset.
293
+ * @returns The space-delimited scope string to grant.
294
+ * @__NO_SIDE_EFFECTS__
295
+ */
296
+ export declare function resolveCliTokenScopes(input: ResolveCliTokenScopesInput): string;
297
+ /**
298
+ * Inputs to {@link resolveCliTokenTtlSeconds}.
299
+ */
300
+ export interface ResolveCliTokenTtlSecondsInput {
301
+ /**
302
+ * The TTL the caller asked for, if any.
303
+ */
304
+ readonly requestedTtlSeconds?: Maybe<Seconds>;
305
+ /**
306
+ * The app-configured default, used when the caller asks for none.
307
+ */
308
+ readonly defaultTtlSeconds?: Maybe<Seconds>;
309
+ /**
310
+ * Seconds of life remaining on the MINTING caller's own grant, when known. A child must never
311
+ * outlive its parent.
312
+ */
313
+ readonly parentRemainingSeconds?: Maybe<Seconds>;
314
+ }
315
+ /**
316
+ * Resolves a minted credential's lifetime as
317
+ * `min(requested ?? default, MAX_CLI_TOKEN_TTL_SECONDS, parentRemaining)`, floored at one second.
318
+ *
319
+ * @param input - The requested/default TTL and the parent grant's remaining life.
320
+ * @returns The TTL to mint with, in seconds.
321
+ * @__NO_SIDE_EFFECTS__
322
+ */
323
+ export declare function resolveCliTokenTtlSeconds(input: ResolveCliTokenTtlSecondsInput): Seconds;
@@ -0,0 +1,45 @@
1
+ import { type Request } from 'express';
2
+ import { type CliTokenHandoffBundle, type CliTokenMintResult } from './oidc.cli-token.config';
3
+ import { type ClaimCliTokenParams, type MintCliTokenParams, OidcCliTokenService } from './oidc.cli-token.service';
4
+ /**
5
+ * REST controller for the CLI credential handoff.
6
+ *
7
+ * Mounted at `oidc`, giving `POST /oidc/cli-token` and `POST /oidc/cli-token/claim`. It lives in
8
+ * `@dereekb/firebase-server/oidc` rather than beside the session API because minting needs
9
+ * `OidcService.getProvider()`, which `@dereekb/firebase-server` core does not have.
10
+ *
11
+ * **Registration order matters.** `OidcProviderController` also mounts at `oidc` and ends in an
12
+ * `@All('{*path}')` catch-all that proxies to the oidc-provider callback. Express matches routes in
13
+ * registration order, so this controller MUST be listed BEFORE `OidcProviderController` in
14
+ * `oidcModuleMetadata`'s `controllers` array — otherwise both routes are swallowed by the catch-all
15
+ * and answered by the provider as unknown endpoints.
16
+ *
17
+ * Auth on the mint route comes from the global OIDC bearer middleware, so
18
+ * `FIREBASE_SERVER_CLI_TOKEN_API_PROTECTED_PATH` must be listed in the OIDC module's
19
+ * `protectedPaths`. The claim route is unauthenticated BY DESIGN — the one-time code is the
20
+ * credential there — and because protection matches by prefix, the app must also list
21
+ * `FIREBASE_SERVER_CLI_TOKEN_CLAIM_PATH` in `unprotectedPaths`.
22
+ */
23
+ export declare class OidcCliTokenController {
24
+ private readonly cliTokenService;
25
+ constructor(cliTokenService: OidcCliTokenService);
26
+ /**
27
+ * Mints a short-lived CLI credential for the calling user and returns the one-time claim code.
28
+ *
29
+ * @param req - The Express request carrying auth credentials on `req.auth`.
30
+ * @param body - The optional requested scope subset + TTL.
31
+ * @returns The {@link CliTokenMintResult}. Never the refresh token itself.
32
+ */
33
+ mintCliToken(req: Request, body: MintCliTokenParams): Promise<CliTokenMintResult>;
34
+ /**
35
+ * Redeems a one-time claim code for the credential bundle it wraps.
36
+ *
37
+ * Unauthenticated by design: the machine redeeming the code has no credential yet — obtaining one
38
+ * is the entire point. Every failure returns the same generic error so the route is not an oracle.
39
+ *
40
+ * @param req - The Express request, read only for the caller's address.
41
+ * @param body - The claim code.
42
+ * @returns The {@link CliTokenHandoffBundle}.
43
+ */
44
+ claimCliToken(req: Request, body: ClaimCliTokenParams): Promise<CliTokenHandoffBundle>;
45
+ }
@@ -0,0 +1,138 @@
1
+ import { type Maybe, type Seconds } from '@dereekb/util';
2
+ import { type OidcScope } from '@dereekb/firebase';
3
+ import { type FirebaseServerAuthData } from '@dereekb/firebase-server';
4
+ import { OidcModuleConfig } from '../oidc.config';
5
+ import { OidcService } from '../service/oidc.service';
6
+ import { OidcEncryptionService } from '../service/oidc.encryption.service';
7
+ import { OidcServerFirestoreCollections } from '../model/model';
8
+ import { type CliTokenAdminPredicate, CliTokenApiModuleConfig, type CliTokenHandoffBundle, type CliTokenMintResult } from './oidc.cli-token.config';
9
+ /**
10
+ * Error code thrown when the caller is not authorized to mint a CLI credential.
11
+ */
12
+ export declare const CLI_TOKEN_FORBIDDEN_ERROR_CODE = "CLI_TOKEN_FORBIDDEN_ERROR";
13
+ /**
14
+ * Error code thrown when the app did not configure a usable CLI client, so minting is disabled.
15
+ */
16
+ export declare const CLI_TOKEN_DISABLED_ERROR_CODE = "CLI_TOKEN_DISABLED_ERROR";
17
+ /**
18
+ * Error code returned for EVERY failed claim redemption — not found, expired, and already-consumed
19
+ * alike. Deliberately undifferentiated so the unauthenticated claim endpoint is not an oracle a
20
+ * caller can probe for valid codes.
21
+ */
22
+ export declare const CLI_TOKEN_CLAIM_INVALID_ERROR_CODE = "CLI_TOKEN_CLAIM_INVALID_ERROR";
23
+ /**
24
+ * Body of `POST /oidc/cli-token`.
25
+ */
26
+ export interface MintCliTokenParams {
27
+ /**
28
+ * An optional subset of the caller's own scopes to grant. May only NARROW — a scope the caller
29
+ * does not hold is ignored.
30
+ */
31
+ readonly scopes?: Maybe<readonly OidcScope[]>;
32
+ /**
33
+ * An optional requested lifetime in seconds. Clamped to one hour and to the remaining life of the
34
+ * caller's own grant.
35
+ */
36
+ readonly ttlSeconds?: Maybe<Seconds>;
37
+ }
38
+ /**
39
+ * Body of `POST /oidc/cli-token/claim`.
40
+ */
41
+ export interface ClaimCliTokenParams {
42
+ readonly code: string;
43
+ }
44
+ /**
45
+ * Per-request context for a mint or claim, carrying what the transport layer observed.
46
+ */
47
+ export interface CliTokenRequestContext {
48
+ /**
49
+ * The calling client's address, as resolved by the controller.
50
+ */
51
+ readonly requestIp?: Maybe<string>;
52
+ }
53
+ /**
54
+ * Mints a short-lived CLI login credential for the caller, and redeems the one-time claim code that
55
+ * carries it.
56
+ *
57
+ * ## Security
58
+ *
59
+ * Gates apply in this order, mirroring `FirestoreSessionApiService`:
60
+ *
61
+ * 1. `auth?.uid` present, else unauthenticated.
62
+ * 2. The app-supplied {@link CliTokenAdminPredicate} — the load-bearing check. **Fails closed** when
63
+ * the app provides no predicate.
64
+ * 3. The `token.cli` OIDC scope — defence in depth only. It cannot stand alone: a non-OIDC caller
65
+ * carries no `scope` claim and every enforcement site treats that as "skip".
66
+ * 4. A configured, provider-resolvable CLI `client_id`, else the endpoint is disabled.
67
+ *
68
+ * The minted credential is never MORE privileged than the session that minted it: its scopes are the
69
+ * caller's own, minus `token.cli` / `token.service` (see {@link resolveCliTokenScopes}), and its TTL
70
+ * is capped at one hour AND at the remaining life of the caller's own grant.
71
+ *
72
+ * ## Accepted limitation — no revocation cascade
73
+ *
74
+ * A refresh token is CLIENT-BOUND: oidc-provider's `validateGrant` requires
75
+ * `grant.clientId === client.clientId`, so the parent grant (owned by the MCP client) cannot be
76
+ * reused for the CLI client and a **separate** Grant must be created. Revoking the parent session
77
+ * therefore does NOT cascade to the minted credential. The ≤1h cap is the mitigation; `auth logout
78
+ * --revoke` and the `deleteOidcToken` callModel action are the manual kill switches.
79
+ *
80
+ * Rotation is deliberately left ON (the scope is NOT added to `nonRotatingScopes`): the CLI client is
81
+ * a public PKCE client, so oidc-provider rotates the refresh token on every exchange and its reuse
82
+ * detection kills the whole grant if a stolen copy is replayed. That is why the credential must land
83
+ * in the CLI's PERSISTED token cache rather than the non-persisting `fromEnv` path.
84
+ */
85
+ export declare class OidcCliTokenService {
86
+ private readonly oidcService;
87
+ private readonly oidcModuleConfig;
88
+ private readonly encryptionService;
89
+ private readonly collections;
90
+ private readonly config?;
91
+ private readonly adminPredicate?;
92
+ private readonly _logger;
93
+ constructor(oidcService: OidcService, oidcModuleConfig: OidcModuleConfig, encryptionService: OidcEncryptionService, collections: OidcServerFirestoreCollections, config?: CliTokenApiModuleConfig | undefined, adminPredicate?: CliTokenAdminPredicate | undefined);
94
+ /**
95
+ * Mints a CLI credential for the calling user and returns the one-time claim code that unwraps it.
96
+ *
97
+ * @param auth - The authenticated request's auth data (`req.auth`).
98
+ * @param params - The optional requested scope subset + TTL.
99
+ * @returns The claim code and the minted credential's metadata. The refresh token itself is NOT returned.
100
+ * @throws {HttpsError} `401` with no uid, `403` when a gate rejects the caller, `400` when minting is disabled.
101
+ */
102
+ mintCliToken(auth: Maybe<FirebaseServerAuthData>, params: MintCliTokenParams, context?: CliTokenRequestContext): Promise<CliTokenMintResult>;
103
+ /**
104
+ * Redeems a one-time claim code, consuming it inside a Firestore transaction so a double-redeem
105
+ * loses the race rather than handing out the same credential twice.
106
+ *
107
+ * @param params - The claim code to redeem.
108
+ * @returns The handoff bundle: everything a bare machine needs to be logged in.
109
+ * @throws {HttpsError} A `404` with the SAME generic error for a missing, expired, or already-consumed code.
110
+ */
111
+ claimCliToken(params: ClaimCliTokenParams, context?: CliTokenRequestContext): Promise<CliTokenHandoffBundle>;
112
+ private _storeClaim;
113
+ private _claimInvalidError;
114
+ }
115
+ /**
116
+ * Generates a high-entropy, URL-safe one-time claim code.
117
+ *
118
+ * @returns A base64url-encoded random string of {@link CLI_TOKEN_CLAIM_CODE_BYTES} bytes.
119
+ */
120
+ export declare function generateCliTokenClaimCode(): string;
121
+ /**
122
+ * Prefix of the `oidcEntry` document id a pending CLI-token claim is stored under.
123
+ *
124
+ * Namespaced so a claim can never collide with an oidc-provider adapter entry, which keys documents
125
+ * by the provider's own token/grant ids.
126
+ */
127
+ export declare const CLI_TOKEN_CLAIM_DOCUMENT_ID_PREFIX = "cli-token-claim-";
128
+ /**
129
+ * Builds the `oidcEntry` document id for a claim code.
130
+ *
131
+ * The code IS the lookup key — there is no query — so a redeem is a single point read and a wrong
132
+ * code is indistinguishable from an expired one.
133
+ *
134
+ * @param code - The one-time claim code.
135
+ * @returns The namespaced document id.
136
+ * @__NO_SIDE_EFFECTS__
137
+ */
138
+ export declare function cliTokenClaimDocumentId(code: string): string;
@@ -69,6 +69,36 @@ export declare class OidcInteractionController {
69
69
  */
70
70
  private _verifyIdToken;
71
71
  }
72
+ /**
73
+ * Inputs to {@link withheldGatedScopesForClient}.
74
+ */
75
+ export interface WithheldGatedScopesForClientInput {
76
+ /**
77
+ * Every scope the authorization request named.
78
+ */
79
+ readonly requestedScopes: ReadonlySet<string>;
80
+ /**
81
+ * Every scope referenced by ANY provider profile — the "gated" set.
82
+ */
83
+ readonly profileGatedScopes: ReadonlySet<string>;
84
+ /**
85
+ * The scopes THIS client's resolved profiles unlock.
86
+ */
87
+ readonly clientUnlockedScopes: ReadonlySet<string>;
88
+ }
89
+ /**
90
+ * The requested scopes that are provider-profile gated and NOT unlocked by this client's profiles —
91
+ * the set a consent must withhold from the user and drop from the grant.
92
+ *
93
+ * A gated scope is not necessarily unavailable: one unlocked by a DEFAULT profile is reachable by
94
+ * every client, which `clientUnlockedScopes` already accounts for (it is resolved through
95
+ * `oidcClientProviderProfileScopes`, whose fallback is the registry's default profiles).
96
+ *
97
+ * @param input - The requested, gated, and client-unlocked scope sets.
98
+ * @returns The requested scopes to withhold, in request order.
99
+ * @__NO_SIDE_EFFECTS__
100
+ */
101
+ export declare function withheldGatedScopesForClient(input: WithheldGatedScopesForClientInput): string[];
72
102
  /**
73
103
  * Inputs for {@link resolveEffectiveSubset}.
74
104
  */
@@ -91,6 +121,12 @@ export interface ResolveEffectiveSubsetInput {
91
121
  * `reconsiderRejectedValues` — or it can never be granted on that Grant.
92
122
  */
93
123
  readonly alreadyEncountered?: readonly string[];
124
+ /**
125
+ * Entries to withhold from the grant no matter what else applies. Subtracted LAST, so a withheld
126
+ * entry lands in `rejected` even when the caller explicitly named it or an `alwaysGranted` rule
127
+ * would otherwise have added it. Withholding can only ever subtract, never widen a grant.
128
+ */
129
+ readonly withheld?: readonly string[];
94
130
  }
95
131
  /**
96
132
  * Resolves the effective set to grant — and the complementary set to
@@ -107,6 +143,8 @@ export interface ResolveEffectiveSubsetInput {
107
143
  * entry so oidc-provider does not re-prompt for them. Values in `alreadyEncountered` pass validation but
108
144
  * are not re-applied to the grant (no-op).
109
145
  * - Always-granted entries are union'd into `granted` (clamped to `missing`).
146
+ * - Withheld entries are subtracted last, so they end up in `rejected` even when the caller named
147
+ * them or an always-granted rule would otherwise have added them.
110
148
  *
111
149
  * @param input - Missing entries plus optional subset, always-granted, and already-encountered lists.
112
150
  * @returns `granted` to add to the grant and `rejected` to record on the grant.
@@ -33,6 +33,14 @@ export declare abstract class OidcAuthMiddlewareConfig {
33
33
  * since those are protected by AppCheck.
34
34
  */
35
35
  readonly protectedPaths: SlashPath[];
36
+ /**
37
+ * Path prefixes excluded from verification, checked BEFORE {@link protectedPaths}.
38
+ *
39
+ * Needed because protection matches by prefix: a deliberately-public sub-route of a protected
40
+ * prefix (e.g. `/oidc/cli-token/claim` under `/oidc/cli-token`) would otherwise be 401'd for
41
+ * exactly the callers it exists for.
42
+ */
43
+ readonly unprotectedPaths?: SlashPath[];
36
44
  /**
37
45
  * Absolute URL of the OAuth 2.0 Protected Resource Metadata document
38
46
  * (RFC 9728). When set, included as the `resource_metadata` parameter
@@ -367,6 +367,15 @@ export declare abstract class OidcModuleConfig {
367
367
  * since those are typically protected by AppCheck.
368
368
  */
369
369
  readonly protectedPaths?: SlashPath[];
370
+ /**
371
+ * Path prefixes EXCLUDED from bearer verification, checked before {@link protectedPaths}.
372
+ *
373
+ * Protection matches by prefix, so a public sub-route of a protected prefix is otherwise
374
+ * unreachable: `POST /oidc/cli-token/claim` is deliberately unauthenticated (the one-time claim
375
+ * code IS the credential there), yet it sits under the protected `/oidc/cli-token` prefix. Listing
376
+ * it here is what keeps it reachable.
377
+ */
378
+ readonly unprotectedPaths?: SlashPath[];
370
379
  /**
371
380
  * Map of recognized OAuth resource indicator URLs (RFC 8707) to their
372
381
  * {@link OidcResourceServerInfo}. When non-empty, oidc-provider's
@@ -101,7 +101,7 @@ export declare function oidcFirestoreCollectionsFactory(firestoreContext: Firest
101
101
  * Subset of {@link OidcModuleConfig} that consumers may override via
102
102
  * `oidcModuleMetadata`'s `config` or `configFactory`.
103
103
  */
104
- export type OidcModuleMetadataOverrides = Partial<Pick<OidcModuleConfig, 'issuer' | 'suppressBodyParserWarning' | 'renderError' | 'protectedPaths' | 'appOAuthInteractionPath' | 'appOAuthLoginUrlPart' | 'appOAuthConsentUrlPart' | 'tokenEndpointAuthMethods' | 'registrationEnabled' | 'trustProxy' | 'trustProxyInNonProduction' | 'tokenLifetimes' | 'maxRequestedLoginDuration' | 'minRequestedLoginDuration' | 'defaultRequestedLoginDuration' | 'resourceServers' | 'resourceMetadataUrl' | 'configureMcpResourceServer' | 'cors'>>;
104
+ export type OidcModuleMetadataOverrides = Partial<Pick<OidcModuleConfig, 'issuer' | 'suppressBodyParserWarning' | 'renderError' | 'protectedPaths' | 'unprotectedPaths' | 'appOAuthInteractionPath' | 'appOAuthLoginUrlPart' | 'appOAuthConsentUrlPart' | 'tokenEndpointAuthMethods' | 'registrationEnabled' | 'trustProxy' | 'trustProxyInNonProduction' | 'tokenLifetimes' | 'maxRequestedLoginDuration' | 'minRequestedLoginDuration' | 'defaultRequestedLoginDuration' | 'resourceServers' | 'resourceMetadataUrl' | 'configureMcpResourceServer' | 'cors'>>;
105
105
  export interface ProvideAppOidcModuleMetadataConfig extends Pick<ModuleMetadata, 'imports' | 'exports' | 'providers'> {
106
106
  /**
107
107
  * Module that exports the required dependencies for this module.
@@ -12,3 +12,4 @@ export * from './oidc.interaction.service';
12
12
  export * from './oidc.grant';
13
13
  export * from './oidc.interaction-policy';
14
14
  export * from './oidc.jwt-signing.service';
15
+ export * from './oidc.download-signer';