@mohasinac/next 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/index.cjs +185 -0
- package/dist/index.d.cts +175 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +160 -0
- package/package.json +40 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __spreadValues = (a, b) => {
|
|
10
|
+
for (var prop in b || (b = {}))
|
|
11
|
+
if (__hasOwnProp.call(b, prop))
|
|
12
|
+
__defNormalProp(a, prop, b[prop]);
|
|
13
|
+
if (__getOwnPropSymbols)
|
|
14
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
15
|
+
if (__propIsEnum.call(b, prop))
|
|
16
|
+
__defNormalProp(a, prop, b[prop]);
|
|
17
|
+
}
|
|
18
|
+
return a;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
|
+
};
|
|
24
|
+
var __copyProps = (to, from, except, desc) => {
|
|
25
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
26
|
+
for (let key of __getOwnPropNames(from))
|
|
27
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
28
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
29
|
+
}
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
|
|
34
|
+
// src/index.ts
|
|
35
|
+
var index_exports = {};
|
|
36
|
+
__export(index_exports, {
|
|
37
|
+
createApiErrorHandler: () => createApiErrorHandler,
|
|
38
|
+
createRouteHandler: () => createRouteHandler
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/api/errorHandler.ts
|
|
43
|
+
var import_server = require("next/server");
|
|
44
|
+
function createApiErrorHandler(options) {
|
|
45
|
+
const {
|
|
46
|
+
isAppError,
|
|
47
|
+
getStatusCode,
|
|
48
|
+
toJSON,
|
|
49
|
+
logger,
|
|
50
|
+
internalErrorCode = "INTERNAL_ERROR",
|
|
51
|
+
internalErrorMessage = "An internal server error occurred"
|
|
52
|
+
} = options;
|
|
53
|
+
return function handleApiError(error) {
|
|
54
|
+
if (isAppError(error)) {
|
|
55
|
+
const status = getStatusCode(error);
|
|
56
|
+
if (status >= 500) {
|
|
57
|
+
logger.error("API Error", {
|
|
58
|
+
body: toJSON(error)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return import_server.NextResponse.json(toJSON(error), { status });
|
|
62
|
+
}
|
|
63
|
+
if (error !== null && typeof error === "object" && "issues" in error && Array.isArray(error.issues)) {
|
|
64
|
+
return import_server.NextResponse.json(
|
|
65
|
+
{
|
|
66
|
+
success: false,
|
|
67
|
+
error: "Validation failed",
|
|
68
|
+
code: "VALIDATION_ERROR",
|
|
69
|
+
data: error
|
|
70
|
+
},
|
|
71
|
+
{ status: 400 }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
logger.error("Unexpected API error", {
|
|
75
|
+
error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : error
|
|
76
|
+
});
|
|
77
|
+
return import_server.NextResponse.json(
|
|
78
|
+
{
|
|
79
|
+
success: false,
|
|
80
|
+
error: internalErrorMessage,
|
|
81
|
+
code: internalErrorCode
|
|
82
|
+
},
|
|
83
|
+
{ status: 500 }
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/api/routeHandler.ts
|
|
89
|
+
var import_server2 = require("next/server");
|
|
90
|
+
var import_contracts = require("@mohasinac/contracts");
|
|
91
|
+
function readSessionCookie(request) {
|
|
92
|
+
var _a;
|
|
93
|
+
const cookieHeader = (_a = request.headers.get("cookie")) != null ? _a : "";
|
|
94
|
+
const match = cookieHeader.match(/(?:^|;\s*)__session=([^;]+)/);
|
|
95
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
96
|
+
}
|
|
97
|
+
async function verifySession(request) {
|
|
98
|
+
const { session } = (0, import_contracts.getProviders)();
|
|
99
|
+
if (!session) {
|
|
100
|
+
throw Object.assign(new Error("Session provider not configured"), {
|
|
101
|
+
status: 503
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const cookie = readSessionCookie(request);
|
|
105
|
+
if (!cookie) {
|
|
106
|
+
throw Object.assign(new Error("Authentication required"), { status: 401 });
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const payload = await session.verifySession(cookie);
|
|
110
|
+
return __spreadValues({
|
|
111
|
+
uid: payload.uid,
|
|
112
|
+
email: payload.email,
|
|
113
|
+
role: payload.role,
|
|
114
|
+
emailVerified: payload.emailVerified
|
|
115
|
+
}, payload.claims);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
throw Object.assign(new Error("Invalid or expired session"), {
|
|
118
|
+
status: 401
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function createRouteHandler(options) {
|
|
123
|
+
return async (request, context) => {
|
|
124
|
+
var _a, _b, _c;
|
|
125
|
+
try {
|
|
126
|
+
let user;
|
|
127
|
+
const needsAuth = options.auth || options.roles && options.roles.length > 0;
|
|
128
|
+
if (needsAuth) {
|
|
129
|
+
user = await verifySession(request);
|
|
130
|
+
}
|
|
131
|
+
if (options.roles && options.roles.length > 0) {
|
|
132
|
+
if (!user || !options.roles.includes((_a = user.role) != null ? _a : "")) {
|
|
133
|
+
return import_server2.NextResponse.json(
|
|
134
|
+
{ success: false, error: "Insufficient permissions" },
|
|
135
|
+
{ status: 403 }
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let body;
|
|
140
|
+
if (options.schema) {
|
|
141
|
+
let raw;
|
|
142
|
+
try {
|
|
143
|
+
raw = await request.json();
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return import_server2.NextResponse.json(
|
|
146
|
+
{ success: false, error: "Invalid JSON body" },
|
|
147
|
+
{ status: 400 }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const result = options.schema.safeParse(raw);
|
|
151
|
+
if (!result.success) {
|
|
152
|
+
return import_server2.NextResponse.json(
|
|
153
|
+
{
|
|
154
|
+
success: false,
|
|
155
|
+
error: "Validation failed",
|
|
156
|
+
issues: (_c = (_b = result.error) == null ? void 0 : _b.issues) != null ? _c : []
|
|
157
|
+
},
|
|
158
|
+
{ status: 400 }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
body = result.data;
|
|
162
|
+
}
|
|
163
|
+
const params = (context == null ? void 0 : context.params) ? await context.params : void 0;
|
|
164
|
+
return await options.handler({
|
|
165
|
+
request,
|
|
166
|
+
user,
|
|
167
|
+
body,
|
|
168
|
+
params
|
|
169
|
+
});
|
|
170
|
+
} catch (err) {
|
|
171
|
+
const status = typeof (err == null ? void 0 : err.status) === "number" ? err.status : 500;
|
|
172
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
173
|
+
console.error(`[createRouteHandler] ${request.method} failed`, err);
|
|
174
|
+
return import_server2.NextResponse.json(
|
|
175
|
+
{ success: false, error: message },
|
|
176
|
+
{ status }
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
182
|
+
0 && (module.exports = {
|
|
183
|
+
createApiErrorHandler,
|
|
184
|
+
createRouteHandler
|
|
185
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IAuthVerifier — Injectable auth interface for createApiHandler.
|
|
5
|
+
*
|
|
6
|
+
* Decouples @mohasinac/next from any specific auth provider (Firebase, Auth.js, etc.).
|
|
7
|
+
* The consuming app provides a concrete implementation backed by its own
|
|
8
|
+
* auth SDK. The interface is intentionally minimal — only what createApiHandler
|
|
9
|
+
* needs: a uid and an optional role string.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // apps/web/src/lib/firebase/auth-verifier.ts
|
|
14
|
+
* import type { IAuthVerifier, AuthVerifiedUser } from '@mohasinac/next';
|
|
15
|
+
* import { getAdminAuth } from '@/lib/firebase/admin';
|
|
16
|
+
*
|
|
17
|
+
* export const firebaseAuthVerifier: IAuthVerifier = {
|
|
18
|
+
* async verify(sessionCookie) {
|
|
19
|
+
* const decoded = await getAdminAuth().verifySessionCookie(sessionCookie, true);
|
|
20
|
+
* return { uid: decoded.uid, role: decoded.role as string | undefined };
|
|
21
|
+
* },
|
|
22
|
+
* };
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Minimal user shape returned by a successful auth verification.
|
|
27
|
+
* Implementors may return additional fields; createApiHandler only consumes
|
|
28
|
+
* `uid` and `role`.
|
|
29
|
+
*/
|
|
30
|
+
interface AuthVerifiedUser {
|
|
31
|
+
uid: string;
|
|
32
|
+
email?: string;
|
|
33
|
+
role?: string;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generic auth verifier interface.
|
|
38
|
+
*
|
|
39
|
+
* An implementation should:
|
|
40
|
+
* - Verify the session token/cookie cryptographically.
|
|
41
|
+
* - Throw or return `null` when verification fails (both are handled by
|
|
42
|
+
* createApiHandler — returning null triggers a 401; throwing triggers
|
|
43
|
+
* handleApiError which maps to a 401 for AuthenticationError subclasses).
|
|
44
|
+
*/
|
|
45
|
+
interface IAuthVerifier {
|
|
46
|
+
/**
|
|
47
|
+
* Verify a session token (cookie value, JWT, or any string credential) and
|
|
48
|
+
* return the decoded user payload, or `null` if invalid.
|
|
49
|
+
*
|
|
50
|
+
* @param token - Raw session token (typically an httpOnly cookie value).
|
|
51
|
+
* @returns Verified user, or `null` when the token is expired / invalid.
|
|
52
|
+
* @throws May throw an `AuthenticationError` subclass; createApiHandler
|
|
53
|
+
* will convert it to a 401 response via handleApiError.
|
|
54
|
+
*/
|
|
55
|
+
verify(token: string): Promise<AuthVerifiedUser | null>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generic API error handler for Next.js API routes.
|
|
60
|
+
*
|
|
61
|
+
* Framework-agnostic version of the app's handleApiError. Business-specific
|
|
62
|
+
* error classes and loggers are injected via factory options so this module
|
|
63
|
+
* has zero knowledge of Firebase, Resend, or letitrip.in domain logic.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* // apps/web/src/lib/api/error-handler.ts
|
|
68
|
+
* import { createApiErrorHandler } from '@mohasinac/next';
|
|
69
|
+
* import { AppError } from '@/lib/errors';
|
|
70
|
+
* import { serverLogger } from '@/lib/server-logger';
|
|
71
|
+
*
|
|
72
|
+
* export const handleApiError = createApiErrorHandler({
|
|
73
|
+
* isAppError: (e): e is AppError => e instanceof AppError,
|
|
74
|
+
* getStatusCode: (e) => e.statusCode,
|
|
75
|
+
* toJSON: (e) => e.toJSON(),
|
|
76
|
+
* logger: serverLogger,
|
|
77
|
+
* internalErrorCode: 'GEN_INTERNAL_ERROR',
|
|
78
|
+
* internalErrorMessage: 'An internal server error occurred',
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/** Minimal logger interface — satisfied by Winston, Pino, or console. */
|
|
84
|
+
interface IApiErrorLogger {
|
|
85
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
86
|
+
}
|
|
87
|
+
/** Options for createApiErrorHandler factory. */
|
|
88
|
+
interface ApiErrorHandlerOptions<TAppError> {
|
|
89
|
+
/**
|
|
90
|
+
* Type guard — returns true when the thrown value is your app's AppError
|
|
91
|
+
* (or any subclass) that has statusCode / toJSON methods.
|
|
92
|
+
*/
|
|
93
|
+
isAppError(err: unknown): err is TAppError;
|
|
94
|
+
/** Extract the HTTP status code from your AppError. */
|
|
95
|
+
getStatusCode(err: TAppError): number;
|
|
96
|
+
/** Serialise the error to a JSON-safe shape for the response body. */
|
|
97
|
+
toJSON(err: TAppError): unknown;
|
|
98
|
+
/** Logger used for 5xx and unexpected errors. */
|
|
99
|
+
logger: IApiErrorLogger;
|
|
100
|
+
/** Error code included in the 500 response body. */
|
|
101
|
+
internalErrorCode?: string;
|
|
102
|
+
/** Error message included in the 500 response body. */
|
|
103
|
+
internalErrorMessage?: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build a typed handleApiError function bound to your app's error hierarchy.
|
|
107
|
+
*
|
|
108
|
+
* The returned function:
|
|
109
|
+
* - Returns a typed NextResponse for known AppError subclasses.
|
|
110
|
+
* - Returns a 400 with validation details for Zod-like error shapes.
|
|
111
|
+
* - Logs unexpected errors server-side; returns a generic 500 (no stack trace
|
|
112
|
+
* leaks to the client).
|
|
113
|
+
*/
|
|
114
|
+
declare function createApiErrorHandler<TAppError>(options: ApiErrorHandlerOptions<TAppError>): (error: unknown) => NextResponse;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* createRouteHandler — provider-aware API route handler factory for feat-* packages.
|
|
118
|
+
*
|
|
119
|
+
* Works like letitrip.in's local `createApiHandler` but uses `getProviders().session`
|
|
120
|
+
* for auth instead of a hardwired Firebase Admin call. This makes it portable across
|
|
121
|
+
* all consumer projects that register an `ISessionProvider`.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* export const POST = createRouteHandler({
|
|
126
|
+
* auth: true,
|
|
127
|
+
* roles: ["admin"],
|
|
128
|
+
* schema: mySchema,
|
|
129
|
+
* handler: async ({ user, body }) => { ... },
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/** Minimal schema interface compatible with Zod v3 and v4. */
|
|
135
|
+
interface ParseableSchema<TOutput> {
|
|
136
|
+
safeParse(data: unknown): {
|
|
137
|
+
success: true;
|
|
138
|
+
data: TOutput;
|
|
139
|
+
} | {
|
|
140
|
+
success: false;
|
|
141
|
+
error: any;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
interface RouteUser {
|
|
145
|
+
uid: string;
|
|
146
|
+
email?: string | null;
|
|
147
|
+
role?: string;
|
|
148
|
+
[key: string]: unknown;
|
|
149
|
+
}
|
|
150
|
+
interface RouteHandlerOptions<TInput = unknown, TParams = Record<string, string>> {
|
|
151
|
+
/** Require a valid session cookie. Implied when `roles` is set. */
|
|
152
|
+
auth?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* If provided, the verified user's `role` must be in this list.
|
|
155
|
+
* Implies `auth: true`.
|
|
156
|
+
*/
|
|
157
|
+
roles?: string[];
|
|
158
|
+
/** Zod schema to validate + parse the JSON request body. */
|
|
159
|
+
schema?: ParseableSchema<TInput>;
|
|
160
|
+
/** Route handler. `user` is present when `auth: true`. */
|
|
161
|
+
handler: (ctx: {
|
|
162
|
+
request: Request;
|
|
163
|
+
user?: RouteUser;
|
|
164
|
+
body?: TInput;
|
|
165
|
+
params?: TParams;
|
|
166
|
+
}) => Promise<any>;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Create a typed Next.js App Router handler with built-in auth + Zod validation.
|
|
170
|
+
*/
|
|
171
|
+
declare function createRouteHandler<TInput = unknown, TParams = Record<string, string>>(options: RouteHandlerOptions<TInput, TParams>): (request: Request, context: {
|
|
172
|
+
params: Promise<TParams>;
|
|
173
|
+
}) => Promise<NextResponse>;
|
|
174
|
+
|
|
175
|
+
export { type ApiErrorHandlerOptions, type AuthVerifiedUser, type IApiErrorLogger, type IAuthVerifier, type RouteUser, createApiErrorHandler, createRouteHandler };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IAuthVerifier — Injectable auth interface for createApiHandler.
|
|
5
|
+
*
|
|
6
|
+
* Decouples @mohasinac/next from any specific auth provider (Firebase, Auth.js, etc.).
|
|
7
|
+
* The consuming app provides a concrete implementation backed by its own
|
|
8
|
+
* auth SDK. The interface is intentionally minimal — only what createApiHandler
|
|
9
|
+
* needs: a uid and an optional role string.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // apps/web/src/lib/firebase/auth-verifier.ts
|
|
14
|
+
* import type { IAuthVerifier, AuthVerifiedUser } from '@mohasinac/next';
|
|
15
|
+
* import { getAdminAuth } from '@/lib/firebase/admin';
|
|
16
|
+
*
|
|
17
|
+
* export const firebaseAuthVerifier: IAuthVerifier = {
|
|
18
|
+
* async verify(sessionCookie) {
|
|
19
|
+
* const decoded = await getAdminAuth().verifySessionCookie(sessionCookie, true);
|
|
20
|
+
* return { uid: decoded.uid, role: decoded.role as string | undefined };
|
|
21
|
+
* },
|
|
22
|
+
* };
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Minimal user shape returned by a successful auth verification.
|
|
27
|
+
* Implementors may return additional fields; createApiHandler only consumes
|
|
28
|
+
* `uid` and `role`.
|
|
29
|
+
*/
|
|
30
|
+
interface AuthVerifiedUser {
|
|
31
|
+
uid: string;
|
|
32
|
+
email?: string;
|
|
33
|
+
role?: string;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generic auth verifier interface.
|
|
38
|
+
*
|
|
39
|
+
* An implementation should:
|
|
40
|
+
* - Verify the session token/cookie cryptographically.
|
|
41
|
+
* - Throw or return `null` when verification fails (both are handled by
|
|
42
|
+
* createApiHandler — returning null triggers a 401; throwing triggers
|
|
43
|
+
* handleApiError which maps to a 401 for AuthenticationError subclasses).
|
|
44
|
+
*/
|
|
45
|
+
interface IAuthVerifier {
|
|
46
|
+
/**
|
|
47
|
+
* Verify a session token (cookie value, JWT, or any string credential) and
|
|
48
|
+
* return the decoded user payload, or `null` if invalid.
|
|
49
|
+
*
|
|
50
|
+
* @param token - Raw session token (typically an httpOnly cookie value).
|
|
51
|
+
* @returns Verified user, or `null` when the token is expired / invalid.
|
|
52
|
+
* @throws May throw an `AuthenticationError` subclass; createApiHandler
|
|
53
|
+
* will convert it to a 401 response via handleApiError.
|
|
54
|
+
*/
|
|
55
|
+
verify(token: string): Promise<AuthVerifiedUser | null>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generic API error handler for Next.js API routes.
|
|
60
|
+
*
|
|
61
|
+
* Framework-agnostic version of the app's handleApiError. Business-specific
|
|
62
|
+
* error classes and loggers are injected via factory options so this module
|
|
63
|
+
* has zero knowledge of Firebase, Resend, or letitrip.in domain logic.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* // apps/web/src/lib/api/error-handler.ts
|
|
68
|
+
* import { createApiErrorHandler } from '@mohasinac/next';
|
|
69
|
+
* import { AppError } from '@/lib/errors';
|
|
70
|
+
* import { serverLogger } from '@/lib/server-logger';
|
|
71
|
+
*
|
|
72
|
+
* export const handleApiError = createApiErrorHandler({
|
|
73
|
+
* isAppError: (e): e is AppError => e instanceof AppError,
|
|
74
|
+
* getStatusCode: (e) => e.statusCode,
|
|
75
|
+
* toJSON: (e) => e.toJSON(),
|
|
76
|
+
* logger: serverLogger,
|
|
77
|
+
* internalErrorCode: 'GEN_INTERNAL_ERROR',
|
|
78
|
+
* internalErrorMessage: 'An internal server error occurred',
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/** Minimal logger interface — satisfied by Winston, Pino, or console. */
|
|
84
|
+
interface IApiErrorLogger {
|
|
85
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
86
|
+
}
|
|
87
|
+
/** Options for createApiErrorHandler factory. */
|
|
88
|
+
interface ApiErrorHandlerOptions<TAppError> {
|
|
89
|
+
/**
|
|
90
|
+
* Type guard — returns true when the thrown value is your app's AppError
|
|
91
|
+
* (or any subclass) that has statusCode / toJSON methods.
|
|
92
|
+
*/
|
|
93
|
+
isAppError(err: unknown): err is TAppError;
|
|
94
|
+
/** Extract the HTTP status code from your AppError. */
|
|
95
|
+
getStatusCode(err: TAppError): number;
|
|
96
|
+
/** Serialise the error to a JSON-safe shape for the response body. */
|
|
97
|
+
toJSON(err: TAppError): unknown;
|
|
98
|
+
/** Logger used for 5xx and unexpected errors. */
|
|
99
|
+
logger: IApiErrorLogger;
|
|
100
|
+
/** Error code included in the 500 response body. */
|
|
101
|
+
internalErrorCode?: string;
|
|
102
|
+
/** Error message included in the 500 response body. */
|
|
103
|
+
internalErrorMessage?: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build a typed handleApiError function bound to your app's error hierarchy.
|
|
107
|
+
*
|
|
108
|
+
* The returned function:
|
|
109
|
+
* - Returns a typed NextResponse for known AppError subclasses.
|
|
110
|
+
* - Returns a 400 with validation details for Zod-like error shapes.
|
|
111
|
+
* - Logs unexpected errors server-side; returns a generic 500 (no stack trace
|
|
112
|
+
* leaks to the client).
|
|
113
|
+
*/
|
|
114
|
+
declare function createApiErrorHandler<TAppError>(options: ApiErrorHandlerOptions<TAppError>): (error: unknown) => NextResponse;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* createRouteHandler — provider-aware API route handler factory for feat-* packages.
|
|
118
|
+
*
|
|
119
|
+
* Works like letitrip.in's local `createApiHandler` but uses `getProviders().session`
|
|
120
|
+
* for auth instead of a hardwired Firebase Admin call. This makes it portable across
|
|
121
|
+
* all consumer projects that register an `ISessionProvider`.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* export const POST = createRouteHandler({
|
|
126
|
+
* auth: true,
|
|
127
|
+
* roles: ["admin"],
|
|
128
|
+
* schema: mySchema,
|
|
129
|
+
* handler: async ({ user, body }) => { ... },
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/** Minimal schema interface compatible with Zod v3 and v4. */
|
|
135
|
+
interface ParseableSchema<TOutput> {
|
|
136
|
+
safeParse(data: unknown): {
|
|
137
|
+
success: true;
|
|
138
|
+
data: TOutput;
|
|
139
|
+
} | {
|
|
140
|
+
success: false;
|
|
141
|
+
error: any;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
interface RouteUser {
|
|
145
|
+
uid: string;
|
|
146
|
+
email?: string | null;
|
|
147
|
+
role?: string;
|
|
148
|
+
[key: string]: unknown;
|
|
149
|
+
}
|
|
150
|
+
interface RouteHandlerOptions<TInput = unknown, TParams = Record<string, string>> {
|
|
151
|
+
/** Require a valid session cookie. Implied when `roles` is set. */
|
|
152
|
+
auth?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* If provided, the verified user's `role` must be in this list.
|
|
155
|
+
* Implies `auth: true`.
|
|
156
|
+
*/
|
|
157
|
+
roles?: string[];
|
|
158
|
+
/** Zod schema to validate + parse the JSON request body. */
|
|
159
|
+
schema?: ParseableSchema<TInput>;
|
|
160
|
+
/** Route handler. `user` is present when `auth: true`. */
|
|
161
|
+
handler: (ctx: {
|
|
162
|
+
request: Request;
|
|
163
|
+
user?: RouteUser;
|
|
164
|
+
body?: TInput;
|
|
165
|
+
params?: TParams;
|
|
166
|
+
}) => Promise<any>;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Create a typed Next.js App Router handler with built-in auth + Zod validation.
|
|
170
|
+
*/
|
|
171
|
+
declare function createRouteHandler<TInput = unknown, TParams = Record<string, string>>(options: RouteHandlerOptions<TInput, TParams>): (request: Request, context: {
|
|
172
|
+
params: Promise<TParams>;
|
|
173
|
+
}) => Promise<NextResponse>;
|
|
174
|
+
|
|
175
|
+
export { type ApiErrorHandlerOptions, type AuthVerifiedUser, type IApiErrorLogger, type IAuthVerifier, type RouteUser, createApiErrorHandler, createRouteHandler };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __spreadValues = (a, b) => {
|
|
7
|
+
for (var prop in b || (b = {}))
|
|
8
|
+
if (__hasOwnProp.call(b, prop))
|
|
9
|
+
__defNormalProp(a, prop, b[prop]);
|
|
10
|
+
if (__getOwnPropSymbols)
|
|
11
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
+
if (__propIsEnum.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/api/errorHandler.ts
|
|
19
|
+
import { NextResponse } from "next/server";
|
|
20
|
+
function createApiErrorHandler(options) {
|
|
21
|
+
const {
|
|
22
|
+
isAppError,
|
|
23
|
+
getStatusCode,
|
|
24
|
+
toJSON,
|
|
25
|
+
logger,
|
|
26
|
+
internalErrorCode = "INTERNAL_ERROR",
|
|
27
|
+
internalErrorMessage = "An internal server error occurred"
|
|
28
|
+
} = options;
|
|
29
|
+
return function handleApiError(error) {
|
|
30
|
+
if (isAppError(error)) {
|
|
31
|
+
const status = getStatusCode(error);
|
|
32
|
+
if (status >= 500) {
|
|
33
|
+
logger.error("API Error", {
|
|
34
|
+
body: toJSON(error)
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return NextResponse.json(toJSON(error), { status });
|
|
38
|
+
}
|
|
39
|
+
if (error !== null && typeof error === "object" && "issues" in error && Array.isArray(error.issues)) {
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{
|
|
42
|
+
success: false,
|
|
43
|
+
error: "Validation failed",
|
|
44
|
+
code: "VALIDATION_ERROR",
|
|
45
|
+
data: error
|
|
46
|
+
},
|
|
47
|
+
{ status: 400 }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
logger.error("Unexpected API error", {
|
|
51
|
+
error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : error
|
|
52
|
+
});
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{
|
|
55
|
+
success: false,
|
|
56
|
+
error: internalErrorMessage,
|
|
57
|
+
code: internalErrorCode
|
|
58
|
+
},
|
|
59
|
+
{ status: 500 }
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/api/routeHandler.ts
|
|
65
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
66
|
+
import { getProviders } from "@mohasinac/contracts";
|
|
67
|
+
function readSessionCookie(request) {
|
|
68
|
+
var _a;
|
|
69
|
+
const cookieHeader = (_a = request.headers.get("cookie")) != null ? _a : "";
|
|
70
|
+
const match = cookieHeader.match(/(?:^|;\s*)__session=([^;]+)/);
|
|
71
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
72
|
+
}
|
|
73
|
+
async function verifySession(request) {
|
|
74
|
+
const { session } = getProviders();
|
|
75
|
+
if (!session) {
|
|
76
|
+
throw Object.assign(new Error("Session provider not configured"), {
|
|
77
|
+
status: 503
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const cookie = readSessionCookie(request);
|
|
81
|
+
if (!cookie) {
|
|
82
|
+
throw Object.assign(new Error("Authentication required"), { status: 401 });
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const payload = await session.verifySession(cookie);
|
|
86
|
+
return __spreadValues({
|
|
87
|
+
uid: payload.uid,
|
|
88
|
+
email: payload.email,
|
|
89
|
+
role: payload.role,
|
|
90
|
+
emailVerified: payload.emailVerified
|
|
91
|
+
}, payload.claims);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
throw Object.assign(new Error("Invalid or expired session"), {
|
|
94
|
+
status: 401
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function createRouteHandler(options) {
|
|
99
|
+
return async (request, context) => {
|
|
100
|
+
var _a, _b, _c;
|
|
101
|
+
try {
|
|
102
|
+
let user;
|
|
103
|
+
const needsAuth = options.auth || options.roles && options.roles.length > 0;
|
|
104
|
+
if (needsAuth) {
|
|
105
|
+
user = await verifySession(request);
|
|
106
|
+
}
|
|
107
|
+
if (options.roles && options.roles.length > 0) {
|
|
108
|
+
if (!user || !options.roles.includes((_a = user.role) != null ? _a : "")) {
|
|
109
|
+
return NextResponse2.json(
|
|
110
|
+
{ success: false, error: "Insufficient permissions" },
|
|
111
|
+
{ status: 403 }
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let body;
|
|
116
|
+
if (options.schema) {
|
|
117
|
+
let raw;
|
|
118
|
+
try {
|
|
119
|
+
raw = await request.json();
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return NextResponse2.json(
|
|
122
|
+
{ success: false, error: "Invalid JSON body" },
|
|
123
|
+
{ status: 400 }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const result = options.schema.safeParse(raw);
|
|
127
|
+
if (!result.success) {
|
|
128
|
+
return NextResponse2.json(
|
|
129
|
+
{
|
|
130
|
+
success: false,
|
|
131
|
+
error: "Validation failed",
|
|
132
|
+
issues: (_c = (_b = result.error) == null ? void 0 : _b.issues) != null ? _c : []
|
|
133
|
+
},
|
|
134
|
+
{ status: 400 }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
body = result.data;
|
|
138
|
+
}
|
|
139
|
+
const params = (context == null ? void 0 : context.params) ? await context.params : void 0;
|
|
140
|
+
return await options.handler({
|
|
141
|
+
request,
|
|
142
|
+
user,
|
|
143
|
+
body,
|
|
144
|
+
params
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
const status = typeof (err == null ? void 0 : err.status) === "number" ? err.status : 500;
|
|
148
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
149
|
+
console.error(`[createRouteHandler] ${request.method} failed`, err);
|
|
150
|
+
return NextResponse2.json(
|
|
151
|
+
{ success: false, error: message },
|
|
152
|
+
{ status }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
createApiErrorHandler,
|
|
159
|
+
createRouteHandler
|
|
160
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mohasinac/next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"lint": "eslint src"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"next": ">=14",
|
|
28
|
+
"react": ">=18",
|
|
29
|
+
"zod": ">=3"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@mohasinac/contracts": "workspace:*"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"tsup": "^8.5.0",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vitest": "^3.2.4",
|
|
38
|
+
"eslint": "^9.37.0"
|
|
39
|
+
}
|
|
40
|
+
}
|