@axova/shared 1.0.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.
Files changed (112) hide show
  1. package/CONFIGURATION_GUIDE.md +1 -0
  2. package/README.md +384 -0
  3. package/SCHEMA_ORGANIZATION.md +209 -0
  4. package/dist/configs/index.d.ts +85 -0
  5. package/dist/configs/index.js +555 -0
  6. package/dist/events/kafka.d.ts +40 -0
  7. package/dist/events/kafka.js +311 -0
  8. package/dist/index.d.ts +13 -0
  9. package/dist/index.js +41 -0
  10. package/dist/interfaces/customer-events.d.ts +85 -0
  11. package/dist/interfaces/customer-events.js +2 -0
  12. package/dist/interfaces/inventory-events.d.ts +453 -0
  13. package/dist/interfaces/inventory-events.js +3 -0
  14. package/dist/interfaces/inventory-types.d.ts +894 -0
  15. package/dist/interfaces/inventory-types.js +3 -0
  16. package/dist/interfaces/order-events.d.ts +320 -0
  17. package/dist/interfaces/order-events.js +3 -0
  18. package/dist/lib/auditLogger.d.ts +162 -0
  19. package/dist/lib/auditLogger.js +626 -0
  20. package/dist/lib/authOrganization.d.ts +24 -0
  21. package/dist/lib/authOrganization.js +110 -0
  22. package/dist/lib/db.d.ts +6 -0
  23. package/dist/lib/db.js +88 -0
  24. package/dist/middleware/serviceAuth.d.ts +60 -0
  25. package/dist/middleware/serviceAuth.js +272 -0
  26. package/dist/middleware/storeOwnership.d.ts +15 -0
  27. package/dist/middleware/storeOwnership.js +156 -0
  28. package/dist/middleware/storeValidationMiddleware.d.ts +44 -0
  29. package/dist/middleware/storeValidationMiddleware.js +180 -0
  30. package/dist/middleware/userAuth.d.ts +27 -0
  31. package/dist/middleware/userAuth.js +218 -0
  32. package/dist/schemas/admin/admin-schema.d.ts +741 -0
  33. package/dist/schemas/admin/admin-schema.js +111 -0
  34. package/dist/schemas/ai-moderation/ai-moderation-schema.d.ts +648 -0
  35. package/dist/schemas/ai-moderation/ai-moderation-schema.js +88 -0
  36. package/dist/schemas/common/common-schemas.d.ts +436 -0
  37. package/dist/schemas/common/common-schemas.js +94 -0
  38. package/dist/schemas/compliance/compliance-schema.d.ts +3388 -0
  39. package/dist/schemas/compliance/compliance-schema.js +472 -0
  40. package/dist/schemas/compliance/kyc-schema.d.ts +2642 -0
  41. package/dist/schemas/compliance/kyc-schema.js +361 -0
  42. package/dist/schemas/customer/customer-schema.d.ts +2727 -0
  43. package/dist/schemas/customer/customer-schema.js +399 -0
  44. package/dist/schemas/index.d.ts +27 -0
  45. package/dist/schemas/index.js +138 -0
  46. package/dist/schemas/inventory/inventory-tables.d.ts +9476 -0
  47. package/dist/schemas/inventory/inventory-tables.js +1470 -0
  48. package/dist/schemas/inventory/lot-tables.d.ts +3281 -0
  49. package/dist/schemas/inventory/lot-tables.js +608 -0
  50. package/dist/schemas/order/order-schema.d.ts +5825 -0
  51. package/dist/schemas/order/order-schema.js +954 -0
  52. package/dist/schemas/product/discount-relations.d.ts +15 -0
  53. package/dist/schemas/product/discount-relations.js +34 -0
  54. package/dist/schemas/product/discount-schema.d.ts +1975 -0
  55. package/dist/schemas/product/discount-schema.js +297 -0
  56. package/dist/schemas/product/product-relations.d.ts +41 -0
  57. package/dist/schemas/product/product-relations.js +133 -0
  58. package/dist/schemas/product/product-schema.d.ts +4544 -0
  59. package/dist/schemas/product/product-schema.js +671 -0
  60. package/dist/schemas/store/store-audit-schema.d.ts +4135 -0
  61. package/dist/schemas/store/store-audit-schema.js +556 -0
  62. package/dist/schemas/store/store-schema.d.ts +3100 -0
  63. package/dist/schemas/store/store-schema.js +381 -0
  64. package/dist/schemas/store/store-settings-schema.d.ts +665 -0
  65. package/dist/schemas/store/store-settings-schema.js +141 -0
  66. package/dist/schemas/types.d.ts +50 -0
  67. package/dist/schemas/types.js +3 -0
  68. package/dist/types/events.d.ts +2396 -0
  69. package/dist/types/events.js +505 -0
  70. package/dist/utils/errorHandler.d.ts +12 -0
  71. package/dist/utils/errorHandler.js +36 -0
  72. package/dist/utils/subdomain.d.ts +6 -0
  73. package/dist/utils/subdomain.js +20 -0
  74. package/nul +8 -0
  75. package/package.json +43 -0
  76. package/src/configs/index.ts +654 -0
  77. package/src/events/kafka.ts +429 -0
  78. package/src/index.ts +26 -0
  79. package/src/interfaces/customer-events.ts +106 -0
  80. package/src/interfaces/inventory-events.ts +545 -0
  81. package/src/interfaces/inventory-types.ts +1004 -0
  82. package/src/interfaces/order-events.ts +381 -0
  83. package/src/lib/auditLogger.ts +1117 -0
  84. package/src/lib/authOrganization.ts +153 -0
  85. package/src/lib/db.ts +64 -0
  86. package/src/middleware/serviceAuth.ts +328 -0
  87. package/src/middleware/storeOwnership.ts +199 -0
  88. package/src/middleware/storeValidationMiddleware.ts +247 -0
  89. package/src/middleware/userAuth.ts +248 -0
  90. package/src/schemas/admin/admin-schema.ts +208 -0
  91. package/src/schemas/ai-moderation/ai-moderation-schema.ts +180 -0
  92. package/src/schemas/common/common-schemas.ts +108 -0
  93. package/src/schemas/compliance/compliance-schema.ts +927 -0
  94. package/src/schemas/compliance/kyc-schema.ts +649 -0
  95. package/src/schemas/customer/customer-schema.ts +576 -0
  96. package/src/schemas/index.ts +189 -0
  97. package/src/schemas/inventory/inventory-tables.ts +1927 -0
  98. package/src/schemas/inventory/lot-tables.ts +799 -0
  99. package/src/schemas/order/order-schema.ts +1400 -0
  100. package/src/schemas/product/discount-relations.ts +44 -0
  101. package/src/schemas/product/discount-schema.ts +464 -0
  102. package/src/schemas/product/product-relations.ts +187 -0
  103. package/src/schemas/product/product-schema.ts +955 -0
  104. package/src/schemas/store/ethiopian_business_api.md.resolved +212 -0
  105. package/src/schemas/store/store-audit-schema.ts +1257 -0
  106. package/src/schemas/store/store-schema.ts +661 -0
  107. package/src/schemas/store/store-settings-schema.ts +231 -0
  108. package/src/schemas/types.ts +67 -0
  109. package/src/types/events.ts +646 -0
  110. package/src/utils/errorHandler.ts +44 -0
  111. package/src/utils/subdomain.ts +19 -0
  112. package/tsconfig.json +21 -0
@@ -0,0 +1,199 @@
1
+ import { eq } from "drizzle-orm";
2
+ import type { FastifyReply, FastifyRequest } from "fastify";
3
+ import { db } from "../lib/db";
4
+ import { stores } from "../schemas/store/store-schema";
5
+ import {
6
+ createServiceAuthenticator,
7
+ getServiceAuthConfigFromEnv,
8
+ } from "./serviceAuth";
9
+
10
+ export interface StoreOwnerMiddlewareOptions {
11
+ requireActive?: boolean;
12
+ allowParameterStoreId?: boolean;
13
+ storeIdParamName?: string;
14
+ allowAdminOverride?: boolean;
15
+ overrideRoles?: string[];
16
+ overridePermissions?: string[];
17
+ }
18
+
19
+ /**
20
+ * Factory that returns a middleware ensuring the current user owns the target store.
21
+ * - Uses shared store validation under the hood for existence/active checks
22
+ * - Supports optional admin/permission overrides (e.g. SUPER_ADMIN or * permissions)
23
+ */
24
+ export function requireStoreOwner(options: StoreOwnerMiddlewareOptions = {}) {
25
+ const {
26
+ requireActive = true,
27
+ allowParameterStoreId = true,
28
+ storeIdParamName = "storeId",
29
+ allowAdminOverride = true,
30
+ overrideRoles = ["SUPER_ADMIN"],
31
+ overridePermissions = ["*", "store:read:all", "store:write:all"],
32
+ } = options;
33
+
34
+ return async (request: FastifyRequest, reply: FastifyReply) => {
35
+ const user = (request as any).user as
36
+ | {
37
+ userId: string;
38
+ role?: string;
39
+ permissions?: string[];
40
+ }
41
+ | undefined;
42
+
43
+ // Extract storeId from params/query/body/user.context
44
+ let storeId: string | undefined;
45
+ const params = (request.params || {}) as Record<string, any>;
46
+ const query = (request.query || {}) as Record<string, any>;
47
+ const body = (request.body || {}) as Record<string, any>;
48
+ if (allowParameterStoreId && typeof params[storeIdParamName] === "string") {
49
+ storeId = params[storeIdParamName] as string;
50
+ }
51
+ if (!storeId && typeof query.storeId === "string")
52
+ storeId = query.storeId as string;
53
+ if (!storeId && typeof body.storeId === "string")
54
+ storeId = body.storeId as string;
55
+ if (!storeId && typeof (request as any).user?.storeId === "string") {
56
+ storeId = (request as any).user?.storeId as string;
57
+ }
58
+
59
+ if (!storeId) {
60
+ return reply.status(400).send({
61
+ error: "Store ID Required",
62
+ message: `Store ID must be provided in URL (:${storeIdParamName}), query, or body`,
63
+ code: "STORE_ID_MISSING",
64
+ });
65
+ }
66
+
67
+ // Admin/permission overrides
68
+ const hasRoleOverride = !!user?.role && overrideRoles.includes(user.role);
69
+ const hasPermissionOverride = !!user?.permissions?.some((p) =>
70
+ overridePermissions.includes(p),
71
+ );
72
+ if (allowAdminOverride && (hasRoleOverride || hasPermissionOverride)) {
73
+ // Optionally attach minimal store info by fetching; if it fails, still allow
74
+ await attachStoreInfoIfPossible(request, storeId);
75
+ return;
76
+ }
77
+
78
+ // Fetch store details from Store Service internal route
79
+ try {
80
+ const store = await fetchStoreById(storeId);
81
+
82
+ if (!store) {
83
+ return reply.status(404).send({
84
+ error: "Store Not Found",
85
+ message: `Store with ID ${storeId} does not exist`,
86
+ code: "STORE_NOT_FOUND",
87
+ });
88
+ }
89
+
90
+ // requireActive check
91
+ if (requireActive && store.isActive === false) {
92
+ return reply.status(403).send({
93
+ error: "Store Inactive",
94
+ message: `Store ${storeId} is currently inactive`,
95
+ code: "STORE_INACTIVE",
96
+ });
97
+ }
98
+
99
+ // Ownership check
100
+ if (!user?.userId || store.userId !== user.userId) {
101
+ return reply.status(403).send({
102
+ error: "Store Access Denied",
103
+ message: "You do not have permission to access this store",
104
+ code: "STORE_ACCESS_DENIED",
105
+ });
106
+ }
107
+
108
+ // Attach store info to request for downstream handlers
109
+ (request as any).storeInfo = {
110
+ storeId: store.id,
111
+ storeName: store.storeName,
112
+ isActive: !!store.isActive,
113
+ userId: store.userId,
114
+ };
115
+ } catch (error) {
116
+ console.error("Store ownership validation error:", error);
117
+ return reply.status(500).send({
118
+ error: "Internal Server Error",
119
+ message: "Failed to validate store ownership",
120
+ code: "STORE_OWNERSHIP_VALIDATION_ERROR",
121
+ });
122
+ }
123
+ };
124
+ }
125
+
126
+ // Fetches store by ID from the Store Service internal endpoint
127
+ async function fetchStoreById(storeId: string): Promise<{
128
+ id: string;
129
+ storeName: string;
130
+ isActive?: boolean;
131
+ userId: string;
132
+ } | null> {
133
+ const baseUrl = process.env.STORE_SERVICE_URL || "http://localhost:3001";
134
+ const url = `${baseUrl}/internal/stores/${encodeURIComponent(storeId)}`;
135
+
136
+ let token: string | undefined;
137
+ try {
138
+ const cfg = getServiceAuthConfigFromEnv();
139
+ token = createServiceAuthenticator(cfg).generateServiceToken(
140
+ process.env.SERVICE_NAME || "inventory-core-service",
141
+ ["store:read"],
142
+ );
143
+ } catch {
144
+ // Development fallback tokens accepted by store service
145
+ const isDev = process.env.NODE_ENV !== "production";
146
+ if (isDev) token = "inventory-dev-access";
147
+ }
148
+
149
+ const headers: Record<string, string> = {
150
+ "Content-Type": "application/json",
151
+ };
152
+ if (token) headers["x-service-token"] = token;
153
+
154
+ try {
155
+ const res = await fetch(url, { method: "GET", headers });
156
+ if (res.ok) {
157
+ const data = (await res.json()) as any;
158
+ const store = data?.store || data?.data?.store || null;
159
+ if (store) return store;
160
+ }
161
+ } catch {
162
+ // ignore network error and try DB fallback when possible
163
+ }
164
+ // DB fallback (primarily for in-process store-service)
165
+ try {
166
+ const result = await db
167
+ .select({
168
+ id: stores.id,
169
+ storeName: stores.storeName,
170
+ isActive: stores.isActive,
171
+ userId: stores.userId,
172
+ })
173
+ .from(stores)
174
+ .where(eq(stores.id, storeId))
175
+ .limit(1);
176
+ return result[0] || null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ async function attachStoreInfoIfPossible(
183
+ request: FastifyRequest,
184
+ storeId: string,
185
+ ) {
186
+ try {
187
+ const store = await fetchStoreById(storeId);
188
+ if (store) {
189
+ (request as any).storeInfo = {
190
+ storeId: store.id,
191
+ storeName: store.storeName,
192
+ isActive: !!store.isActive,
193
+ userId: store.userId,
194
+ };
195
+ }
196
+ } catch {
197
+ // ignore
198
+ }
199
+ }
@@ -0,0 +1,247 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { FastifyReply, FastifyRequest } from "fastify";
3
+ import { db } from "../lib/db";
4
+ import { stores } from "../schemas/store/store-schema";
5
+
6
+ // Store cache to reduce database calls
7
+ const storeCache = new Map<
8
+ string,
9
+ {
10
+ isValid: boolean;
11
+ timestamp: number;
12
+ storeData?: {
13
+ id: string;
14
+ storeName: string;
15
+ isActive: boolean;
16
+ userId: string;
17
+ };
18
+ }
19
+ >();
20
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
21
+
22
+ export interface StoreValidationOptions {
23
+ requireActive?: boolean;
24
+ allowParameterStoreId?: boolean;
25
+ storeIdParamName?: string;
26
+ requireOwnership?: boolean;
27
+ }
28
+
29
+ // Extended request interface to include store information
30
+ declare module "fastify" {
31
+ interface FastifyRequest {
32
+ user?: {
33
+ userId: string;
34
+ storeId: string;
35
+ role: string;
36
+ permissions: string[];
37
+ iat?: number;
38
+ exp?: number;
39
+ };
40
+ session?: unknown;
41
+ storeInfo?: {
42
+ storeId: string;
43
+ storeName: string;
44
+ isActive: boolean;
45
+ userId: string;
46
+ };
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Middleware to validate store existence and status
52
+ */
53
+ export const validateStoreMiddleware = (
54
+ options: StoreValidationOptions = {},
55
+ ) => {
56
+ return async (request: FastifyRequest, reply: FastifyReply) => {
57
+ try {
58
+ const {
59
+ requireActive = true,
60
+ allowParameterStoreId = true,
61
+ storeIdParamName = "storeId",
62
+ requireOwnership = false,
63
+ } = options;
64
+
65
+ // Extract store ID from various sources
66
+ let storeId: string | undefined;
67
+
68
+ // 1. Try from authenticated user context
69
+ if (request.user?.storeId) {
70
+ storeId = request.user.storeId;
71
+ }
72
+
73
+ // 2. Try from URL parameters if allowed
74
+ if (!storeId && allowParameterStoreId) {
75
+ const params = request.params as Record<string, string>;
76
+ storeId = params[storeIdParamName];
77
+ }
78
+
79
+ // 3. Try from query parameters
80
+ if (!storeId) {
81
+ const query = request.query as Record<string, string>;
82
+ storeId = query.storeId;
83
+ }
84
+
85
+ // 4. Try from request body
86
+ if (!storeId && request.body && typeof request.body === "object") {
87
+ const body = request.body as Record<string, unknown>;
88
+ if (typeof body.storeId === "string") {
89
+ storeId = body.storeId;
90
+ }
91
+ }
92
+
93
+ if (!storeId) {
94
+ return reply.status(400).send({
95
+ error: "Store ID Required",
96
+ message:
97
+ "Store ID must be provided in URL, query parameters, or request body",
98
+ code: "STORE_ID_MISSING",
99
+ });
100
+ }
101
+
102
+ // Check cache first
103
+ const cached = storeCache.get(storeId);
104
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
105
+ if (!cached.isValid) {
106
+ return reply.status(404).send({
107
+ error: "Store Not Found",
108
+ message: `Store with ID ${storeId} does not exist`,
109
+ code: "STORE_NOT_FOUND",
110
+ });
111
+ }
112
+
113
+ // Use cached store data if available
114
+ if (cached.storeData) {
115
+ const storeData = cached.storeData;
116
+
117
+ // Check if store is active when required
118
+ if (requireActive && !storeData.isActive) {
119
+ return reply.status(403).send({
120
+ error: "Store Inactive",
121
+ message: `Store ${storeId} is currently inactive`,
122
+ code: "STORE_INACTIVE",
123
+ });
124
+ }
125
+
126
+ // Check ownership when required
127
+ if (requireOwnership && request.user?.userId !== storeData.userId) {
128
+ return reply.status(403).send({
129
+ error: "Store Access Denied",
130
+ message: "You do not have permission to access this store",
131
+ code: "STORE_ACCESS_DENIED",
132
+ });
133
+ }
134
+
135
+ // Attach store information to request
136
+ request.storeInfo = {
137
+ storeId: storeData.id,
138
+ storeName: storeData.storeName,
139
+ isActive: storeData.isActive,
140
+ userId: storeData.userId,
141
+ };
142
+ return; // Successfully validated using cache
143
+ }
144
+ }
145
+
146
+ // Validate store exists and get details from database
147
+ const store = await db
148
+ .select({
149
+ id: stores.id,
150
+ storeName: stores.storeName,
151
+ isActive: stores.isActive,
152
+ userId: stores.userId,
153
+ })
154
+ .from(stores)
155
+ .where(eq(stores.id, storeId))
156
+ .limit(1);
157
+
158
+ if (store.length === 0) {
159
+ // Cache negative result
160
+ storeCache.set(storeId, { isValid: false, timestamp: Date.now() });
161
+
162
+ return reply.status(404).send({
163
+ error: "Store Not Found",
164
+ message: `Store with ID ${storeId} does not exist`,
165
+ code: "STORE_NOT_FOUND",
166
+ });
167
+ }
168
+
169
+ const storeData = store[0];
170
+
171
+ // Check if store is active when required
172
+ if (requireActive && !storeData.isActive) {
173
+ return reply.status(403).send({
174
+ error: "Store Inactive",
175
+ message: `Store ${storeId} is currently inactive`,
176
+ code: "STORE_INACTIVE",
177
+ });
178
+ }
179
+
180
+ // Check ownership when required
181
+ if (requireOwnership && request.user?.userId !== storeData.userId) {
182
+ return reply.status(403).send({
183
+ error: "Store Access Denied",
184
+ message: "You do not have permission to access this store",
185
+ code: "STORE_ACCESS_DENIED",
186
+ });
187
+ }
188
+
189
+ // Cache positive result with store data
190
+ storeCache.set(storeId, {
191
+ isValid: true,
192
+ timestamp: Date.now(),
193
+ storeData: {
194
+ id: storeData.id,
195
+ storeName: storeData.storeName,
196
+ isActive: storeData.isActive,
197
+ userId: storeData.userId,
198
+ },
199
+ });
200
+
201
+ // Attach store information to request
202
+ request.storeInfo = {
203
+ storeId: storeData.id,
204
+ storeName: storeData.storeName,
205
+ isActive: storeData.isActive,
206
+ userId: storeData.userId,
207
+ };
208
+
209
+ // Continue to next handler - explicitly return to proceed
210
+ } catch (error) {
211
+ console.error("Store validation error:", error);
212
+ return reply.status(500).send({
213
+ error: "Internal Server Error",
214
+ message: "Failed to validate store",
215
+ code: "STORE_VALIDATION_ERROR",
216
+ });
217
+ }
218
+ };
219
+ };
220
+
221
+ /**
222
+ * Helper to clear store cache entry
223
+ */
224
+ export const clearStoreCache = (storeId: string) => {
225
+ storeCache.delete(storeId);
226
+ };
227
+
228
+ /**
229
+ * Helper to clear all store cache
230
+ */
231
+ export const clearAllStoreCache = () => {
232
+ storeCache.clear();
233
+ };
234
+
235
+ /**
236
+ * Simplified middleware for common use cases
237
+ */
238
+ export const requireValidStore = validateStoreMiddleware({
239
+ requireActive: true,
240
+ });
241
+ export const requireOwnedStore = validateStoreMiddleware({
242
+ requireActive: true,
243
+ requireOwnership: true,
244
+ });
245
+ export const requireAnyStore = validateStoreMiddleware({
246
+ requireActive: false,
247
+ });
@@ -0,0 +1,248 @@
1
+ import { createAuthMiddleware } from "better-middleware";
2
+ import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
3
+ import fp from "fastify-plugin";
4
+
5
+ // Standardized user context aligned with existing request augmentation in this package
6
+ export interface SharedUserContext {
7
+ userId: string;
8
+ storeId: string;
9
+ role: string;
10
+ permissions: string[];
11
+ organizationId?: string;
12
+ activeOrganizationId?: string;
13
+ iat?: number;
14
+ exp?: number;
15
+ }
16
+
17
+ // Optional role → permission mapping (used for helpers)
18
+ export const DEFAULT_ROLE_PERMISSIONS: Record<string, string[]> = {
19
+ SUPER_ADMIN: ["*"],
20
+ STORE_ADMIN: [
21
+ "store:read",
22
+ "store:write",
23
+ "store:delete",
24
+ "orders:read",
25
+ "orders:write",
26
+ "products:read",
27
+ "products:write",
28
+ ],
29
+ STORE_MANAGER: [
30
+ "store:read",
31
+ "store:write",
32
+ "orders:read",
33
+ "orders:write",
34
+ "products:read",
35
+ "products:write",
36
+ ],
37
+ STORE_EMPLOYEE: ["store:read", "orders:read", "products:read"],
38
+ USER: ["store:read"],
39
+ };
40
+
41
+ // Core adapter of better-middleware to Fastify
42
+ function createFastifyAuthHandler() {
43
+ const auth = createAuthMiddleware<FastifyRequest>({
44
+ baseURL: process.env.NEXT_PUBLIC_AUTH_URL || "http://localhost:4000",
45
+ cache: { enabled: true, ttl: 300, max: 1000 },
46
+ onError: async () => ({
47
+ status: 401,
48
+ body: {
49
+ success: false,
50
+ error: "Authentication required",
51
+ code: "UNAUTHORIZED",
52
+ },
53
+ }),
54
+ logger: {
55
+ info: (m, d) =>
56
+ process.env.NODE_ENV === "development"
57
+ ? console.log(`[Auth] ${m}`, d)
58
+ : undefined,
59
+ error: (m, d) => console.error(`[Auth] ${m}`, d),
60
+ debug: (m, d) =>
61
+ process.env.NODE_ENV === "development"
62
+ ? console.debug(`[Auth] ${m}`, d)
63
+ : undefined,
64
+ },
65
+ framework: {
66
+ getHeaders: (request: any) => {
67
+ const headers: Record<string, string> = {};
68
+ Object.entries(request.headers || {}).forEach(([k, v]) => {
69
+ if (typeof v === "string") headers[k] = v;
70
+ else if (Array.isArray(v)) headers[k] = v.join(", ");
71
+ });
72
+ const hasCookie = (headers["cookie"] || "").includes(
73
+ "better-auth.session_token=",
74
+ );
75
+ const bearer = headers["authorization"]?.startsWith("Bearer ")
76
+ ? headers["authorization"].substring(7)
77
+ : undefined;
78
+ if (!hasCookie && bearer) {
79
+ headers["cookie"] =
80
+ `${headers["cookie"] ? headers["cookie"] + "; " : ""}better-auth.session_token=${bearer}`;
81
+ }
82
+ return headers;
83
+ },
84
+ getCookies: (request: any) => {
85
+ const cookies = request.cookies || {};
86
+ if (
87
+ !cookies["better-auth.session_token"] &&
88
+ request.headers.authorization?.startsWith("Bearer ")
89
+ ) {
90
+ cookies["better-auth.session_token"] =
91
+ request.headers.authorization.substring(7);
92
+ }
93
+ return cookies;
94
+ },
95
+ setContext: (request: any, key: "user" | "session", value: any) => {
96
+ if (key === "user" && value) {
97
+ const u = value as any;
98
+ request.user = {
99
+ userId: u.id,
100
+ storeId: u.activeOrganizationId || u.organizationId || "",
101
+ role: u.role || "USER",
102
+ permissions: DEFAULT_ROLE_PERMISSIONS[u.role || "USER"] || [],
103
+ organizationId: u.organizationId,
104
+ activeOrganizationId: u.activeOrganizationId,
105
+ iat: u.iat,
106
+ exp: u.exp,
107
+ } satisfies SharedUserContext;
108
+ } else if (key === "session" && value) {
109
+ const s = value as any;
110
+ if (request.user && !request.user.storeId)
111
+ request.user.storeId =
112
+ s.activeOrganizationId || request.user.storeId;
113
+ }
114
+ },
115
+ createResponse: (_request: any, body: unknown, status: number) => ({
116
+ status,
117
+ body,
118
+ }),
119
+ },
120
+ });
121
+ return auth;
122
+ }
123
+
124
+ // Pre-handler that runs authentication; emits response when not authorized
125
+ export const AuthPreHandler = async (
126
+ request: FastifyRequest,
127
+ reply: FastifyReply,
128
+ ) => {
129
+ const auth = createFastifyAuthHandler();
130
+ const result = await auth(request, request, async () => {});
131
+ if (result) {
132
+ reply.code(result.status).send(result.body);
133
+ }
134
+ };
135
+
136
+ // Fastify plugin for easy registration
137
+ export const fastifyUserAuthPlugin: FastifyPluginAsync = fp(
138
+ async (fastify: any) => {
139
+ fastify.addHook("preHandler", AuthPreHandler as any);
140
+ },
141
+ );
142
+
143
+ // ---------- Helper utilities ----------
144
+ export function getAuthTokenFromRequest(
145
+ request: FastifyRequest,
146
+ ): string | undefined {
147
+ const cookies = (request as any).cookies || {};
148
+ const tokenFromCookie = cookies["better-auth.session_token"] as
149
+ | string
150
+ | undefined;
151
+ if (tokenFromCookie) return tokenFromCookie;
152
+ const cookieHeader = request.headers.cookie;
153
+ if (cookieHeader) {
154
+ const match = cookieHeader.match(/better-auth\.session_token=([^;]+)/);
155
+ if (match) return match[1];
156
+ }
157
+ const authHeader = request.headers.authorization;
158
+ if (authHeader?.startsWith("Bearer ")) return authHeader.substring(7);
159
+ return undefined;
160
+ }
161
+
162
+ export function getAuthHeadersFromRequest(
163
+ request: FastifyRequest,
164
+ ): Record<string, string> {
165
+ const headers: Record<string, string> = {
166
+ "Content-Type": "application/json",
167
+ };
168
+ const token = getAuthTokenFromRequest(request);
169
+ if (token) {
170
+ headers["Cookie"] = `better-auth.session_token=${token}`;
171
+ headers["Authorization"] = `Bearer ${token}`;
172
+ }
173
+ return headers;
174
+ }
175
+
176
+ export function getUserDataFromRequest(request: FastifyRequest): {
177
+ userId: string;
178
+ organizationId?: string;
179
+ role?: string;
180
+ permissions?: string[];
181
+ } | null {
182
+ const user = (request as any).user as SharedUserContext | undefined;
183
+ if (user) {
184
+ return {
185
+ userId: user.userId,
186
+ organizationId: user.storeId || user.organizationId,
187
+ role: user.role,
188
+ permissions: user.permissions,
189
+ };
190
+ }
191
+
192
+ const userIdFromHeader = request.headers["x-user-id"] as string | undefined;
193
+ const storeIdFromHeader = request.headers["x-store-id"] as string | undefined;
194
+ const roleFromHeader = request.headers["x-user-role"] as string | undefined;
195
+ if (userIdFromHeader) {
196
+ return {
197
+ userId: userIdFromHeader,
198
+ organizationId: storeIdFromHeader,
199
+ role: roleFromHeader || "USER",
200
+ permissions: DEFAULT_ROLE_PERMISSIONS[roleFromHeader || "USER"] || [],
201
+ };
202
+ }
203
+ return null;
204
+ }
205
+
206
+ export function getUserIdFromRequest(request: FastifyRequest): string | null {
207
+ return getUserDataFromRequest(request)?.userId || null;
208
+ }
209
+
210
+ export function hasPermission(
211
+ request: FastifyRequest,
212
+ permission: string,
213
+ ): boolean {
214
+ const user = (request as any).user as SharedUserContext | undefined;
215
+ const perms = user?.permissions || [];
216
+ return perms.includes("*") || perms.includes(permission);
217
+ }
218
+
219
+ export function hasRole(request: FastifyRequest, role: string): boolean {
220
+ const user = (request as any).user as SharedUserContext | undefined;
221
+ return (user?.role || "").toUpperCase() === role.toUpperCase();
222
+ }
223
+
224
+ export function requirePermission(permission: string) {
225
+ return async (request: FastifyRequest, reply: FastifyReply) => {
226
+ if (!hasPermission(request, permission)) {
227
+ reply.code(403).send({
228
+ success: false,
229
+ error: "Insufficient permissions",
230
+ message: `Required permission: ${permission}`,
231
+ code: "FORBIDDEN",
232
+ });
233
+ }
234
+ };
235
+ }
236
+
237
+ export function requireRole(role: string) {
238
+ return async (request: FastifyRequest, reply: FastifyReply) => {
239
+ if (!hasRole(request, role)) {
240
+ reply.code(403).send({
241
+ success: false,
242
+ error: "Insufficient role",
243
+ message: `Required role: ${role}`,
244
+ code: "FORBIDDEN",
245
+ });
246
+ }
247
+ };
248
+ }