@lpdjs/firestore-repo-service 2.2.9-beta.2 → 2.2.9-beta.3
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/dist/{create-servers-xx0m3wwM.d.ts → create-servers-Dbmq3aN0.d.ts} +3 -3
- package/dist/{create-servers-CfLtucbQ.d.cts → create-servers-XKBCK7vV.d.cts} +3 -3
- package/dist/firebase-auth-CBqtAeb5.d.cts +309 -0
- package/dist/firebase-auth-CBqtAeb5.d.ts +309 -0
- package/dist/{index-Njwf8jvu.d.ts → index-BTrgC2ZA.d.cts} +7 -119
- package/dist/{index-BvKyjs6k.d.cts → index-CP4WoShV.d.ts} +7 -119
- package/dist/index.cjs +96 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -5
- package/dist/index.d.ts +6 -5
- package/dist/index.js +96 -96
- package/dist/index.js.map +1 -1
- package/dist/{openapi-BHZ2XHgn.d.ts → openapi-BSdeinkq.d.ts} +1 -1
- package/dist/{openapi-DXUE_MEP.d.cts → openapi-BSp3x2yW.d.cts} +1 -1
- package/dist/servers/admin/index.cjs +46 -46
- package/dist/servers/admin/index.cjs.map +1 -1
- package/dist/servers/admin/index.d.cts +3 -2
- package/dist/servers/admin/index.d.ts +3 -2
- package/dist/servers/admin/index.js +46 -46
- package/dist/servers/admin/index.js.map +1 -1
- package/dist/servers/auth/index.cjs +194 -0
- package/dist/servers/auth/index.cjs.map +1 -0
- package/dist/servers/auth/index.d.cts +13 -0
- package/dist/servers/auth/index.d.ts +13 -0
- package/dist/servers/auth/index.js +194 -0
- package/dist/servers/auth/index.js.map +1 -0
- package/dist/servers/crud/index.cjs +2 -2
- package/dist/servers/crud/index.cjs.map +1 -1
- package/dist/servers/crud/index.d.cts +5 -4
- package/dist/servers/crud/index.d.ts +5 -4
- package/dist/servers/crud/index.js +2 -2
- package/dist/servers/crud/index.js.map +1 -1
- package/dist/servers/index.cjs +111 -111
- package/dist/servers/index.cjs.map +1 -1
- package/dist/servers/index.d.cts +6 -5
- package/dist/servers/index.d.ts +6 -5
- package/dist/servers/index.js +111 -111
- package/dist/servers/index.js.map +1 -1
- package/dist/{types-C822D0dX.d.ts → types-CqXbEeXp.d.ts} +93 -6
- package/dist/{types-Z1Zy-2hs.d.cts → types-DptSRAK6.d.cts} +93 -6
- package/package.json +14 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { S as SyncQueue } from './queue-D_-aMf4H.js';
|
|
2
2
|
import { F as FirestoreSyncConfig, S as SyncEvent } from './types-CX5AbZWV.js';
|
|
3
|
-
import { O as OpenAPIDocument } from './openapi-
|
|
3
|
+
import { O as OpenAPIDocument } from './openapi-BSdeinkq.js';
|
|
4
4
|
import * as firebase_functions_v2_https from 'firebase-functions/v2/https';
|
|
5
5
|
import { onRequest, HttpsOptions } from 'firebase-functions/v2/https';
|
|
6
6
|
import { a as HistoryTriggersConfig } from './read-D4xAmsbE.js';
|
|
7
|
-
import { C as ConfiguredRepository, m as CrudServerOptions, l as CrudRepoConfig } from './types-
|
|
8
|
-
import { b as AdminServerOptions, A as AdminRepoConfig } from './index-
|
|
7
|
+
import { C as ConfiguredRepository, m as CrudServerOptions, l as CrudRepoConfig } from './types-CqXbEeXp.js';
|
|
8
|
+
import { b as AdminServerOptions, A as AdminRepoConfig } from './index-CP4WoShV.js';
|
|
9
9
|
|
|
10
10
|
/** Per-repo admin config with `repo` omitted (auto-bound from the registry key). */
|
|
11
11
|
type BoundAdminRepoConfig<TRepo extends ConfiguredRepository<any>> = Omit<AdminRepoConfig<TRepo>, "repo">;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { S as SyncQueue } from './queue-Dk1Ezhkf.cjs';
|
|
2
2
|
import { F as FirestoreSyncConfig, S as SyncEvent } from './types-CX5AbZWV.cjs';
|
|
3
|
-
import { O as OpenAPIDocument } from './openapi-
|
|
3
|
+
import { O as OpenAPIDocument } from './openapi-BSp3x2yW.cjs';
|
|
4
4
|
import * as firebase_functions_v2_https from 'firebase-functions/v2/https';
|
|
5
5
|
import { onRequest, HttpsOptions } from 'firebase-functions/v2/https';
|
|
6
6
|
import { a as HistoryTriggersConfig } from './read-D4xAmsbE.cjs';
|
|
7
|
-
import { C as ConfiguredRepository, m as CrudServerOptions, l as CrudRepoConfig } from './types-
|
|
8
|
-
import { b as AdminServerOptions, A as AdminRepoConfig } from './index-
|
|
7
|
+
import { C as ConfiguredRepository, m as CrudServerOptions, l as CrudRepoConfig } from './types-DptSRAK6.cjs';
|
|
8
|
+
import { b as AdminServerOptions, A as AdminRepoConfig } from './index-BTrgC2ZA.cjs';
|
|
9
9
|
|
|
10
10
|
/** Per-repo admin config with `repo` omitted (auto-bound from the registry key). */
|
|
11
11
|
type BoundAdminRepoConfig<TRepo extends ConfiguredRepository<any>> = Omit<AdminRepoConfig<TRepo>, "repo">;
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal zero-dependency HTTP router for Firebase Functions.
|
|
3
|
+
* Compatible with any Express-like (req, res) handler.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Named path parameters (e.g. "/repos/:name/:id")
|
|
7
|
+
* - GET, POST, DELETE methods
|
|
8
|
+
* - Global middleware (before each route)
|
|
9
|
+
* - 404 / error fallbacks
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { MiniRouter } from "@lpdjs/firestore-repo-service/servers/admin";
|
|
14
|
+
*
|
|
15
|
+
* // Create router
|
|
16
|
+
* const router = new MiniRouter();
|
|
17
|
+
*
|
|
18
|
+
* // Add global middleware (executed before every route)
|
|
19
|
+
* router.use(async (req, res, next) => {
|
|
20
|
+
* console.log(`${req.method} ${req.url}`);
|
|
21
|
+
* await next();
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Auth middleware
|
|
25
|
+
* router.use((req, res, next) => {
|
|
26
|
+
* if (!req.headers?.authorization) {
|
|
27
|
+
* res.status(401).send("Unauthorized");
|
|
28
|
+
* return;
|
|
29
|
+
* }
|
|
30
|
+
* next();
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* // Define routes with path parameters
|
|
34
|
+
* router.get("/users", async (req, res) => {
|
|
35
|
+
* res.json({ users: await getAllUsers() });
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* router.get("/users/:id", async (req, res) => {
|
|
39
|
+
* const user = await getUser(req.params.id); // Access path params
|
|
40
|
+
* if (!user) {
|
|
41
|
+
* res.status(404).send("User not found");
|
|
42
|
+
* return;
|
|
43
|
+
* }
|
|
44
|
+
* res.json(user);
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* router.post("/users", async (req, res) => {
|
|
48
|
+
* const user = await createUser(req.body);
|
|
49
|
+
* res.status(201).json(user);
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* router.delete("/users/:id", async (req, res) => {
|
|
53
|
+
* await deleteUser(req.params.id);
|
|
54
|
+
* res.status(204).end();
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* // Custom 404 handler
|
|
58
|
+
* router.onNotFound((req, res) => {
|
|
59
|
+
* res.status(404).json({ error: "Route not found", path: req.url });
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* // Custom error handler
|
|
63
|
+
* router.onError((err, req, res) => {
|
|
64
|
+
* console.error("Error:", err);
|
|
65
|
+
* res.status(500).json({ error: "Internal server error" });
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* // Use with Firebase Functions
|
|
69
|
+
* export const api = onRequest(async (req, res) => {
|
|
70
|
+
* await router.handle(req, res);
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
type AnyReq = {
|
|
75
|
+
method?: string;
|
|
76
|
+
url?: string;
|
|
77
|
+
/** Express originalUrl — preserved before any router stripping, contains the full path including the Firebase Functions prefix */
|
|
78
|
+
originalUrl?: string;
|
|
79
|
+
path?: string;
|
|
80
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
81
|
+
body?: unknown;
|
|
82
|
+
query?: Record<string, string | string[] | undefined>;
|
|
83
|
+
};
|
|
84
|
+
type AnyRes = {
|
|
85
|
+
status: (code: number) => AnyRes;
|
|
86
|
+
set: (key: string, value: string) => AnyRes;
|
|
87
|
+
send: (body: string) => void;
|
|
88
|
+
json: (body: unknown) => void;
|
|
89
|
+
end: () => void;
|
|
90
|
+
};
|
|
91
|
+
type RouteParams = Record<string, string>;
|
|
92
|
+
type RouteHandler = (req: AnyReq & {
|
|
93
|
+
params: RouteParams;
|
|
94
|
+
}, res: AnyRes) => void | Promise<void>;
|
|
95
|
+
type Middleware = (req: AnyReq & {
|
|
96
|
+
params: RouteParams;
|
|
97
|
+
}, res: AnyRes, next: () => void | Promise<void>) => void | Promise<void>;
|
|
98
|
+
declare class MiniRouter {
|
|
99
|
+
private routes;
|
|
100
|
+
private middlewares;
|
|
101
|
+
private notFoundHandler;
|
|
102
|
+
private errorHandler;
|
|
103
|
+
use(middleware: Middleware): this;
|
|
104
|
+
get(path: string, handler: RouteHandler): this;
|
|
105
|
+
post(path: string, handler: RouteHandler): this;
|
|
106
|
+
put(path: string, handler: RouteHandler): this;
|
|
107
|
+
patch(path: string, handler: RouteHandler): this;
|
|
108
|
+
delete(path: string, handler: RouteHandler): this;
|
|
109
|
+
onNotFound(handler: RouteHandler): this;
|
|
110
|
+
onError(handler: (err: unknown, req: AnyReq, res: AnyRes) => void): this;
|
|
111
|
+
private addRoute;
|
|
112
|
+
handle(req: AnyReq, res: AnyRes): Promise<void>;
|
|
113
|
+
private runMiddlewareChain;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Firebase Auth helper for the admin & CRUD servers.
|
|
118
|
+
*
|
|
119
|
+
* Returns an {@link AuthExtension} ready to plug into `servers.admin()` or
|
|
120
|
+
* `servers.crud()`. Supports two transport modes:
|
|
121
|
+
*
|
|
122
|
+
* - **`cookie`** — session cookie pattern (default for admin UIs). Mounts
|
|
123
|
+
* `/__login`, `/__session`, `/__logout` routes; the page lets the user sign
|
|
124
|
+
* in client-side with the Firebase JS SDK and exchanges the resulting ID
|
|
125
|
+
* token for an HttpOnly session cookie via Firebase Admin SDK.
|
|
126
|
+
* - **`bearer`** — verifies `Authorization: Bearer <idToken>` on every request
|
|
127
|
+
* (default for REST APIs). No login routes mounted.
|
|
128
|
+
* - **`both`** — accept either cookie or bearer.
|
|
129
|
+
*
|
|
130
|
+
* The helper is **agnostic** about authorization: pass an `allow` callback
|
|
131
|
+
* returning whatever role/context shape you need. The result is exposed as
|
|
132
|
+
* `req.user.context` to downstream middlewares and route handlers.
|
|
133
|
+
*
|
|
134
|
+
* @example Admin (cookie + role trio)
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { firebaseAuth } from "@lpdjs/firestore-repo-service/servers/auth";
|
|
137
|
+
* import { getAuth } from "firebase-admin/auth";
|
|
138
|
+
*
|
|
139
|
+
* servers.admin({
|
|
140
|
+
* auth: firebaseAuth({
|
|
141
|
+
* getAuth,
|
|
142
|
+
* mode: "cookie",
|
|
143
|
+
* apiKey: process.env.FIREBASE_WEB_API_KEY!,
|
|
144
|
+
* authDomain: process.env.FIREBASE_AUTH_DOMAIN!,
|
|
145
|
+
* allow: ({ email, claims }) => {
|
|
146
|
+
* if (claims.superAdmin) return { role: "superAdmin" };
|
|
147
|
+
* if (email?.endsWith("@solarpush.io")) return { role: "admin" };
|
|
148
|
+
* if (email) return { role: "viewer" };
|
|
149
|
+
* return null;
|
|
150
|
+
* },
|
|
151
|
+
* }),
|
|
152
|
+
* repos: { ... },
|
|
153
|
+
* });
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* @example CRUD (bearer + business rules per repo)
|
|
157
|
+
* ```ts
|
|
158
|
+
* servers.crud({
|
|
159
|
+
* auth: firebaseAuth({ getAuth, mode: "bearer", allow: (u) => u }),
|
|
160
|
+
* repos: {
|
|
161
|
+
* comments: {
|
|
162
|
+
* repo: repos.comments,
|
|
163
|
+
* rules: {
|
|
164
|
+
* list: () => true,
|
|
165
|
+
* get: ({ user, doc }) => doc.public || doc.authorId === user.uid,
|
|
166
|
+
* create: ({ user }) => !!user.uid,
|
|
167
|
+
* update: ({ user, doc }) => user.uid === doc.authorId,
|
|
168
|
+
* delete: ({ user, doc }) => user.claims.role === "moderator",
|
|
169
|
+
* },
|
|
170
|
+
* },
|
|
171
|
+
* },
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Minimal Firebase Admin Auth surface needed by this helper.
|
|
178
|
+
* Avoids a hard import of `firebase-admin/auth` so the package stays
|
|
179
|
+
* decoupled from a specific firebase-admin version.
|
|
180
|
+
*/
|
|
181
|
+
interface FirebaseAdminAuthLike {
|
|
182
|
+
verifyIdToken(idToken: string, checkRevoked?: boolean): Promise<DecodedIdTokenLike>;
|
|
183
|
+
verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise<DecodedIdTokenLike>;
|
|
184
|
+
createSessionCookie(idToken: string, sessionCookieOptions: {
|
|
185
|
+
expiresIn: number;
|
|
186
|
+
}): Promise<string>;
|
|
187
|
+
revokeRefreshTokens(uid: string): Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
interface DecodedIdTokenLike {
|
|
190
|
+
uid: string;
|
|
191
|
+
email?: string;
|
|
192
|
+
email_verified?: boolean;
|
|
193
|
+
[claim: string]: any;
|
|
194
|
+
}
|
|
195
|
+
/** Identity attached to every authenticated request as `req.user`. */
|
|
196
|
+
interface AuthUser<TContext = unknown> {
|
|
197
|
+
uid: string;
|
|
198
|
+
email: string | null;
|
|
199
|
+
emailVerified: boolean;
|
|
200
|
+
claims: Record<string, any>;
|
|
201
|
+
/** Result of the user-supplied `allow()` callback. */
|
|
202
|
+
context: TContext;
|
|
203
|
+
}
|
|
204
|
+
/** A route descriptor mounted by `firebaseAuth` before the protected chain. */
|
|
205
|
+
interface AuthRoute {
|
|
206
|
+
method: "GET" | "POST";
|
|
207
|
+
path: string;
|
|
208
|
+
handler: RouteHandler;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Returned by {@link firebaseAuth}. Servers detect this shape (vs.
|
|
212
|
+
* `BasicAuthConfig` / raw `Middleware`) and mount the routes before pushing
|
|
213
|
+
* the middleware onto the chain.
|
|
214
|
+
*/
|
|
215
|
+
interface AuthExtension {
|
|
216
|
+
readonly __authExtension: true;
|
|
217
|
+
middleware: Middleware;
|
|
218
|
+
/** Auxiliary routes (login page, session, logout). Empty in pure bearer mode. */
|
|
219
|
+
routes: AuthRoute[];
|
|
220
|
+
/** Path used to redirect unauthenticated browser requests. */
|
|
221
|
+
loginPath: string;
|
|
222
|
+
}
|
|
223
|
+
type FirebaseAuthMode = "cookie" | "bearer" | "both";
|
|
224
|
+
/** Provider configuration for the bundled login page. */
|
|
225
|
+
interface FirebaseAuthLoginPageConfig {
|
|
226
|
+
/** Page title. Default: "Admin sign-in". */
|
|
227
|
+
title?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Providers shown on the login page.
|
|
230
|
+
* Default: `["password", "google"]`.
|
|
231
|
+
*/
|
|
232
|
+
providers?: ("password" | "google")[];
|
|
233
|
+
}
|
|
234
|
+
interface FirebaseAuthConfig<TContext = unknown> {
|
|
235
|
+
/** Lazy getter for the Firebase Admin Auth instance. */
|
|
236
|
+
getAuth: () => FirebaseAdminAuthLike;
|
|
237
|
+
/** Transport mode. Default: `"cookie"`. */
|
|
238
|
+
mode?: FirebaseAuthMode;
|
|
239
|
+
/**
|
|
240
|
+
* Authorization callback. Receives the verified token claims and returns:
|
|
241
|
+
* - a context object → request is allowed, exposed as `req.user.context`,
|
|
242
|
+
* - `null` → request is rejected (401 / redirect to login).
|
|
243
|
+
*
|
|
244
|
+
* If omitted, the default policy allows any authenticated user with
|
|
245
|
+
* `context = null`.
|
|
246
|
+
*/
|
|
247
|
+
allow?: (user: Omit<AuthUser, "context">) => TContext | null | Promise<TContext | null>;
|
|
248
|
+
/**
|
|
249
|
+
* Whether to mount the bundled `/__login`, `/__session`, `/__logout`
|
|
250
|
+
* routes. Default: `true` for `cookie`/`both`, `false` for `bearer`.
|
|
251
|
+
*/
|
|
252
|
+
loginPage?: boolean | FirebaseAuthLoginPageConfig;
|
|
253
|
+
/**
|
|
254
|
+
* Firebase Web API key required by the JS SDK on the login page.
|
|
255
|
+
* Mandatory when `loginPage` is enabled. Find it in your Firebase Console
|
|
256
|
+
* under Project Settings → General → Web app config.
|
|
257
|
+
*/
|
|
258
|
+
apiKey?: string;
|
|
259
|
+
/**
|
|
260
|
+
* Firebase Auth domain (e.g. `my-project.firebaseapp.com`).
|
|
261
|
+
* Mandatory when `loginPage` is enabled.
|
|
262
|
+
*/
|
|
263
|
+
authDomain?: string;
|
|
264
|
+
/** Cookie name. Default: `__admin_session`. */
|
|
265
|
+
cookieName?: string;
|
|
266
|
+
/** Session cookie TTL in days. Default: `5` (Firebase max is 14). */
|
|
267
|
+
sessionTtlDays?: number;
|
|
268
|
+
/**
|
|
269
|
+
* Cookie `Secure` flag. Default: `true`. Set to `false` only for local
|
|
270
|
+
* development over HTTP.
|
|
271
|
+
*/
|
|
272
|
+
secureCookie?: boolean;
|
|
273
|
+
/** Cookie `SameSite`. Default: `"Lax"`. */
|
|
274
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
275
|
+
/**
|
|
276
|
+
* Behaviour when authentication fails or `allow()` returns `null`.
|
|
277
|
+
* - `"redirect"` (default in cookie mode) → 302 to the login page,
|
|
278
|
+
* - `"401"` (default in bearer mode) → JSON 401 response.
|
|
279
|
+
*/
|
|
280
|
+
onUnauthenticated?: "redirect" | "401";
|
|
281
|
+
/**
|
|
282
|
+
* Routes that should bypass the auth middleware (matched against the path
|
|
283
|
+
* after the basePath stripping). The auxiliary login routes are always
|
|
284
|
+
* public regardless of this option.
|
|
285
|
+
*/
|
|
286
|
+
publicPaths?: (string | RegExp)[];
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Build a Firebase Auth extension for use with `servers.admin()` or
|
|
290
|
+
* `servers.crud()`. See module-level docs for the full design and examples.
|
|
291
|
+
*/
|
|
292
|
+
declare function firebaseAuth<TContext = unknown>(config: FirebaseAuthConfig<TContext>): AuthExtension;
|
|
293
|
+
/**
|
|
294
|
+
* Type guard: detect an {@link AuthExtension} (vs. legacy
|
|
295
|
+
* `BasicAuthConfig` / `Middleware`).
|
|
296
|
+
*/
|
|
297
|
+
declare function isAuthExtension(value: unknown): value is AuthExtension;
|
|
298
|
+
/**
|
|
299
|
+
* Helper for explicitly opening a CRUD operation when the server has
|
|
300
|
+
* `auth` defined (bypasses the default-deny policy).
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* rules: { list: allowAll, get: allowAll }
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
declare const allowAll: () => true;
|
|
308
|
+
|
|
309
|
+
export { type AuthExtension as A, type DecodedIdTokenLike as D, type FirebaseAdminAuthLike as F, MiniRouter as M, type RouteHandler as R, type Middleware as a, type AuthUser as b, allowAll as c, type AuthRoute as d, type FirebaseAuthConfig as e, firebaseAuth as f, type FirebaseAuthLoginPageConfig as g, type FirebaseAuthMode as h, isAuthExtension as i };
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal zero-dependency HTTP router for Firebase Functions.
|
|
3
|
+
* Compatible with any Express-like (req, res) handler.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Named path parameters (e.g. "/repos/:name/:id")
|
|
7
|
+
* - GET, POST, DELETE methods
|
|
8
|
+
* - Global middleware (before each route)
|
|
9
|
+
* - 404 / error fallbacks
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { MiniRouter } from "@lpdjs/firestore-repo-service/servers/admin";
|
|
14
|
+
*
|
|
15
|
+
* // Create router
|
|
16
|
+
* const router = new MiniRouter();
|
|
17
|
+
*
|
|
18
|
+
* // Add global middleware (executed before every route)
|
|
19
|
+
* router.use(async (req, res, next) => {
|
|
20
|
+
* console.log(`${req.method} ${req.url}`);
|
|
21
|
+
* await next();
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Auth middleware
|
|
25
|
+
* router.use((req, res, next) => {
|
|
26
|
+
* if (!req.headers?.authorization) {
|
|
27
|
+
* res.status(401).send("Unauthorized");
|
|
28
|
+
* return;
|
|
29
|
+
* }
|
|
30
|
+
* next();
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* // Define routes with path parameters
|
|
34
|
+
* router.get("/users", async (req, res) => {
|
|
35
|
+
* res.json({ users: await getAllUsers() });
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* router.get("/users/:id", async (req, res) => {
|
|
39
|
+
* const user = await getUser(req.params.id); // Access path params
|
|
40
|
+
* if (!user) {
|
|
41
|
+
* res.status(404).send("User not found");
|
|
42
|
+
* return;
|
|
43
|
+
* }
|
|
44
|
+
* res.json(user);
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* router.post("/users", async (req, res) => {
|
|
48
|
+
* const user = await createUser(req.body);
|
|
49
|
+
* res.status(201).json(user);
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* router.delete("/users/:id", async (req, res) => {
|
|
53
|
+
* await deleteUser(req.params.id);
|
|
54
|
+
* res.status(204).end();
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* // Custom 404 handler
|
|
58
|
+
* router.onNotFound((req, res) => {
|
|
59
|
+
* res.status(404).json({ error: "Route not found", path: req.url });
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* // Custom error handler
|
|
63
|
+
* router.onError((err, req, res) => {
|
|
64
|
+
* console.error("Error:", err);
|
|
65
|
+
* res.status(500).json({ error: "Internal server error" });
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* // Use with Firebase Functions
|
|
69
|
+
* export const api = onRequest(async (req, res) => {
|
|
70
|
+
* await router.handle(req, res);
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
type AnyReq = {
|
|
75
|
+
method?: string;
|
|
76
|
+
url?: string;
|
|
77
|
+
/** Express originalUrl — preserved before any router stripping, contains the full path including the Firebase Functions prefix */
|
|
78
|
+
originalUrl?: string;
|
|
79
|
+
path?: string;
|
|
80
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
81
|
+
body?: unknown;
|
|
82
|
+
query?: Record<string, string | string[] | undefined>;
|
|
83
|
+
};
|
|
84
|
+
type AnyRes = {
|
|
85
|
+
status: (code: number) => AnyRes;
|
|
86
|
+
set: (key: string, value: string) => AnyRes;
|
|
87
|
+
send: (body: string) => void;
|
|
88
|
+
json: (body: unknown) => void;
|
|
89
|
+
end: () => void;
|
|
90
|
+
};
|
|
91
|
+
type RouteParams = Record<string, string>;
|
|
92
|
+
type RouteHandler = (req: AnyReq & {
|
|
93
|
+
params: RouteParams;
|
|
94
|
+
}, res: AnyRes) => void | Promise<void>;
|
|
95
|
+
type Middleware = (req: AnyReq & {
|
|
96
|
+
params: RouteParams;
|
|
97
|
+
}, res: AnyRes, next: () => void | Promise<void>) => void | Promise<void>;
|
|
98
|
+
declare class MiniRouter {
|
|
99
|
+
private routes;
|
|
100
|
+
private middlewares;
|
|
101
|
+
private notFoundHandler;
|
|
102
|
+
private errorHandler;
|
|
103
|
+
use(middleware: Middleware): this;
|
|
104
|
+
get(path: string, handler: RouteHandler): this;
|
|
105
|
+
post(path: string, handler: RouteHandler): this;
|
|
106
|
+
put(path: string, handler: RouteHandler): this;
|
|
107
|
+
patch(path: string, handler: RouteHandler): this;
|
|
108
|
+
delete(path: string, handler: RouteHandler): this;
|
|
109
|
+
onNotFound(handler: RouteHandler): this;
|
|
110
|
+
onError(handler: (err: unknown, req: AnyReq, res: AnyRes) => void): this;
|
|
111
|
+
private addRoute;
|
|
112
|
+
handle(req: AnyReq, res: AnyRes): Promise<void>;
|
|
113
|
+
private runMiddlewareChain;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Firebase Auth helper for the admin & CRUD servers.
|
|
118
|
+
*
|
|
119
|
+
* Returns an {@link AuthExtension} ready to plug into `servers.admin()` or
|
|
120
|
+
* `servers.crud()`. Supports two transport modes:
|
|
121
|
+
*
|
|
122
|
+
* - **`cookie`** — session cookie pattern (default for admin UIs). Mounts
|
|
123
|
+
* `/__login`, `/__session`, `/__logout` routes; the page lets the user sign
|
|
124
|
+
* in client-side with the Firebase JS SDK and exchanges the resulting ID
|
|
125
|
+
* token for an HttpOnly session cookie via Firebase Admin SDK.
|
|
126
|
+
* - **`bearer`** — verifies `Authorization: Bearer <idToken>` on every request
|
|
127
|
+
* (default for REST APIs). No login routes mounted.
|
|
128
|
+
* - **`both`** — accept either cookie or bearer.
|
|
129
|
+
*
|
|
130
|
+
* The helper is **agnostic** about authorization: pass an `allow` callback
|
|
131
|
+
* returning whatever role/context shape you need. The result is exposed as
|
|
132
|
+
* `req.user.context` to downstream middlewares and route handlers.
|
|
133
|
+
*
|
|
134
|
+
* @example Admin (cookie + role trio)
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { firebaseAuth } from "@lpdjs/firestore-repo-service/servers/auth";
|
|
137
|
+
* import { getAuth } from "firebase-admin/auth";
|
|
138
|
+
*
|
|
139
|
+
* servers.admin({
|
|
140
|
+
* auth: firebaseAuth({
|
|
141
|
+
* getAuth,
|
|
142
|
+
* mode: "cookie",
|
|
143
|
+
* apiKey: process.env.FIREBASE_WEB_API_KEY!,
|
|
144
|
+
* authDomain: process.env.FIREBASE_AUTH_DOMAIN!,
|
|
145
|
+
* allow: ({ email, claims }) => {
|
|
146
|
+
* if (claims.superAdmin) return { role: "superAdmin" };
|
|
147
|
+
* if (email?.endsWith("@solarpush.io")) return { role: "admin" };
|
|
148
|
+
* if (email) return { role: "viewer" };
|
|
149
|
+
* return null;
|
|
150
|
+
* },
|
|
151
|
+
* }),
|
|
152
|
+
* repos: { ... },
|
|
153
|
+
* });
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* @example CRUD (bearer + business rules per repo)
|
|
157
|
+
* ```ts
|
|
158
|
+
* servers.crud({
|
|
159
|
+
* auth: firebaseAuth({ getAuth, mode: "bearer", allow: (u) => u }),
|
|
160
|
+
* repos: {
|
|
161
|
+
* comments: {
|
|
162
|
+
* repo: repos.comments,
|
|
163
|
+
* rules: {
|
|
164
|
+
* list: () => true,
|
|
165
|
+
* get: ({ user, doc }) => doc.public || doc.authorId === user.uid,
|
|
166
|
+
* create: ({ user }) => !!user.uid,
|
|
167
|
+
* update: ({ user, doc }) => user.uid === doc.authorId,
|
|
168
|
+
* delete: ({ user, doc }) => user.claims.role === "moderator",
|
|
169
|
+
* },
|
|
170
|
+
* },
|
|
171
|
+
* },
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Minimal Firebase Admin Auth surface needed by this helper.
|
|
178
|
+
* Avoids a hard import of `firebase-admin/auth` so the package stays
|
|
179
|
+
* decoupled from a specific firebase-admin version.
|
|
180
|
+
*/
|
|
181
|
+
interface FirebaseAdminAuthLike {
|
|
182
|
+
verifyIdToken(idToken: string, checkRevoked?: boolean): Promise<DecodedIdTokenLike>;
|
|
183
|
+
verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise<DecodedIdTokenLike>;
|
|
184
|
+
createSessionCookie(idToken: string, sessionCookieOptions: {
|
|
185
|
+
expiresIn: number;
|
|
186
|
+
}): Promise<string>;
|
|
187
|
+
revokeRefreshTokens(uid: string): Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
interface DecodedIdTokenLike {
|
|
190
|
+
uid: string;
|
|
191
|
+
email?: string;
|
|
192
|
+
email_verified?: boolean;
|
|
193
|
+
[claim: string]: any;
|
|
194
|
+
}
|
|
195
|
+
/** Identity attached to every authenticated request as `req.user`. */
|
|
196
|
+
interface AuthUser<TContext = unknown> {
|
|
197
|
+
uid: string;
|
|
198
|
+
email: string | null;
|
|
199
|
+
emailVerified: boolean;
|
|
200
|
+
claims: Record<string, any>;
|
|
201
|
+
/** Result of the user-supplied `allow()` callback. */
|
|
202
|
+
context: TContext;
|
|
203
|
+
}
|
|
204
|
+
/** A route descriptor mounted by `firebaseAuth` before the protected chain. */
|
|
205
|
+
interface AuthRoute {
|
|
206
|
+
method: "GET" | "POST";
|
|
207
|
+
path: string;
|
|
208
|
+
handler: RouteHandler;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Returned by {@link firebaseAuth}. Servers detect this shape (vs.
|
|
212
|
+
* `BasicAuthConfig` / raw `Middleware`) and mount the routes before pushing
|
|
213
|
+
* the middleware onto the chain.
|
|
214
|
+
*/
|
|
215
|
+
interface AuthExtension {
|
|
216
|
+
readonly __authExtension: true;
|
|
217
|
+
middleware: Middleware;
|
|
218
|
+
/** Auxiliary routes (login page, session, logout). Empty in pure bearer mode. */
|
|
219
|
+
routes: AuthRoute[];
|
|
220
|
+
/** Path used to redirect unauthenticated browser requests. */
|
|
221
|
+
loginPath: string;
|
|
222
|
+
}
|
|
223
|
+
type FirebaseAuthMode = "cookie" | "bearer" | "both";
|
|
224
|
+
/** Provider configuration for the bundled login page. */
|
|
225
|
+
interface FirebaseAuthLoginPageConfig {
|
|
226
|
+
/** Page title. Default: "Admin sign-in". */
|
|
227
|
+
title?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Providers shown on the login page.
|
|
230
|
+
* Default: `["password", "google"]`.
|
|
231
|
+
*/
|
|
232
|
+
providers?: ("password" | "google")[];
|
|
233
|
+
}
|
|
234
|
+
interface FirebaseAuthConfig<TContext = unknown> {
|
|
235
|
+
/** Lazy getter for the Firebase Admin Auth instance. */
|
|
236
|
+
getAuth: () => FirebaseAdminAuthLike;
|
|
237
|
+
/** Transport mode. Default: `"cookie"`. */
|
|
238
|
+
mode?: FirebaseAuthMode;
|
|
239
|
+
/**
|
|
240
|
+
* Authorization callback. Receives the verified token claims and returns:
|
|
241
|
+
* - a context object → request is allowed, exposed as `req.user.context`,
|
|
242
|
+
* - `null` → request is rejected (401 / redirect to login).
|
|
243
|
+
*
|
|
244
|
+
* If omitted, the default policy allows any authenticated user with
|
|
245
|
+
* `context = null`.
|
|
246
|
+
*/
|
|
247
|
+
allow?: (user: Omit<AuthUser, "context">) => TContext | null | Promise<TContext | null>;
|
|
248
|
+
/**
|
|
249
|
+
* Whether to mount the bundled `/__login`, `/__session`, `/__logout`
|
|
250
|
+
* routes. Default: `true` for `cookie`/`both`, `false` for `bearer`.
|
|
251
|
+
*/
|
|
252
|
+
loginPage?: boolean | FirebaseAuthLoginPageConfig;
|
|
253
|
+
/**
|
|
254
|
+
* Firebase Web API key required by the JS SDK on the login page.
|
|
255
|
+
* Mandatory when `loginPage` is enabled. Find it in your Firebase Console
|
|
256
|
+
* under Project Settings → General → Web app config.
|
|
257
|
+
*/
|
|
258
|
+
apiKey?: string;
|
|
259
|
+
/**
|
|
260
|
+
* Firebase Auth domain (e.g. `my-project.firebaseapp.com`).
|
|
261
|
+
* Mandatory when `loginPage` is enabled.
|
|
262
|
+
*/
|
|
263
|
+
authDomain?: string;
|
|
264
|
+
/** Cookie name. Default: `__admin_session`. */
|
|
265
|
+
cookieName?: string;
|
|
266
|
+
/** Session cookie TTL in days. Default: `5` (Firebase max is 14). */
|
|
267
|
+
sessionTtlDays?: number;
|
|
268
|
+
/**
|
|
269
|
+
* Cookie `Secure` flag. Default: `true`. Set to `false` only for local
|
|
270
|
+
* development over HTTP.
|
|
271
|
+
*/
|
|
272
|
+
secureCookie?: boolean;
|
|
273
|
+
/** Cookie `SameSite`. Default: `"Lax"`. */
|
|
274
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
275
|
+
/**
|
|
276
|
+
* Behaviour when authentication fails or `allow()` returns `null`.
|
|
277
|
+
* - `"redirect"` (default in cookie mode) → 302 to the login page,
|
|
278
|
+
* - `"401"` (default in bearer mode) → JSON 401 response.
|
|
279
|
+
*/
|
|
280
|
+
onUnauthenticated?: "redirect" | "401";
|
|
281
|
+
/**
|
|
282
|
+
* Routes that should bypass the auth middleware (matched against the path
|
|
283
|
+
* after the basePath stripping). The auxiliary login routes are always
|
|
284
|
+
* public regardless of this option.
|
|
285
|
+
*/
|
|
286
|
+
publicPaths?: (string | RegExp)[];
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Build a Firebase Auth extension for use with `servers.admin()` or
|
|
290
|
+
* `servers.crud()`. See module-level docs for the full design and examples.
|
|
291
|
+
*/
|
|
292
|
+
declare function firebaseAuth<TContext = unknown>(config: FirebaseAuthConfig<TContext>): AuthExtension;
|
|
293
|
+
/**
|
|
294
|
+
* Type guard: detect an {@link AuthExtension} (vs. legacy
|
|
295
|
+
* `BasicAuthConfig` / `Middleware`).
|
|
296
|
+
*/
|
|
297
|
+
declare function isAuthExtension(value: unknown): value is AuthExtension;
|
|
298
|
+
/**
|
|
299
|
+
* Helper for explicitly opening a CRUD operation when the server has
|
|
300
|
+
* `auth` defined (bypasses the default-deny policy).
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* rules: { list: allowAll, get: allowAll }
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
declare const allowAll: () => true;
|
|
308
|
+
|
|
309
|
+
export { type AuthExtension as A, type DecodedIdTokenLike as D, type FirebaseAdminAuthLike as F, MiniRouter as M, type RouteHandler as R, type Middleware as a, type AuthUser as b, allowAll as c, type AuthRoute as d, type FirebaseAuthConfig as e, firebaseAuth as f, type FirebaseAuthLoginPageConfig as g, type FirebaseAuthMode as h, isAuthExtension as i };
|