@webwaka/core 1.3.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 (151) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/ai.d.ts +25 -0
  3. package/dist/ai.d.ts.map +1 -0
  4. package/dist/ai.js +53 -0
  5. package/dist/ai.js.map +1 -0
  6. package/dist/core/ai/AIEngine.d.ts +69 -0
  7. package/dist/core/ai/AIEngine.d.ts.map +1 -0
  8. package/dist/core/ai/AIEngine.js +185 -0
  9. package/dist/core/ai/AIEngine.js.map +1 -0
  10. package/dist/core/auth/index.d.ts +183 -0
  11. package/dist/core/auth/index.d.ts.map +1 -0
  12. package/dist/core/auth/index.js +369 -0
  13. package/dist/core/auth/index.js.map +1 -0
  14. package/dist/core/billing/index.d.ts +52 -0
  15. package/dist/core/billing/index.d.ts.map +1 -0
  16. package/dist/core/billing/index.js +91 -0
  17. package/dist/core/billing/index.js.map +1 -0
  18. package/dist/core/booking/index.d.ts +38 -0
  19. package/dist/core/booking/index.d.ts.map +1 -0
  20. package/dist/core/booking/index.js +60 -0
  21. package/dist/core/booking/index.js.map +1 -0
  22. package/dist/core/chat/index.d.ts +48 -0
  23. package/dist/core/chat/index.d.ts.map +1 -0
  24. package/dist/core/chat/index.js +83 -0
  25. package/dist/core/chat/index.js.map +1 -0
  26. package/dist/core/document/index.d.ts +41 -0
  27. package/dist/core/document/index.d.ts.map +1 -0
  28. package/dist/core/document/index.js +68 -0
  29. package/dist/core/document/index.js.map +1 -0
  30. package/dist/core/events/index.d.ts +64 -0
  31. package/dist/core/events/index.d.ts.map +1 -0
  32. package/dist/core/events/index.js +60 -0
  33. package/dist/core/events/index.js.map +1 -0
  34. package/dist/core/geolocation/index.d.ts +32 -0
  35. package/dist/core/geolocation/index.d.ts.map +1 -0
  36. package/dist/core/geolocation/index.js +50 -0
  37. package/dist/core/geolocation/index.js.map +1 -0
  38. package/dist/core/kyc/index.d.ts +37 -0
  39. package/dist/core/kyc/index.d.ts.map +1 -0
  40. package/dist/core/kyc/index.js +60 -0
  41. package/dist/core/kyc/index.js.map +1 -0
  42. package/dist/core/logger/index.d.ts +60 -0
  43. package/dist/core/logger/index.d.ts.map +1 -0
  44. package/dist/core/logger/index.js +91 -0
  45. package/dist/core/logger/index.js.map +1 -0
  46. package/dist/core/notifications/index.d.ts +41 -0
  47. package/dist/core/notifications/index.d.ts.map +1 -0
  48. package/dist/core/notifications/index.js +111 -0
  49. package/dist/core/notifications/index.js.map +1 -0
  50. package/dist/core/rbac/index.d.ts +43 -0
  51. package/dist/core/rbac/index.d.ts.map +1 -0
  52. package/dist/core/rbac/index.js +66 -0
  53. package/dist/core/rbac/index.js.map +1 -0
  54. package/dist/events.d.ts +23 -0
  55. package/dist/events.d.ts.map +1 -0
  56. package/dist/events.js +22 -0
  57. package/dist/events.js.map +1 -0
  58. package/dist/index.d.ts +33 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +56 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/kyc.d.ts +12 -0
  63. package/dist/kyc.d.ts.map +1 -0
  64. package/dist/kyc.js +2 -0
  65. package/dist/kyc.js.map +1 -0
  66. package/dist/nanoid.d.ts +8 -0
  67. package/dist/nanoid.d.ts.map +1 -0
  68. package/dist/nanoid.js +15 -0
  69. package/dist/nanoid.js.map +1 -0
  70. package/dist/ndpr.d.ts +13 -0
  71. package/dist/ndpr.d.ts.map +1 -0
  72. package/dist/ndpr.js +19 -0
  73. package/dist/ndpr.js.map +1 -0
  74. package/dist/optimistic-lock.d.ts +11 -0
  75. package/dist/optimistic-lock.d.ts.map +1 -0
  76. package/dist/optimistic-lock.js +24 -0
  77. package/dist/optimistic-lock.js.map +1 -0
  78. package/dist/payment.d.ts +41 -0
  79. package/dist/payment.d.ts.map +1 -0
  80. package/dist/payment.js +116 -0
  81. package/dist/payment.js.map +1 -0
  82. package/dist/pin.d.ts +6 -0
  83. package/dist/pin.d.ts.map +1 -0
  84. package/dist/pin.js +18 -0
  85. package/dist/pin.js.map +1 -0
  86. package/dist/query-helpers.d.ts +18 -0
  87. package/dist/query-helpers.d.ts.map +1 -0
  88. package/dist/query-helpers.js +22 -0
  89. package/dist/query-helpers.js.map +1 -0
  90. package/dist/rate-limit.d.ts +13 -0
  91. package/dist/rate-limit.d.ts.map +1 -0
  92. package/dist/rate-limit.js +16 -0
  93. package/dist/rate-limit.js.map +1 -0
  94. package/dist/sms.d.ts +23 -0
  95. package/dist/sms.d.ts.map +1 -0
  96. package/dist/sms.js +60 -0
  97. package/dist/sms.js.map +1 -0
  98. package/dist/tax.d.ts +25 -0
  99. package/dist/tax.d.ts.map +1 -0
  100. package/dist/tax.js +31 -0
  101. package/dist/tax.js.map +1 -0
  102. package/package.json +99 -0
  103. package/src/ai.test.ts +146 -0
  104. package/src/ai.ts +75 -0
  105. package/src/core/ai/AIEngine.test.ts +386 -0
  106. package/src/core/ai/AIEngine.ts +281 -0
  107. package/src/core/auth/index.test.ts +268 -0
  108. package/src/core/auth/index.ts +570 -0
  109. package/src/core/billing/index.test.ts +154 -0
  110. package/src/core/billing/index.ts +132 -0
  111. package/src/core/booking/index.test.ts +153 -0
  112. package/src/core/booking/index.ts +91 -0
  113. package/src/core/chat/index.test.ts +159 -0
  114. package/src/core/chat/index.ts +130 -0
  115. package/src/core/document/index.test.ts +106 -0
  116. package/src/core/document/index.ts +99 -0
  117. package/src/core/events/index.test.ts +91 -0
  118. package/src/core/events/index.ts +91 -0
  119. package/src/core/geolocation/index.test.ts +70 -0
  120. package/src/core/geolocation/index.ts +69 -0
  121. package/src/core/kyc/index.test.ts +105 -0
  122. package/src/core/kyc/index.ts +86 -0
  123. package/src/core/logger/index.test.ts +110 -0
  124. package/src/core/logger/index.ts +127 -0
  125. package/src/core/notifications/index.test.ts +85 -0
  126. package/src/core/notifications/index.ts +136 -0
  127. package/src/core/rbac/index.test.ts +81 -0
  128. package/src/core/rbac/index.ts +85 -0
  129. package/src/events.test.ts +43 -0
  130. package/src/events.ts +23 -0
  131. package/src/index.test.ts +123 -0
  132. package/src/index.ts +97 -0
  133. package/src/kyc.ts +23 -0
  134. package/src/nanoid.test.ts +43 -0
  135. package/src/nanoid.ts +16 -0
  136. package/src/ndpr.test.ts +68 -0
  137. package/src/ndpr.ts +49 -0
  138. package/src/optimistic-lock.test.ts +75 -0
  139. package/src/optimistic-lock.ts +36 -0
  140. package/src/payment.test.ts +152 -0
  141. package/src/payment.ts +163 -0
  142. package/src/pin.test.ts +57 -0
  143. package/src/pin.ts +38 -0
  144. package/src/query-helpers.test.ts +98 -0
  145. package/src/query-helpers.ts +36 -0
  146. package/src/rate-limit.test.ts +98 -0
  147. package/src/rate-limit.ts +33 -0
  148. package/src/sms.test.ts +112 -0
  149. package/src/sms.ts +85 -0
  150. package/src/tax.test.ts +85 -0
  151. package/src/tax.ts +57 -0
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @webwaka/core — Auth Module Tests
3
+ * Blueprint Reference: Part 9.2 (Universal Architecture Standards)
4
+ *
5
+ * Tests cover:
6
+ * - signJWT / verifyJWT round-trip
7
+ * - Token expiry rejection
8
+ * - Tampered token rejection
9
+ * - jwtAuthMiddleware: public routes, missing header, invalid token, valid token
10
+ * - requireRole: correct role, wrong role, missing user
11
+ * - requirePermissions: SUPER_ADMIN bypass, missing permission, granted permission
12
+ * - getTenantId / getAuthUser: present and missing
13
+ */
14
+
15
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
16
+ import {
17
+ signJWT,
18
+ verifyJWT,
19
+ jwtAuthMiddleware,
20
+ requireRole,
21
+ requirePermissions,
22
+ getTenantId,
23
+ getAuthUser,
24
+ type JWTPayload,
25
+ type AuthUser,
26
+ } from './index';
27
+
28
+ // ─── Test Helpers ─────────────────────────────────────────────────────────────
29
+
30
+ const TEST_SECRET = 'test-secret-key-for-unit-tests-only-32chars!!';
31
+
32
+ const TEST_PAYLOAD: Omit<JWTPayload, 'iat' | 'exp'> = {
33
+ sub: 'user_001',
34
+ email: 'test@webwaka.com',
35
+ tenantId: 'tenant_abc',
36
+ role: 'TENANT_ADMIN',
37
+ permissions: ['read:products', 'write:products'],
38
+ };
39
+
40
+ function makeMockContext(overrides: {
41
+ authHeader?: string | null;
42
+ path?: string;
43
+ method?: string;
44
+ contextValues?: Record<string, unknown>;
45
+ env?: Record<string, unknown>;
46
+ }) {
47
+ const contextValues: Record<string, unknown> = { ...(overrides.contextValues ?? {}) };
48
+ return {
49
+ req: {
50
+ method: overrides.method ?? 'GET',
51
+ path: overrides.path ?? '/api/test',
52
+ header: (name: string) => {
53
+ if (name === 'Authorization') return overrides.authHeader ?? undefined;
54
+ return undefined;
55
+ },
56
+ },
57
+ env: overrides.env ?? { JWT_SECRET: TEST_SECRET, ENVIRONMENT: 'test' },
58
+ json: (body: unknown, status?: number) => ({ body, status: status ?? 200 }),
59
+ header: vi.fn(),
60
+ get: (key: string) => contextValues[key],
61
+ set: (key: string, value: unknown) => { contextValues[key] = value; },
62
+ };
63
+ }
64
+
65
+ const mockNext = vi.fn(async () => {});
66
+
67
+ // ─── signJWT / verifyJWT ──────────────────────────────────────────────────────
68
+
69
+ describe('signJWT / verifyJWT', () => {
70
+ it('signs and verifies a token round-trip', async () => {
71
+ const token = await signJWT(TEST_PAYLOAD, TEST_SECRET);
72
+ expect(token.split('.')).toHaveLength(3);
73
+
74
+ const decoded = await verifyJWT(token, TEST_SECRET);
75
+ expect(decoded).not.toBeNull();
76
+ expect(decoded!.sub).toBe('user_001');
77
+ expect(decoded!.tenantId).toBe('tenant_abc');
78
+ expect(decoded!.role).toBe('TENANT_ADMIN');
79
+ expect(decoded!.email).toBe('test@webwaka.com');
80
+ expect(decoded!.permissions).toEqual(['read:products', 'write:products']);
81
+ });
82
+
83
+ it('rejects a token signed with a different secret', async () => {
84
+ const token = await signJWT(TEST_PAYLOAD, TEST_SECRET);
85
+ const decoded = await verifyJWT(token, 'wrong-secret');
86
+ expect(decoded).toBeNull();
87
+ });
88
+
89
+ it('rejects an expired token', async () => {
90
+ const token = await signJWT(TEST_PAYLOAD, TEST_SECRET, -1); // expired 1 second ago
91
+ const decoded = await verifyJWT(token, TEST_SECRET);
92
+ expect(decoded).toBeNull();
93
+ });
94
+
95
+ it('rejects a tampered payload', async () => {
96
+ const token = await signJWT(TEST_PAYLOAD, TEST_SECRET);
97
+ const parts = token.split('.');
98
+ // Tamper with the payload
99
+ const tamperedPayload = btoa(JSON.stringify({ ...TEST_PAYLOAD, role: 'SUPER_ADMIN', iat: 0, exp: 9999999999 }))
100
+ .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
101
+ const tamperedToken = `${parts[0]}.${tamperedPayload}.${parts[2]}`;
102
+ const decoded = await verifyJWT(tamperedToken, TEST_SECRET);
103
+ expect(decoded).toBeNull();
104
+ });
105
+
106
+ it('rejects a malformed token', async () => {
107
+ expect(await verifyJWT('not-a-jwt', TEST_SECRET)).toBeNull();
108
+ expect(await verifyJWT('a.b', TEST_SECRET)).toBeNull();
109
+ expect(await verifyJWT('', TEST_SECRET)).toBeNull();
110
+ });
111
+ });
112
+
113
+ // ─── jwtAuthMiddleware ────────────────────────────────────────────────────────
114
+
115
+ describe('jwtAuthMiddleware', () => {
116
+ beforeEach(() => {
117
+ vi.clearAllMocks();
118
+ });
119
+
120
+ it('allows public routes without a token', async () => {
121
+ const middleware = jwtAuthMiddleware({ publicRoutes: [{ path: '/health' }] });
122
+ const ctx = makeMockContext({ path: '/health', authHeader: null });
123
+ await middleware(ctx as any, mockNext);
124
+ expect(mockNext).toHaveBeenCalled();
125
+ });
126
+
127
+ it('rejects requests with no Authorization header', async () => {
128
+ const middleware = jwtAuthMiddleware();
129
+ const ctx = makeMockContext({ authHeader: null });
130
+ const result = await middleware(ctx as any, mockNext) as any;
131
+ expect(result.status).toBe(401);
132
+ expect(mockNext).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it('rejects requests with malformed Authorization header', async () => {
136
+ const middleware = jwtAuthMiddleware();
137
+ const ctx = makeMockContext({ authHeader: 'Token abc123' });
138
+ const result = await middleware(ctx as any, mockNext) as any;
139
+ expect(result.status).toBe(401);
140
+ });
141
+
142
+ it('rejects requests with an invalid JWT', async () => {
143
+ const middleware = jwtAuthMiddleware();
144
+ const ctx = makeMockContext({ authHeader: 'Bearer invalid.token.here' });
145
+ const result = await middleware(ctx as any, mockNext) as any;
146
+ expect(result.status).toBe(401);
147
+ });
148
+
149
+ it('accepts a valid JWT and injects user and tenantId into context', async () => {
150
+ const token = await signJWT(TEST_PAYLOAD, TEST_SECRET);
151
+ const middleware = jwtAuthMiddleware();
152
+ const contextValues: Record<string, unknown> = {};
153
+ const ctx = {
154
+ req: {
155
+ method: 'GET',
156
+ path: '/api/products',
157
+ header: (name: string) => name === 'Authorization' ? `Bearer ${token}` : undefined,
158
+ },
159
+ env: { JWT_SECRET: TEST_SECRET, ENVIRONMENT: 'test' },
160
+ json: vi.fn(),
161
+ header: vi.fn(),
162
+ get: (key: string) => contextValues[key],
163
+ set: (key: string, value: unknown) => { contextValues[key] = value; },
164
+ };
165
+
166
+ await middleware(ctx as any, mockNext);
167
+ expect(mockNext).toHaveBeenCalled();
168
+ expect(contextValues['user']).toMatchObject({
169
+ userId: 'user_001',
170
+ tenantId: 'tenant_abc',
171
+ role: 'TENANT_ADMIN',
172
+ });
173
+ expect(contextValues['tenantId']).toBe('tenant_abc');
174
+ });
175
+ });
176
+
177
+ // ─── requireRole ─────────────────────────────────────────────────────────────
178
+
179
+ describe('requireRole', () => {
180
+ beforeEach(() => vi.clearAllMocks());
181
+
182
+ const makeCtxWithUser = (role: string) => {
183
+ const user: AuthUser = { userId: 'u1', email: 'a@b.com', role, tenantId: 't1', permissions: [] };
184
+ return {
185
+ get: (key: string) => key === 'user' ? user : undefined,
186
+ json: (body: unknown, status?: number) => ({ body, status: status ?? 200 }),
187
+ };
188
+ };
189
+
190
+ it('allows a user with the correct role', async () => {
191
+ const middleware = requireRole(['TENANT_ADMIN', 'SUPER_ADMIN']);
192
+ await middleware(makeCtxWithUser('TENANT_ADMIN') as any, mockNext);
193
+ expect(mockNext).toHaveBeenCalled();
194
+ });
195
+
196
+ it('blocks a user with an incorrect role', async () => {
197
+ const middleware = requireRole(['SUPER_ADMIN']);
198
+ const result = await middleware(makeCtxWithUser('CUSTOMER') as any, mockNext) as any;
199
+ expect(result.status).toBe(403);
200
+ expect(mockNext).not.toHaveBeenCalled();
201
+ });
202
+
203
+ it('returns 401 if no user in context', async () => {
204
+ const middleware = requireRole(['SUPER_ADMIN']);
205
+ const ctx = { get: () => undefined, json: (body: unknown, status?: number) => ({ body, status }) };
206
+ const result = await middleware(ctx as any, mockNext) as any;
207
+ expect(result.status).toBe(401);
208
+ });
209
+ });
210
+
211
+ // ─── requirePermissions ───────────────────────────────────────────────────────
212
+
213
+ describe('requirePermissions', () => {
214
+ beforeEach(() => vi.clearAllMocks());
215
+
216
+ const makeCtxWithPerms = (role: string, permissions: string[]) => {
217
+ const user: AuthUser = { userId: 'u1', email: 'a@b.com', role, tenantId: 't1', permissions };
218
+ return {
219
+ get: (key: string) => key === 'user' ? user : undefined,
220
+ json: (body: unknown, status?: number) => ({ body, status: status ?? 200 }),
221
+ };
222
+ };
223
+
224
+ it('allows SUPER_ADMIN regardless of permissions', async () => {
225
+ const middleware = requirePermissions(['delete:tenants']);
226
+ await middleware(makeCtxWithPerms('SUPER_ADMIN', []) as any, mockNext);
227
+ expect(mockNext).toHaveBeenCalled();
228
+ });
229
+
230
+ it('allows a user with all required permissions', async () => {
231
+ const middleware = requirePermissions(['read:products', 'write:products']);
232
+ await middleware(makeCtxWithPerms('STAFF', ['read:products', 'write:products', 'read:orders']) as any, mockNext);
233
+ expect(mockNext).toHaveBeenCalled();
234
+ });
235
+
236
+ it('blocks a user missing a required permission', async () => {
237
+ const middleware = requirePermissions(['delete:products']);
238
+ const result = await middleware(makeCtxWithPerms('STAFF', ['read:products']) as any, mockNext) as any;
239
+ expect(result.status).toBe(403);
240
+ });
241
+ });
242
+
243
+ // ─── getTenantId / getAuthUser ────────────────────────────────────────────────
244
+
245
+ describe('getTenantId', () => {
246
+ it('returns tenantId from context', () => {
247
+ const ctx = { get: (key: string) => key === 'tenantId' ? 'tenant_xyz' : undefined };
248
+ expect(getTenantId(ctx as any)).toBe('tenant_xyz');
249
+ });
250
+
251
+ it('throws if tenantId is not in context', () => {
252
+ const ctx = { get: () => undefined };
253
+ expect(() => getTenantId(ctx as any)).toThrow();
254
+ });
255
+ });
256
+
257
+ describe('getAuthUser', () => {
258
+ it('returns user from context', () => {
259
+ const user: AuthUser = { userId: 'u1', email: 'a@b.com', role: 'STAFF', tenantId: 't1', permissions: [] };
260
+ const ctx = { get: (key: string) => key === 'user' ? user : undefined };
261
+ expect(getAuthUser(ctx as any)).toEqual(user);
262
+ });
263
+
264
+ it('throws if user is not in context', () => {
265
+ const ctx = { get: () => undefined };
266
+ expect(() => getAuthUser(ctx as any)).toThrow();
267
+ });
268
+ });