@progressive-development/pd-provider-firebase-functions 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { CallableRequest } from 'firebase-functions/v2/https';
2
+ import { AuthContext } from './types';
3
+ /**
4
+ * Require authenticated user
5
+ */
6
+ export declare function requireAuth(request: CallableRequest): AuthContext;
7
+ /**
8
+ * Require specific role claims
9
+ */
10
+ export declare function requireClaims(request: CallableRequest, allowedRoles: string[]): AuthContext;
11
+ /**
12
+ * Ensure user can only access own resources
13
+ */
14
+ export declare function requireOwnership(auth: AuthContext, resourceOwnerId: string): void;
15
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGtC;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,WAAW,CASjE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,MAAM,EAAE,GACrB,WAAW,CASb;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,CAMjF"}
package/dist/auth.js ADDED
@@ -0,0 +1,31 @@
1
+ import { UnauthorizedError, ForbiddenError } from './errors.js';
2
+ import { logger } from './logger.js';
3
+
4
+ function requireAuth(request) {
5
+ if (!request.auth) {
6
+ throw new UnauthorizedError();
7
+ }
8
+ return {
9
+ uid: request.auth.uid,
10
+ email: request.auth.token.email ?? null,
11
+ claims: request.auth.token
12
+ };
13
+ }
14
+ function requireClaims(request, allowedRoles) {
15
+ const auth = requireAuth(request);
16
+ const userRoles = auth.claims.roles ?? [];
17
+ if (!allowedRoles.some((role) => userRoles.includes(role))) {
18
+ logger.warn("Access denied", { uid: auth.uid, required: allowedRoles });
19
+ throw new ForbiddenError();
20
+ }
21
+ return auth;
22
+ }
23
+ function requireOwnership(auth, resourceOwnerId) {
24
+ const isAdmin = (auth.claims.roles ?? []).includes("admin");
25
+ if (!isAdmin && auth.uid !== resourceOwnerId) {
26
+ logger.warn("Ownership violation", { uid: auth.uid, resourceOwnerId });
27
+ throw new ForbiddenError("Cannot access other user's resources");
28
+ }
29
+ }
30
+
31
+ export { requireAuth, requireClaims, requireOwnership };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Get next counter value atomically
3
+ * Uses Firestore transaction to prevent race conditions
4
+ */
5
+ export declare function getNextCounter(counterName: string, databaseId?: string): Promise<number>;
6
+ //# sourceMappingURL=counter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAsB,cAAc,CAClC,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CAWjB"}
@@ -0,0 +1,16 @@
1
+ import { getDb } from './firestore.js';
2
+
3
+ const COUNTERS_COLLECTION = "counters";
4
+ async function getNextCounter(counterName, databaseId) {
5
+ const db = getDb(databaseId);
6
+ const counterRef = db.collection(COUNTERS_COLLECTION).doc(counterName);
7
+ return db.runTransaction(async (transaction) => {
8
+ const doc = await transaction.get(counterRef);
9
+ const current = doc.exists ? doc.data()?.value ?? 0 : 0;
10
+ const next = current + 1;
11
+ transaction.set(counterRef, { value: next, updatedAt: /* @__PURE__ */ new Date() }, { merge: true });
12
+ return next;
13
+ });
14
+ }
15
+
16
+ export { getNextCounter };
@@ -0,0 +1,18 @@
1
+ export declare class AppError extends Error {
2
+ readonly code: string;
3
+ readonly statusCode: number;
4
+ constructor(message: string, code: string, statusCode: number);
5
+ }
6
+ export declare class ValidationError extends AppError {
7
+ constructor(message: string);
8
+ }
9
+ export declare class UnauthorizedError extends AppError {
10
+ constructor(message?: string);
11
+ }
12
+ export declare class ForbiddenError extends AppError {
13
+ constructor(message?: string);
14
+ }
15
+ export declare class NotFoundError extends AppError {
16
+ constructor(message: string);
17
+ }
18
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,qBAAa,QAAS,SAAQ,KAAK;aAGf,IAAI,EAAE,MAAM;aACZ,UAAU,EAAE,MAAM;gBAFlC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM;CAKrC;AAED,qBAAa,eAAgB,SAAQ,QAAQ;gBAC/B,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,iBAAkB,SAAQ,QAAQ;gBACjC,OAAO,SAA4B;CAGhD;AAED,qBAAa,cAAe,SAAQ,QAAQ;gBAC9B,OAAO,SAAkB;CAGtC;AAED,qBAAa,aAAc,SAAQ,QAAQ;gBAC7B,OAAO,EAAE,MAAM;CAG5B"}
package/dist/errors.js ADDED
@@ -0,0 +1,30 @@
1
+ class AppError extends Error {
2
+ constructor(message, code, statusCode) {
3
+ super(message);
4
+ this.code = code;
5
+ this.statusCode = statusCode;
6
+ this.name = this.constructor.name;
7
+ }
8
+ }
9
+ class ValidationError extends AppError {
10
+ constructor(message) {
11
+ super(message, "VALIDATION_ERROR", 400);
12
+ }
13
+ }
14
+ class UnauthorizedError extends AppError {
15
+ constructor(message = "Authentication required") {
16
+ super(message, "UNAUTHORIZED", 401);
17
+ }
18
+ }
19
+ class ForbiddenError extends AppError {
20
+ constructor(message = "Access denied") {
21
+ super(message, "FORBIDDEN", 403);
22
+ }
23
+ }
24
+ class NotFoundError extends AppError {
25
+ constructor(message) {
26
+ super(message, "NOT_FOUND", 404);
27
+ }
28
+ }
29
+
30
+ export { AppError, ForbiddenError, NotFoundError, UnauthorizedError, ValidationError };
@@ -0,0 +1,7 @@
1
+ import { Firestore } from 'firebase-admin/firestore';
2
+ /**
3
+ * Get Firestore instance (cached)
4
+ * @param databaseId Optional named database
5
+ */
6
+ export declare function getDb(databaseId?: string): Firestore;
7
+ //# sourceMappingURL=firestore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firestore.d.ts","sourceRoot":"","sources":["../src/firestore.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAKnE;;;GAGG;AACH,wBAAgB,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAepD"}
@@ -0,0 +1,19 @@
1
+ import { initializeApp } from 'firebase-admin/app';
2
+ import { getFirestore } from 'firebase-admin/firestore';
3
+
4
+ let app;
5
+ const dbCache = /* @__PURE__ */ new Map();
6
+ function getDb(databaseId) {
7
+ const cacheKey = databaseId ?? "default";
8
+ if (!dbCache.has(cacheKey)) {
9
+ if (!app) {
10
+ app = initializeApp();
11
+ }
12
+ const db = databaseId ? getFirestore(app, databaseId) : getFirestore(app);
13
+ db.settings({ ignoreUndefinedProperties: true });
14
+ dbCache.set(cacheKey, db);
15
+ }
16
+ return dbCache.get(cacheKey);
17
+ }
18
+
19
+ export { getDb };
@@ -0,0 +1,9 @@
1
+ export type { ApiResponse, AuthContext } from './types';
2
+ export { StatusCode } from './types';
3
+ export { AppError, ValidationError, UnauthorizedError, ForbiddenError, NotFoundError } from './errors';
4
+ export { requireAuth, requireClaims, requireOwnership } from './auth';
5
+ export { success, created, handleError } from './response';
6
+ export { getDb } from './firestore';
7
+ export { getNextCounter } from './counter';
8
+ export { logger } from './logger';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAGrC,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGvG,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAGtE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG3D,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAGpC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { StatusCode } from './types.js';
2
+ export { AppError, ForbiddenError, NotFoundError, UnauthorizedError, ValidationError } from './errors.js';
3
+ export { requireAuth, requireClaims, requireOwnership } from './auth.js';
4
+ export { created, handleError, success } from './response.js';
5
+ export { getDb } from './firestore.js';
6
+ export { getNextCounter } from './counter.js';
7
+ export { logger } from './logger.js';
@@ -0,0 +1,7 @@
1
+ export declare const logger: {
2
+ debug: (msg: string, data?: object) => void;
3
+ info: (msg: string, data?: object) => void;
4
+ warn: (msg: string, data?: object) => void;
5
+ error: (msg: string, error?: unknown, data?: object) => void;
6
+ };
7
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;iBACJ,MAAM,SAAS,MAAM;gBACtB,MAAM,SAAS,MAAM;gBACrB,MAAM,SAAS,MAAM;iBACpB,MAAM,UAAU,OAAO,SAAS,MAAM;CAEpD,CAAC"}
package/dist/logger.js ADDED
@@ -0,0 +1,10 @@
1
+ import * as functions from 'firebase-functions/logger';
2
+
3
+ const logger = {
4
+ debug: (msg, data) => functions.debug(msg, data),
5
+ info: (msg, data) => functions.info(msg, data),
6
+ warn: (msg, data) => functions.warn(msg, data),
7
+ error: (msg, error, data) => functions.error(msg, { error, ...data })
8
+ };
9
+
10
+ export { logger };
@@ -0,0 +1,8 @@
1
+ import { ApiResponse } from './types';
2
+ export declare function success<T>(data: T): ApiResponse<T>;
3
+ export declare function created<T>(data: T): ApiResponse<T>;
4
+ export declare function handleError(error: unknown): ApiResponse<{
5
+ error: string;
6
+ code: string;
7
+ }>;
8
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../src/response.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAIlD,wBAAgB,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAElD;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAElD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAmBxF"}
@@ -0,0 +1,30 @@
1
+ import { StatusCode } from './types.js';
2
+ import { AppError } from './errors.js';
3
+ import { logger } from './logger.js';
4
+
5
+ function success(data) {
6
+ return { statusCode: StatusCode.OK, resultData: data };
7
+ }
8
+ function created(data) {
9
+ return { statusCode: StatusCode.CREATED, resultData: data };
10
+ }
11
+ function handleError(error) {
12
+ if (error instanceof AppError) {
13
+ if (error.statusCode >= 500) {
14
+ logger.error("App error", error);
15
+ } else {
16
+ logger.warn("Request error", { code: error.code, message: error.message });
17
+ }
18
+ return {
19
+ statusCode: error.statusCode,
20
+ resultData: { error: error.message, code: error.code }
21
+ };
22
+ }
23
+ logger.error("Unexpected error", error);
24
+ return {
25
+ statusCode: StatusCode.INTERNAL_ERROR,
26
+ resultData: { error: "Internal server error", code: "INTERNAL_ERROR" }
27
+ };
28
+ }
29
+
30
+ export { created, handleError, success };
@@ -0,0 +1,19 @@
1
+ export interface ApiResponse<T = unknown> {
2
+ statusCode: number;
3
+ resultData: T;
4
+ }
5
+ export interface AuthContext {
6
+ uid: string;
7
+ email: string | null;
8
+ claims: Record<string, unknown>;
9
+ }
10
+ export declare const StatusCode: {
11
+ readonly OK: 200;
12
+ readonly CREATED: 201;
13
+ readonly BAD_REQUEST: 400;
14
+ readonly UNAUTHORIZED: 401;
15
+ readonly FORBIDDEN: 403;
16
+ readonly NOT_FOUND: 404;
17
+ readonly INTERNAL_ERROR: 500;
18
+ };
19
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,CAAC,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,eAAO,MAAM,UAAU;;;;;;;;CAQb,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ const StatusCode = {
2
+ OK: 200,
3
+ CREATED: 201,
4
+ BAD_REQUEST: 400,
5
+ UNAUTHORIZED: 401,
6
+ FORBIDDEN: 403,
7
+ NOT_FOUND: 404,
8
+ INTERNAL_ERROR: 500
9
+ };
10
+
11
+ export { StatusCode };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@progressive-development/pd-provider-firebase-functions",
3
+ "version": "0.1.0",
4
+ "description": "Firebase Functions v2 utilities for pd-spa-helper backend",
5
+ "author": "PD Progressive Development",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "type": "module",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": "./dist/index.js"
13
+ },
14
+ "files": [
15
+ "dist/",
16
+ "package.json",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "dependencies": {
21
+ "firebase-admin": "^12.6.0",
22
+ "firebase-functions": "^6.0.0"
23
+ },
24
+ "peerDependencies": {
25
+ "firebase-admin": "^12.6.0",
26
+ "firebase-functions": "^6.0.0"
27
+ },
28
+ "keywords": [
29
+ "pd",
30
+ "progressive",
31
+ "development",
32
+ "firebase",
33
+ "functions",
34
+ "backend"
35
+ ],
36
+ "scripts": {
37
+ "build": "vite build",
38
+ "clean": "rm -rf dist",
39
+ "clean:all": "rm -rf dist node_modules pnpm-lock.yaml",
40
+ "lint": "eslint --ext .ts src --ignore-path ../../.eslintignore && prettier \"**/*.ts\" --check --ignore-path ../../.eslintignore",
41
+ "format": "eslint --ext .ts src --fix --ignore-path ../../.eslintignore && prettier \"**/*.ts\" --write --ignore-path ../../.eslintignore",
42
+ "check": "pnpm run lint && pnpm run build"
43
+ }
44
+ }