@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
@@ -90,9 +90,14 @@ export declare class OidcProviderConfigService {
90
90
  * provider does support those scopes, for the clients holding the profile. This narrower list is
91
91
  * for metadata a client treats as a request template — notably an MCP protected-resource
92
92
  * document's `scopes_supported`, which dynamic-registration clients (the Claude Code CLI) copy
93
- * verbatim onto `/authorize`. An assignment-only scope advertised there ends the flow in
94
- * `access_denied`: the consent unlock gate judges the REQUEST, so unlike an admin-only scope
95
- * there is no deselect-at-consent way through.
93
+ * verbatim onto `/authorize`. Defaulting such a document to this narrower list keeps an
94
+ * unassigned client from requesting a scope it can never be granted.
95
+ *
96
+ * A resource MAY still advertise an assignment-only scope deliberately (demo-api does for
97
+ * `token.cli`, so an MCP connector can ask for it at all). That is not fatal for the clients
98
+ * lacking the profile: the consent URL builder withholds the scope from them, so it lands in the
99
+ * submit's `rejected` set and the flow completes without it — the same treatment an admin-only
100
+ * scope gets for a non-admin.
96
101
  */
97
102
  readonly clientRequestableScopesSupported: string[];
98
103
  /**
@@ -0,0 +1,17 @@
1
+ import { type DownloadTokenSigner } from '@dereekb/firebase-server';
2
+ import { type OidcJwtSigningService } from './oidc.jwt-signing.service';
3
+ /**
4
+ * Adapts {@link OidcJwtSigningService} to the `@dereekb/firebase-server` {@link DownloadTokenSigner}
5
+ * interface, so the signed asset-download endpoint can use the OIDC provider's own JWKS without
6
+ * `@dereekb/firebase-server` taking a dependency on this package.
7
+ *
8
+ * Using the provider's JWKS is the point: it is already shared across function instances and already
9
+ * rotates, so a download capability token needs no new secret to distribute. The `typ` + `aud`
10
+ * discriminators the download module passes in are what keep such a token from ever being accepted
11
+ * as an OAuth access token (and vice versa) even though both are signed by the same keys.
12
+ *
13
+ * @param signingService - The provider's JWT signing service.
14
+ * @returns A signer to register under the `DOWNLOAD_TOKEN_SIGNER` injection token.
15
+ * @__NO_SIDE_EFFECTS__
16
+ */
17
+ export declare function oidcDownloadTokenSigner(signingService: OidcJwtSigningService): DownloadTokenSigner;
@@ -1,4 +1,5 @@
1
1
  import { type Maybe, type Milliseconds, type Seconds } from '@dereekb/util';
2
+ import { type JWTPayload } from 'jose';
2
3
  import { OidcModuleConfig } from '../oidc.config';
3
4
  import { JwksService } from './oidc.jwks.service';
4
5
  /**
@@ -39,6 +40,23 @@ export interface OidcSignJwtInput {
39
40
  */
40
41
  readonly typ?: Maybe<string>;
41
42
  }
43
+ export interface OidcVerifyJwtInput {
44
+ /**
45
+ * The compact JWT to verify.
46
+ */
47
+ readonly token: string;
48
+ /**
49
+ * Required `aud` claim value.
50
+ */
51
+ readonly audience: string | readonly string[];
52
+ /**
53
+ * Required header `typ`. Pass the SAME discriminator the token was minted with — a first-party JWT
54
+ * minted for a non-OAuth purpose (e.g. an asset-download capability) is signed by the same JWKS as
55
+ * the provider's `at+jwt` access tokens, so `typ` + `aud` are what keep the two from being
56
+ * interchangeable.
57
+ */
58
+ readonly typ?: Maybe<string>;
59
+ }
42
60
  export interface OidcSignedJwt {
43
61
  readonly token: string;
44
62
  readonly expiresAt: Date;
@@ -65,6 +83,7 @@ export declare class OidcJwtSigningService {
65
83
  private readonly _jwks;
66
84
  private readonly _config;
67
85
  private _cached;
86
+ private _verifyKey;
68
87
  constructor(_jwks: JwksService, _config: OidcModuleConfig);
69
88
  /**
70
89
  * Mints a signed JWT.
@@ -73,5 +92,21 @@ export declare class OidcJwtSigningService {
73
92
  * @returns The compact token, its expiry, and the signing key id.
74
93
  */
75
94
  signJwt(input: OidcSignJwtInput): Promise<OidcSignedJwt>;
95
+ /**
96
+ * Verifies a JWT that THIS provider signed, against its own JWKS.
97
+ *
98
+ * Enforces `iss` (this provider), `aud`, and — when supplied — the header `typ`. Returns
99
+ * `undefined` rather than throwing when the token does not verify, so a caller serving an
100
+ * unauthenticated route can answer with one generic error for every failure mode.
101
+ *
102
+ * The counterpart to {@link signJwt}: `verifyAccessToken` deliberately does NOT accept a token
103
+ * minted with a non-`at+jwt` `typ` and a non-OAuth audience, and this method is how a purpose-built
104
+ * token proves it is the RIGHT kind.
105
+ *
106
+ * @param input - The token, its required audience, and its required `typ`.
107
+ * @returns The verified payload, or `undefined` when verification fails.
108
+ */
109
+ verifyJwt(input: OidcVerifyJwtInput): Promise<Maybe<JWTPayload>>;
110
+ private _getVerifyKey;
76
111
  private _loadSigningKey;
77
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server",
3
- "version": "14.4.0",
3
+ "version": "14.5.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -58,18 +58,18 @@
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@cantoo/pdf-lib": ">=2.6.5 <2.11.0",
61
- "@dereekb/analytics": "14.4.0",
62
- "@dereekb/calcom": "14.4.0",
63
- "@dereekb/date": "14.4.0",
64
- "@dereekb/dbx-core": "14.4.0",
65
- "@dereekb/discord": "14.4.0",
66
- "@dereekb/firebase": "14.4.0",
67
- "@dereekb/model": "14.4.0",
68
- "@dereekb/nestjs": "14.4.0",
69
- "@dereekb/oauth-resource": "14.4.0",
70
- "@dereekb/rxjs": "14.4.0",
71
- "@dereekb/util": "14.4.0",
72
- "@dereekb/zoho": "14.4.0",
61
+ "@dereekb/analytics": "14.5.1",
62
+ "@dereekb/calcom": "14.5.1",
63
+ "@dereekb/date": "14.5.1",
64
+ "@dereekb/dbx-core": "14.5.1",
65
+ "@dereekb/discord": "14.5.1",
66
+ "@dereekb/firebase": "14.5.1",
67
+ "@dereekb/model": "14.5.1",
68
+ "@dereekb/nestjs": "14.5.1",
69
+ "@dereekb/oauth-resource": "14.5.1",
70
+ "@dereekb/rxjs": "14.5.1",
71
+ "@dereekb/util": "14.5.1",
72
+ "@dereekb/zoho": "14.5.1",
73
73
  "@google-cloud/firestore": "^7.11.6",
74
74
  "@google-cloud/storage": "^7.22.0",
75
75
  "@modelcontextprotocol/node": "2.0.0",
@@ -0,0 +1,203 @@
1
+ import { type Maybe, type Seconds, type WebsiteUrl } from '@dereekb/util';
2
+ /**
3
+ * Route prefix the download controller is mounted at. Under the `/api` global route prefix the route
4
+ * becomes `GET /api/download`.
5
+ */
6
+ export declare const DOWNLOAD_API_ROUTE_PREFIX = "download";
7
+ /**
8
+ * Path (relative to the API base URL) of the signed asset-download endpoint.
9
+ */
10
+ export declare const DOWNLOAD_API_PATH = "/download";
11
+ /**
12
+ * Query parameter carrying the signed capability token.
13
+ */
14
+ export declare const DOWNLOAD_API_ASSET_QUERY_PARAM = "asset";
15
+ /**
16
+ * Folder, relative to the app's dist output, that holds every mintable asset.
17
+ *
18
+ * The downloadable set is exactly this ONE folder — nothing outside it is ever mintable, and the
19
+ * containment check in {@link resolveSecureAssetPath} is what enforces that.
20
+ */
21
+ export declare const DEFAULT_SECURE_ASSETS_DIRECTORY = "assets/secure";
22
+ /**
23
+ * `aud` claim of a download capability token.
24
+ *
25
+ * Distinct from every OAuth audience so a download token can never be presented as a bearer access
26
+ * token — `oidcProviderIssuerProfiles` only accepts the registered resource-server audiences and the
27
+ * issuer itself.
28
+ */
29
+ export declare const DOWNLOAD_TOKEN_AUDIENCE = "dbx:asset-download";
30
+ /**
31
+ * Header `typ` of a download capability token.
32
+ *
33
+ * **Load-bearing.** The same JWKS signs the provider's `at+jwt` access tokens, so this is the second
34
+ * half of the mutual rejection: the download verifier accepts ONLY this `typ` (plus
35
+ * {@link DOWNLOAD_TOKEN_AUDIENCE}), and the bearer path rejects a download token on its audience.
36
+ */
37
+ export declare const DOWNLOAD_TOKEN_TYP = "dbx-dl+jwt";
38
+ /**
39
+ * `sub` claim of a download capability token. The token carries NO user identifier — the caller is
40
+ * authorized at MINT time and the uid goes to the audit log there, so it never lands in a URL that
41
+ * ends up in shell history and proxy logs.
42
+ */
43
+ export declare const DOWNLOAD_TOKEN_SUBJECT = "dbx:asset";
44
+ /**
45
+ * Claim carrying the asset path, RELATIVE to the secure root. Never absolute.
46
+ */
47
+ export declare const DOWNLOAD_TOKEN_PATH_CLAIM = "p";
48
+ /**
49
+ * Default lifetime of a minted download URL.
50
+ */
51
+ export declare const DEFAULT_DOWNLOAD_TOKEN_TTL_SECONDS: Seconds;
52
+ /**
53
+ * Ceiling on a minted download URL's lifetime.
54
+ *
55
+ * A download URL is deliberately NOT one-time — a network blip partway through a multi-megabyte
56
+ * download would otherwise burn it — so the TTL is the whole control, with the token's `jti` logged
57
+ * so a replay is at least visible.
58
+ */
59
+ export declare const MAX_DOWNLOAD_TOKEN_TTL_SECONDS: Seconds;
60
+ /**
61
+ * The verified claims of a download capability token.
62
+ */
63
+ export interface DownloadTokenClaims {
64
+ /**
65
+ * The asset path, relative to the secure root.
66
+ */
67
+ readonly path: string;
68
+ /**
69
+ * The token's unique id, logged so a replay is visible.
70
+ */
71
+ readonly jti?: string;
72
+ }
73
+ /**
74
+ * Input to {@link DownloadTokenSigner.signToken}.
75
+ */
76
+ export interface DownloadTokenSignInput {
77
+ readonly claims: Record<string, unknown>;
78
+ readonly audience: string;
79
+ readonly subject: string;
80
+ readonly typ: string;
81
+ readonly expiresIn: Seconds;
82
+ }
83
+ /**
84
+ * Result of {@link DownloadTokenSigner.signToken}.
85
+ */
86
+ export interface DownloadTokenSignResult {
87
+ readonly token: string;
88
+ readonly expiresAt: Date;
89
+ }
90
+ /**
91
+ * Input to {@link DownloadTokenSigner.verifyToken}.
92
+ */
93
+ export interface DownloadTokenVerifyInput {
94
+ readonly token: string;
95
+ readonly audience: string;
96
+ readonly typ: string;
97
+ }
98
+ /**
99
+ * App-supplied JWT signer/verifier backing the download capability token.
100
+ *
101
+ * Kept as an interface so `@dereekb/firebase-server` takes no dependency on the OIDC package: the
102
+ * app adapts `OidcJwtSigningService` (whose keys are the provider's own already-rotating JWKS, so
103
+ * there is no new secret to distribute). Nothing outside the download module knows how the token is
104
+ * BUILT — the signer only signs and verifies whatever claims it is handed.
105
+ *
106
+ * Without a signer the endpoint fails closed and no URL can be minted.
107
+ */
108
+ export interface DownloadTokenSigner {
109
+ signToken(input: DownloadTokenSignInput): Promise<DownloadTokenSignResult>;
110
+ /**
111
+ * Verifies a token, returning its claims, or `undefined` when it is not a valid token of this
112
+ * `typ` + `audience` from this issuer.
113
+ */
114
+ verifyToken(input: DownloadTokenVerifyInput): Promise<Maybe<Record<string, unknown>>>;
115
+ }
116
+ /**
117
+ * NestJS injection token for the {@link DownloadTokenSigner} provider.
118
+ */
119
+ export declare const DOWNLOAD_TOKEN_SIGNER = "DOWNLOAD_TOKEN_SIGNER";
120
+ /**
121
+ * Configuration for the signed asset-download endpoint, supplied by the app via its dependency module.
122
+ *
123
+ * Without a resolvable {@link secureAssetsRoot} the endpoint fails closed for every request.
124
+ */
125
+ export declare abstract class DownloadApiModuleConfig {
126
+ /**
127
+ * Absolute path to the ONLY folder assets may be served from.
128
+ *
129
+ * Resolve it with the same three-cwd probe the dist manifests use — the Functions runtime, a
130
+ * workspace-root run, and vitest all set different `process.cwd()` values.
131
+ */
132
+ readonly secureAssetsRoot?: Maybe<string>;
133
+ /**
134
+ * The app's API base URL, used to build the absolute minted URL.
135
+ */
136
+ readonly apiBaseUrl?: Maybe<WebsiteUrl>;
137
+ /**
138
+ * Default lifetime for a minted URL. Clamped to {@link MAX_DOWNLOAD_TOKEN_TTL_SECONDS}. Defaults to
139
+ * {@link DEFAULT_DOWNLOAD_TOKEN_TTL_SECONDS}.
140
+ */
141
+ readonly defaultTtlSeconds?: Maybe<Seconds>;
142
+ }
143
+ /**
144
+ * Resolves an asset path, relative to the secure root, to the absolute file it names — or
145
+ * `undefined` when it escapes containment.
146
+ *
147
+ * **This function IS the security model for Part C.** With a folder root rather than a key registry,
148
+ * a mistake here is an arbitrary-file-read primitive on the function's filesystem, so it is enforced
149
+ * in BOTH places (at mint, so a URL for an outside file can never be obtained; and at download, so a
150
+ * bug in the minting path cannot become a read primitive) and both call sites share this one
151
+ * function so they cannot drift.
152
+ *
153
+ * Rejected up front: an absolute path, any `..` segment, a null byte, and an empty path. Then
154
+ * `realpath` is applied to BOTH sides — that is what stops a symlink inside `secure/` from pointing
155
+ * at `/etc` or at the function's `.env`. The final containment compare appends `path.sep` so a
156
+ * sibling directory sharing a name prefix (`secure-other/x`) cannot pass a naive `startsWith`.
157
+ *
158
+ * @param secureRoot - Absolute path to the secure root folder, or `undefined` when unconfigured (⇒ always fails).
159
+ * @param relativePath - The asset path relative to the secure root.
160
+ * @returns The absolute, real path of the file, or `undefined` when it is not a contained regular file.
161
+ * @__NO_SIDE_EFFECTS__
162
+ */
163
+ export declare function resolveSecureAssetPath(secureRoot: Maybe<string>, relativePath: Maybe<string>): Maybe<string>;
164
+ /**
165
+ * Returns true when a relative asset path is structurally safe to resolve — non-empty, relative, free
166
+ * of `..` segments, and free of null bytes.
167
+ *
168
+ * Split out from {@link resolveSecureAssetPath} so the structural rejection is testable without a
169
+ * filesystem, and so the reason a path is refused is one check rather than a compound condition.
170
+ *
171
+ * @param relativePath - The candidate path.
172
+ * @returns True when the path may be resolved against the secure root.
173
+ * @__NO_SIDE_EFFECTS__
174
+ */
175
+ export declare function isSafeRelativeAssetPath(relativePath: string): boolean;
176
+ /**
177
+ * Clamps a requested download-URL lifetime to {@link MAX_DOWNLOAD_TOKEN_TTL_SECONDS}.
178
+ *
179
+ * @param requestedTtlSeconds - The requested lifetime, if any.
180
+ * @param defaultTtlSeconds - The app-configured default, used when none is requested.
181
+ * @returns The lifetime to sign with, in seconds.
182
+ * @__NO_SIDE_EFFECTS__
183
+ */
184
+ export declare function downloadTokenTtlSeconds(requestedTtlSeconds: Maybe<Seconds>, defaultTtlSeconds?: Maybe<Seconds>): Seconds;
185
+ /**
186
+ * File-extension to `Content-Type` table for served assets.
187
+ *
188
+ * Intentionally tiny: the secure root holds build artifacts, and anything unrecognized is served as
189
+ * {@link DEFAULT_DOWNLOAD_CONTENT_TYPE} rather than being guessed at.
190
+ */
191
+ export declare const DOWNLOAD_CONTENT_TYPES: Readonly<Record<string, string>>;
192
+ /**
193
+ * `Content-Type` used for any asset whose extension is not in {@link DOWNLOAD_CONTENT_TYPES}.
194
+ */
195
+ export declare const DEFAULT_DOWNLOAD_CONTENT_TYPE = "application/octet-stream";
196
+ /**
197
+ * Resolves the `Content-Type` for a file path from its extension.
198
+ *
199
+ * @param filePath - The resolved file path.
200
+ * @returns The content type, defaulting to {@link DEFAULT_DOWNLOAD_CONTENT_TYPE}.
201
+ * @__NO_SIDE_EFFECTS__
202
+ */
203
+ export declare function downloadContentTypeForPath(filePath: string): string;
@@ -0,0 +1,25 @@
1
+ import { type Response } from 'express';
2
+ import { DownloadApiService } from './download.api.service';
3
+ /**
4
+ * REST controller that streams one registered asset to whoever holds a valid signed URL.
5
+ *
6
+ * Mounted at `download` — under the `/api` global prefix the route becomes `GET /api/download`.
7
+ *
8
+ * `/api/download` must **NOT** be listed in the OIDC module's `protectedPaths`: it authenticates via
9
+ * the signed `asset` query parameter, and a bearer middleware in front of it would 401 exactly the
10
+ * callers the feature exists for (a bare machine that has no credential yet — obtaining one is the
11
+ * whole point). Do not "fix" that by adding it.
12
+ *
13
+ * Follows `McpController`'s `@Res()` style; there is no `StreamableFile` precedent in this repo.
14
+ */
15
+ export declare class DownloadApiController {
16
+ private readonly downloadService;
17
+ constructor(downloadService: DownloadApiService);
18
+ /**
19
+ * Streams the asset named by a signed capability token.
20
+ *
21
+ * @param asset - The signed token from the `asset` query parameter.
22
+ * @param res - The Express response the file is streamed to.
23
+ */
24
+ downloadAsset(asset: string, res: Response): Promise<void>;
25
+ }
@@ -0,0 +1,47 @@
1
+ import { type ModuleMetadata } from '@nestjs/common';
2
+ import { type ClassType } from '@dereekb/util';
3
+ /**
4
+ * Configuration for {@link downloadApiModuleMetadata}.
5
+ */
6
+ export interface DownloadApiModuleMetadataConfig extends Pick<ModuleMetadata, 'imports' | 'exports' | 'providers'> {
7
+ /**
8
+ * Module that exports the download endpoint's dependencies.
9
+ *
10
+ * Should provide:
11
+ * - {@link DownloadApiModuleConfig} — the absolute secure-assets root and the API base URL. Without
12
+ * a resolvable root every request fails closed.
13
+ * - `DOWNLOAD_TOKEN_SIGNER` — the app's JWT signer/verifier, typically adapting
14
+ * `OidcJwtSigningService` so the provider's own JWKS signs the capability tokens.
15
+ */
16
+ readonly dependencyModule: ClassType;
17
+ }
18
+ /**
19
+ * Generates NestJS module metadata for the signed asset-download API.
20
+ *
21
+ * Mirrors `sessionApiModuleMetadata`: the consumer provides a dependency module exposing the
22
+ * required tokens and this factory wires the controller + service.
23
+ *
24
+ * Do NOT add `'/api/download'` to the OIDC module's `protectedPaths` — the route authenticates via
25
+ * its signed `asset` query parameter, and a bearer middleware would 401 the very callers it exists
26
+ * for.
27
+ *
28
+ * @param metadataConfig - Configuration including the dependency module.
29
+ * @returns NestJS module metadata exposing the download controller + service.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * @Module({
34
+ * imports: [MyOidcModule],
35
+ * providers: [
36
+ * { provide: DownloadApiModuleConfig, useValue: { secureAssetsRoot, apiBaseUrl } },
37
+ * { provide: DOWNLOAD_TOKEN_SIGNER, useFactory: oidcDownloadTokenSignerFactory, inject: [OidcJwtSigningService] }
38
+ * ],
39
+ * exports: [DownloadApiModuleConfig, DOWNLOAD_TOKEN_SIGNER]
40
+ * })
41
+ * export class MyDownloadDependencyModule {}
42
+ *
43
+ * @Module(downloadApiModuleMetadata({ dependencyModule: MyDownloadDependencyModule }))
44
+ * export class MyDownloadApiModule {}
45
+ * ```
46
+ */
47
+ export declare function downloadApiModuleMetadata(metadataConfig: DownloadApiModuleMetadataConfig): ModuleMetadata;
@@ -0,0 +1,111 @@
1
+ import { type Readable } from 'node:stream';
2
+ import { type ISO8601DateString, type Maybe, type Seconds, type WebsiteUrl } from '@dereekb/util';
3
+ import { DownloadApiModuleConfig, type DownloadTokenSigner } from './download.api.config';
4
+ /**
5
+ * Error code returned when the `asset` token is missing, malformed, or not a valid download token.
6
+ */
7
+ export declare const DOWNLOAD_INVALID_TOKEN_ERROR_CODE = "DOWNLOAD_INVALID_TOKEN_ERROR";
8
+ /**
9
+ * Error code returned when the token verifies but names nothing servable — a missing file, a
10
+ * directory, or anything that fails containment. Deliberately one code for all three so the endpoint
11
+ * does not report whether a given path exists outside the secure root.
12
+ */
13
+ export declare const DOWNLOAD_ASSET_NOT_FOUND_ERROR_CODE = "DOWNLOAD_ASSET_NOT_FOUND_ERROR";
14
+ /**
15
+ * Error code returned when minting is asked for an asset that is not inside the secure root.
16
+ */
17
+ export declare const DOWNLOAD_ASSET_NOT_MINTABLE_ERROR_CODE = "DOWNLOAD_ASSET_NOT_MINTABLE_ERROR";
18
+ /**
19
+ * Input to {@link DownloadApiService.downloadUrlForAsset}.
20
+ */
21
+ export interface DownloadUrlForAssetInput {
22
+ /**
23
+ * The asset path RELATIVE to the secure root, e.g. `demo-cli`.
24
+ */
25
+ readonly path: string;
26
+ /**
27
+ * Optional lifetime in seconds. Clamped to one hour.
28
+ */
29
+ readonly ttlSeconds?: Maybe<Seconds>;
30
+ /**
31
+ * Optional uid of the caller the URL is being minted for, written to the audit log. NOT put in the
32
+ * token — a download URL lands in shell history and proxy logs.
33
+ */
34
+ readonly forUid?: Maybe<string>;
35
+ }
36
+ /**
37
+ * A minted download URL.
38
+ */
39
+ export interface DownloadUrlForAssetResult {
40
+ readonly url: WebsiteUrl;
41
+ readonly expiresAt: ISO8601DateString;
42
+ /**
43
+ * SHA-256 of the artifact, so the caller can verify what it fetched.
44
+ */
45
+ readonly sha256: string;
46
+ /**
47
+ * Size of the artifact in bytes.
48
+ */
49
+ readonly size: number;
50
+ }
51
+ /**
52
+ * A resolved, contained asset ready to be streamed.
53
+ */
54
+ export interface ResolvedDownloadAsset {
55
+ readonly absolutePath: string;
56
+ readonly fileName: string;
57
+ readonly contentType: string;
58
+ readonly size: number;
59
+ readonly stream: () => Readable;
60
+ }
61
+ /**
62
+ * Mints and resolves signed, self-authenticating asset-download URLs.
63
+ *
64
+ * A minted URL is a capability: anything holding it can fetch exactly one file until it expires. The
65
+ * downloadable set is ONE folder (the app's configured secure root) and the containment check in
66
+ * `resolveSecureAssetPath` runs on BOTH sides — refusing to sign a token for anything outside the
67
+ * root, and refusing to serve one even if a signed token somehow names an outside path.
68
+ *
69
+ * The token is signed with the OIDC provider's own JWKS through an app-supplied
70
+ * {@link DownloadTokenSigner}, so it needs no new secret and rotates with the provider's keys. Its
71
+ * `typ` (`dbx-dl+jwt`) and `aud` (`dbx:asset-download`) are load-bearing discriminators: the bearer
72
+ * middleware must never accept a download token, and this verifier must never accept an access token.
73
+ */
74
+ export declare class DownloadApiService {
75
+ private readonly config?;
76
+ private readonly signer?;
77
+ private readonly _logger;
78
+ constructor(config?: DownloadApiModuleConfig | undefined, signer?: DownloadTokenSigner | undefined);
79
+ /**
80
+ * Whether the download path is usable at all (a signer AND a secure root are configured).
81
+ */
82
+ get enabled(): boolean;
83
+ /**
84
+ * Mints a signed, self-authenticating URL for one asset inside the secure root.
85
+ *
86
+ * @param input - The asset path, optional TTL, and the minting caller's uid (for the audit log only).
87
+ * @returns The absolute URL, its expiry, and the artifact's SHA-256 + size.
88
+ * @throws {HttpsError} A `400` when the path is not a servable file inside the secure root, or when the endpoint is not configured.
89
+ */
90
+ downloadUrlForAsset(input: DownloadUrlForAssetInput): Promise<DownloadUrlForAssetResult>;
91
+ /**
92
+ * Verifies a download token and resolves the asset it names, re-running containment.
93
+ *
94
+ * @param rawToken - The raw `asset` query parameter.
95
+ * @returns The resolved asset, ready to stream.
96
+ * @throws {HttpsError} A `401` for a missing/invalid/expired token, or a `404` for anything not servable.
97
+ */
98
+ resolveDownloadRequest(rawToken: Maybe<string>): Promise<ResolvedDownloadAsset>;
99
+ private _invalidTokenError;
100
+ private _notFoundError;
101
+ }
102
+ /**
103
+ * Computes the SHA-256 of a file, hex-encoded.
104
+ *
105
+ * Read fully into memory rather than streamed: the secure root holds build artifacts in the tens of
106
+ * megabytes at most, and the hash is only computed at mint time (not per download).
107
+ *
108
+ * @param absolutePath - The absolute path to hash.
109
+ * @returns The lowercase hex digest.
110
+ */
111
+ export declare function sha256ForFile(absolutePath: string): string;
@@ -0,0 +1,4 @@
1
+ export * from './download.api.config';
2
+ export * from './download.api.service';
3
+ export * from './download.api.controller';
4
+ export * from './download.api.module';
@@ -1,4 +1,6 @@
1
1
  export * from './auth.context.server';
2
2
  export * from './api.scope';
3
+ export * from './request.ip';
4
+ export * from './download';
3
5
  export * from './model';
4
6
  export * from './session';
@@ -0,0 +1,60 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ /**
3
+ * Header carrying the originating client address through a proxy chain, as a comma-delimited list
4
+ * whose FIRST entry is the original caller.
5
+ *
6
+ * Cloud Run / Cloud Functions sit behind Google's front end, which always rewrites this header, so
7
+ * the leftmost value is trustworthy there. Behind an arbitrary reverse proxy it is caller-controlled
8
+ * — see {@link requestClientIp}'s note on what this address may and may not be used for.
9
+ */
10
+ export declare const FORWARDED_FOR_REQUEST_HEADER = "x-forwarded-for";
11
+ /**
12
+ * The minimal request shape {@link requestClientIp} reads. Declared structurally so this module (and
13
+ * therefore `@dereekb/firebase-server` core) takes no dependency on Express' types, letting both the
14
+ * HTTP controller and an MCP tool handler pass whatever request object they hold.
15
+ */
16
+ export interface ClientIpRequest {
17
+ readonly headers?: Maybe<Record<string, Maybe<string | string[]>>>;
18
+ readonly ip?: Maybe<string>;
19
+ readonly socket?: Maybe<{
20
+ readonly remoteAddress?: Maybe<string>;
21
+ }>;
22
+ }
23
+ /**
24
+ * Resolves the calling client's IP address from a request, preferring the leftmost
25
+ * {@link FORWARDED_FOR_REQUEST_HEADER} entry and falling back to Express' own `req.ip` and then the
26
+ * raw socket address.
27
+ *
28
+ * IPv4-mapped IPv6 addresses (`::ffff:127.0.0.1`) are normalized to their dotted-quad form, so the
29
+ * same caller reaching a dual-stack listener and a v4-only one compares equal.
30
+ *
31
+ * **This is an advisory address, not an authentication factor.** Behind a proxy that does not
32
+ * rewrite the header it is caller-controlled, and a NAT/mobile caller's address changes on its own.
33
+ * Use it for binding a capability to the network path that minted it (an opt-in defence in depth
34
+ * alongside a real gate) or for logging — never as the sole check.
35
+ *
36
+ * @param request - The incoming request.
37
+ * @returns The resolved client address, or `undefined` when none could be determined.
38
+ * @__NO_SIDE_EFFECTS__
39
+ */
40
+ export declare function requestClientIp(request: Maybe<ClientIpRequest>): Maybe<string>;
41
+ /**
42
+ * Trims an address and unwraps the IPv4-mapped IPv6 form, returning `undefined` for an empty value.
43
+ *
44
+ * @param value - The raw address.
45
+ * @returns The normalized address, or `undefined` when there is nothing usable.
46
+ * @__NO_SIDE_EFFECTS__
47
+ */
48
+ export declare function normalizeClientIp(value: Maybe<string>): Maybe<string>;
49
+ /**
50
+ * Compares two client addresses for the purposes of a mint/redeem binding.
51
+ *
52
+ * Both sides are normalized first, and an ABSENT address on either side is treated as a MISMATCH:
53
+ * a binding that silently passes when the address could not be resolved would be no binding at all.
54
+ *
55
+ * @param a - The first address.
56
+ * @param b - The second address.
57
+ * @returns True when both addresses resolved and are equal.
58
+ * @__NO_SIDE_EFFECTS__
59
+ */
60
+ export declare function clientIpsMatch(a: Maybe<string>, b: Maybe<string>): boolean;
package/test/index.esm.js CHANGED
@@ -5089,6 +5089,7 @@ function _unsupported_iterable_to_array(o, minLen) {
5089
5089
  callbackUrl: callbackUrl,
5090
5090
  accessToken: tokenBody === null || tokenBody === void 0 ? void 0 : tokenBody.access_token,
5091
5091
  idToken: tokenBody === null || tokenBody === void 0 ? void 0 : tokenBody.id_token,
5092
+ refreshToken: tokenBody === null || tokenBody === void 0 ? void 0 : tokenBody.refresh_token,
5092
5093
  tokenType: tokenBody === null || tokenBody === void 0 ? void 0 : tokenBody.token_type,
5093
5094
  scope: tokenBody === null || tokenBody === void 0 ? void 0 : tokenBody.scope,
5094
5095
  session: {
package/test/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/test",
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/firebase-server/oidc": "14.4.0",
12
- "@dereekb/model": "14.4.0",
13
- "@dereekb/nestjs": "14.4.0",
14
- "@dereekb/oauth-resource": "14.4.0",
15
- "@dereekb/rxjs": "14.4.0",
16
- "@dereekb/util": "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/firebase-server/oidc": "14.5.1",
12
+ "@dereekb/model": "14.5.1",
13
+ "@dereekb/nestjs": "14.5.1",
14
+ "@dereekb/oauth-resource": "14.5.1",
15
+ "@dereekb/rxjs": "14.5.1",
16
+ "@dereekb/util": "14.5.1",
17
17
  "@google-cloud/firestore": "^7.11.6",
18
18
  "@google-cloud/storage": "^7.22.0",
19
19
  "@nestjs/common": "^12.0.1",
@@ -26,7 +26,7 @@
26
26
  "supertest": "^7.2.2"
27
27
  },
28
28
  "devDependencies": {
29
- "@dereekb/nestjs": "14.4.0"
29
+ "@dereekb/nestjs": "14.5.1"
30
30
  },
31
31
  "exports": {
32
32
  "./package.json": "./package.json",