@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.
- package/CHANGELOG.md +70 -0
- package/dist/ai.d.ts +25 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +53 -0
- package/dist/ai.js.map +1 -0
- package/dist/core/ai/AIEngine.d.ts +69 -0
- package/dist/core/ai/AIEngine.d.ts.map +1 -0
- package/dist/core/ai/AIEngine.js +185 -0
- package/dist/core/ai/AIEngine.js.map +1 -0
- package/dist/core/auth/index.d.ts +183 -0
- package/dist/core/auth/index.d.ts.map +1 -0
- package/dist/core/auth/index.js +369 -0
- package/dist/core/auth/index.js.map +1 -0
- package/dist/core/billing/index.d.ts +52 -0
- package/dist/core/billing/index.d.ts.map +1 -0
- package/dist/core/billing/index.js +91 -0
- package/dist/core/billing/index.js.map +1 -0
- package/dist/core/booking/index.d.ts +38 -0
- package/dist/core/booking/index.d.ts.map +1 -0
- package/dist/core/booking/index.js +60 -0
- package/dist/core/booking/index.js.map +1 -0
- package/dist/core/chat/index.d.ts +48 -0
- package/dist/core/chat/index.d.ts.map +1 -0
- package/dist/core/chat/index.js +83 -0
- package/dist/core/chat/index.js.map +1 -0
- package/dist/core/document/index.d.ts +41 -0
- package/dist/core/document/index.d.ts.map +1 -0
- package/dist/core/document/index.js +68 -0
- package/dist/core/document/index.js.map +1 -0
- package/dist/core/events/index.d.ts +64 -0
- package/dist/core/events/index.d.ts.map +1 -0
- package/dist/core/events/index.js +60 -0
- package/dist/core/events/index.js.map +1 -0
- package/dist/core/geolocation/index.d.ts +32 -0
- package/dist/core/geolocation/index.d.ts.map +1 -0
- package/dist/core/geolocation/index.js +50 -0
- package/dist/core/geolocation/index.js.map +1 -0
- package/dist/core/kyc/index.d.ts +37 -0
- package/dist/core/kyc/index.d.ts.map +1 -0
- package/dist/core/kyc/index.js +60 -0
- package/dist/core/kyc/index.js.map +1 -0
- package/dist/core/logger/index.d.ts +60 -0
- package/dist/core/logger/index.d.ts.map +1 -0
- package/dist/core/logger/index.js +91 -0
- package/dist/core/logger/index.js.map +1 -0
- package/dist/core/notifications/index.d.ts +41 -0
- package/dist/core/notifications/index.d.ts.map +1 -0
- package/dist/core/notifications/index.js +111 -0
- package/dist/core/notifications/index.js.map +1 -0
- package/dist/core/rbac/index.d.ts +43 -0
- package/dist/core/rbac/index.d.ts.map +1 -0
- package/dist/core/rbac/index.js +66 -0
- package/dist/core/rbac/index.js.map +1 -0
- package/dist/events.d.ts +23 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +22 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/kyc.d.ts +12 -0
- package/dist/kyc.d.ts.map +1 -0
- package/dist/kyc.js +2 -0
- package/dist/kyc.js.map +1 -0
- package/dist/nanoid.d.ts +8 -0
- package/dist/nanoid.d.ts.map +1 -0
- package/dist/nanoid.js +15 -0
- package/dist/nanoid.js.map +1 -0
- package/dist/ndpr.d.ts +13 -0
- package/dist/ndpr.d.ts.map +1 -0
- package/dist/ndpr.js +19 -0
- package/dist/ndpr.js.map +1 -0
- package/dist/optimistic-lock.d.ts +11 -0
- package/dist/optimistic-lock.d.ts.map +1 -0
- package/dist/optimistic-lock.js +24 -0
- package/dist/optimistic-lock.js.map +1 -0
- package/dist/payment.d.ts +41 -0
- package/dist/payment.d.ts.map +1 -0
- package/dist/payment.js +116 -0
- package/dist/payment.js.map +1 -0
- package/dist/pin.d.ts +6 -0
- package/dist/pin.d.ts.map +1 -0
- package/dist/pin.js +18 -0
- package/dist/pin.js.map +1 -0
- package/dist/query-helpers.d.ts +18 -0
- package/dist/query-helpers.d.ts.map +1 -0
- package/dist/query-helpers.js +22 -0
- package/dist/query-helpers.js.map +1 -0
- package/dist/rate-limit.d.ts +13 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +16 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/sms.d.ts +23 -0
- package/dist/sms.d.ts.map +1 -0
- package/dist/sms.js +60 -0
- package/dist/sms.js.map +1 -0
- package/dist/tax.d.ts +25 -0
- package/dist/tax.d.ts.map +1 -0
- package/dist/tax.js +31 -0
- package/dist/tax.js.map +1 -0
- package/package.json +99 -0
- package/src/ai.test.ts +146 -0
- package/src/ai.ts +75 -0
- package/src/core/ai/AIEngine.test.ts +386 -0
- package/src/core/ai/AIEngine.ts +281 -0
- package/src/core/auth/index.test.ts +268 -0
- package/src/core/auth/index.ts +570 -0
- package/src/core/billing/index.test.ts +154 -0
- package/src/core/billing/index.ts +132 -0
- package/src/core/booking/index.test.ts +153 -0
- package/src/core/booking/index.ts +91 -0
- package/src/core/chat/index.test.ts +159 -0
- package/src/core/chat/index.ts +130 -0
- package/src/core/document/index.test.ts +106 -0
- package/src/core/document/index.ts +99 -0
- package/src/core/events/index.test.ts +91 -0
- package/src/core/events/index.ts +91 -0
- package/src/core/geolocation/index.test.ts +70 -0
- package/src/core/geolocation/index.ts +69 -0
- package/src/core/kyc/index.test.ts +105 -0
- package/src/core/kyc/index.ts +86 -0
- package/src/core/logger/index.test.ts +110 -0
- package/src/core/logger/index.ts +127 -0
- package/src/core/notifications/index.test.ts +85 -0
- package/src/core/notifications/index.ts +136 -0
- package/src/core/rbac/index.test.ts +81 -0
- package/src/core/rbac/index.ts +85 -0
- package/src/events.test.ts +43 -0
- package/src/events.ts +23 -0
- package/src/index.test.ts +123 -0
- package/src/index.ts +97 -0
- package/src/kyc.ts +23 -0
- package/src/nanoid.test.ts +43 -0
- package/src/nanoid.ts +16 -0
- package/src/ndpr.test.ts +68 -0
- package/src/ndpr.ts +49 -0
- package/src/optimistic-lock.test.ts +75 -0
- package/src/optimistic-lock.ts +36 -0
- package/src/payment.test.ts +152 -0
- package/src/payment.ts +163 -0
- package/src/pin.test.ts +57 -0
- package/src/pin.ts +38 -0
- package/src/query-helpers.test.ts +98 -0
- package/src/query-helpers.ts +36 -0
- package/src/rate-limit.test.ts +98 -0
- package/src/rate-limit.ts +33 -0
- package/src/sms.test.ts +112 -0
- package/src/sms.ts +85 -0
- package/src/tax.test.ts +85 -0
- package/src/tax.ts +57 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @webwaka/core — Auth Module
|
|
3
|
+
* Blueprint Reference: Part 9.2 (Universal Architecture Standards — Auth & Authorization)
|
|
4
|
+
*
|
|
5
|
+
* Canonical authentication primitives for all WebWaka OS v4 Cloudflare Workers.
|
|
6
|
+
*
|
|
7
|
+
* Invariants enforced:
|
|
8
|
+
* - Build Once Use Infinitely: single implementation, all suites import from here.
|
|
9
|
+
* - tenantId ALWAYS sourced from validated JWT payload, NEVER from request headers.
|
|
10
|
+
* - CORS NEVER uses wildcard `origin: '*'` in production.
|
|
11
|
+
* - All auth/mutation endpoints MUST apply rateLimit middleware.
|
|
12
|
+
*
|
|
13
|
+
* Exports:
|
|
14
|
+
* - signJWT() — Issue a signed HS256 JWT (used by super-admin-v2 login)
|
|
15
|
+
* - verifyJWT() — Verify & decode an HS256 JWT (used by all suites)
|
|
16
|
+
* - jwtAuthMiddleware() — Hono middleware: verify token, inject user into context
|
|
17
|
+
* - requireRole() — Hono middleware factory: enforce RBAC after jwtAuthMiddleware
|
|
18
|
+
* - secureCORS() — Hono CORS middleware with environment-aware origin allowlist
|
|
19
|
+
* - rateLimit() — Hono middleware: KV-backed sliding-window rate limiter
|
|
20
|
+
* - AuthUser — Canonical user session type
|
|
21
|
+
* - JWTPayload — Canonical JWT payload type
|
|
22
|
+
*/
|
|
23
|
+
import { cors } from 'hono/cors';
|
|
24
|
+
import { logger } from '../logger/index.js';
|
|
25
|
+
// ─── JWT Utilities ────────────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Encodes a string as a URL-safe Base64 string (no padding).
|
|
28
|
+
* Uses TextEncoder so any Unicode content (accented chars, CJK, Arabic, etc.)
|
|
29
|
+
* is first converted to UTF-8 bytes before btoa — avoiding InvalidCharacterError.
|
|
30
|
+
*/
|
|
31
|
+
function toBase64Url(str) {
|
|
32
|
+
const bytes = new TextEncoder().encode(str);
|
|
33
|
+
let binary = '';
|
|
34
|
+
for (const byte of bytes) {
|
|
35
|
+
binary += String.fromCharCode(byte);
|
|
36
|
+
}
|
|
37
|
+
return btoa(binary).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Decodes a URL-safe Base64 string back to its original UTF-8 string.
|
|
41
|
+
* Inverse of toBase64Url — required for verifying payloads that contain Unicode.
|
|
42
|
+
*/
|
|
43
|
+
function fromBase64Url(b64url) {
|
|
44
|
+
const padded = b64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
45
|
+
const binary = atob(padded);
|
|
46
|
+
const bytes = new Uint8Array(binary.length);
|
|
47
|
+
for (let i = 0; i < binary.length; i++) {
|
|
48
|
+
bytes[i] = binary.charCodeAt(i);
|
|
49
|
+
}
|
|
50
|
+
return new TextDecoder().decode(bytes);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Sign a new HS256 JWT using the Web Crypto API (Cloudflare Workers compatible).
|
|
54
|
+
* Returns a compact `header.payload.signature` string.
|
|
55
|
+
*/
|
|
56
|
+
export async function signJWT(payload, secret, expiresInSeconds = 86400) {
|
|
57
|
+
const now = Math.floor(Date.now() / 1000);
|
|
58
|
+
const fullPayload = {
|
|
59
|
+
...payload,
|
|
60
|
+
iat: now,
|
|
61
|
+
exp: now + expiresInSeconds,
|
|
62
|
+
};
|
|
63
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
64
|
+
// Header is always ASCII — btoa is fine; payload uses Unicode-safe toBase64Url
|
|
65
|
+
const headerB64 = btoa(JSON.stringify(header))
|
|
66
|
+
.replace(/=/g, '')
|
|
67
|
+
.replace(/\+/g, '-')
|
|
68
|
+
.replace(/\//g, '_');
|
|
69
|
+
const payloadB64 = toBase64Url(JSON.stringify(fullPayload));
|
|
70
|
+
const encoder = new TextEncoder();
|
|
71
|
+
const keyData = encoder.encode(secret);
|
|
72
|
+
const key = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
73
|
+
const data = encoder.encode(`${headerB64}.${payloadB64}`);
|
|
74
|
+
const signatureBuffer = await crypto.subtle.sign('HMAC', key, data);
|
|
75
|
+
const signatureB64 = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)))
|
|
76
|
+
.replace(/=/g, '')
|
|
77
|
+
.replace(/\+/g, '-')
|
|
78
|
+
.replace(/\//g, '_');
|
|
79
|
+
return `${headerB64}.${payloadB64}.${signatureB64}`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Verify an HS256 JWT and return its decoded payload.
|
|
83
|
+
* Returns null if the token is invalid, expired, or tampered with.
|
|
84
|
+
* Uses the Web Crypto API — safe for Cloudflare Workers edge runtime.
|
|
85
|
+
*/
|
|
86
|
+
export async function verifyJWT(token, secret) {
|
|
87
|
+
try {
|
|
88
|
+
const parts = token.split('.');
|
|
89
|
+
if (parts.length !== 3)
|
|
90
|
+
return null;
|
|
91
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
92
|
+
const encoder = new TextEncoder();
|
|
93
|
+
const keyData = encoder.encode(secret);
|
|
94
|
+
const key = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
95
|
+
const data = encoder.encode(`${headerB64}.${payloadB64}`);
|
|
96
|
+
const signature = Uint8Array.from(atob(signatureB64.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
|
|
97
|
+
const valid = await crypto.subtle.verify('HMAC', key, signature, data);
|
|
98
|
+
if (!valid)
|
|
99
|
+
return null;
|
|
100
|
+
const payload = JSON.parse(fromBase64Url(payloadB64));
|
|
101
|
+
// Reject expired tokens
|
|
102
|
+
if (payload.exp < Math.floor(Date.now() / 1000))
|
|
103
|
+
return null;
|
|
104
|
+
return payload;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ─── API Key Authentication ───────────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* Verifies an API key by hashing it with SHA-256 and looking it up in the
|
|
113
|
+
* api_keys table. Returns a WakaUser on success or null if the key is invalid.
|
|
114
|
+
* Compatible with Cloudflare Workers (Web Crypto API only).
|
|
115
|
+
*/
|
|
116
|
+
export async function verifyApiKey(rawKey, db) {
|
|
117
|
+
const encoder = new TextEncoder();
|
|
118
|
+
const data = encoder.encode(rawKey);
|
|
119
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
120
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
121
|
+
const keyHash = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
122
|
+
const row = await db
|
|
123
|
+
.prepare(`SELECT ak.*, o.name as operator_name FROM api_keys ak
|
|
124
|
+
JOIN operators o ON o.id = ak.operator_id
|
|
125
|
+
WHERE ak.key_hash = ? AND ak.revoked_at IS NULL AND ak.deleted_at IS NULL`)
|
|
126
|
+
.bind(keyHash)
|
|
127
|
+
.first();
|
|
128
|
+
if (!row)
|
|
129
|
+
return null;
|
|
130
|
+
// Non-blocking: update last_used_at
|
|
131
|
+
db.prepare(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`)
|
|
132
|
+
.bind(Date.now(), row.id)
|
|
133
|
+
.run()
|
|
134
|
+
.catch(() => { });
|
|
135
|
+
return {
|
|
136
|
+
id: row.id,
|
|
137
|
+
tenant_id: row.operator_id,
|
|
138
|
+
role: row.scope === 'read_write' ? 'TENANT_ADMIN' : 'STAFF',
|
|
139
|
+
name: `api_key:${row.id}`,
|
|
140
|
+
phone: '',
|
|
141
|
+
operator_id: row.operator_id,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Hono middleware that verifies the Bearer JWT, rejects invalid/expired tokens,
|
|
146
|
+
* and injects the canonical `AuthUser` into the Hono context as `c.get('user')`.
|
|
147
|
+
*
|
|
148
|
+
* Also sets `c.get('tenantId')` from the JWT payload — NEVER from headers.
|
|
149
|
+
*
|
|
150
|
+
* Usage:
|
|
151
|
+
* app.use('/api/*', jwtAuthMiddleware({ publicRoutes: [{ path: '/health' }] }));
|
|
152
|
+
*/
|
|
153
|
+
export function jwtAuthMiddleware(options = {}) {
|
|
154
|
+
const { publicRoutes = [] } = options;
|
|
155
|
+
return async (c, next) => {
|
|
156
|
+
const method = c.req.method;
|
|
157
|
+
const path = c.req.path;
|
|
158
|
+
// Allow public routes through without auth
|
|
159
|
+
const isPublic = publicRoutes.some((r) => path.startsWith(r.path) && (!r.method || r.method === '*' || r.method === method));
|
|
160
|
+
if (isPublic)
|
|
161
|
+
return next();
|
|
162
|
+
const authHeader = c.req.header('Authorization') ?? '';
|
|
163
|
+
// ── API Key auth (B2B / third-party systems) ──────────────────────────────
|
|
164
|
+
if (authHeader.startsWith('ApiKey ')) {
|
|
165
|
+
const rawKey = authHeader.slice(7).trim();
|
|
166
|
+
const db = c.env.DB;
|
|
167
|
+
if (!db) {
|
|
168
|
+
return c.json({ success: false, error: 'Unauthorized: DB binding not configured' }, 401);
|
|
169
|
+
}
|
|
170
|
+
const wakaUser = await verifyApiKey(rawKey, db);
|
|
171
|
+
if (!wakaUser) {
|
|
172
|
+
return c.json({ error: 'Invalid API key' }, 401);
|
|
173
|
+
}
|
|
174
|
+
c.set('user', wakaUser);
|
|
175
|
+
c.set('tenantId', wakaUser.operator_id);
|
|
176
|
+
return next();
|
|
177
|
+
}
|
|
178
|
+
// ── JWT Bearer auth ───────────────────────────────────────────────────────
|
|
179
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
180
|
+
return c.json({ success: false, error: 'Unauthorized: missing or malformed Authorization header' }, 401);
|
|
181
|
+
}
|
|
182
|
+
const token = authHeader.slice(7).trim();
|
|
183
|
+
if (!token) {
|
|
184
|
+
return c.json({ success: false, error: 'Unauthorized: empty token' }, 401);
|
|
185
|
+
}
|
|
186
|
+
const payload = await verifyJWT(token, c.env.JWT_SECRET);
|
|
187
|
+
if (!payload) {
|
|
188
|
+
return c.json({ success: false, error: 'Unauthorized: invalid or expired token' }, 401);
|
|
189
|
+
}
|
|
190
|
+
const user = {
|
|
191
|
+
userId: payload.sub,
|
|
192
|
+
email: payload.email,
|
|
193
|
+
role: payload.role,
|
|
194
|
+
tenantId: payload.tenantId,
|
|
195
|
+
permissions: payload.permissions ?? [],
|
|
196
|
+
};
|
|
197
|
+
// Inject into Hono context — downstream handlers use c.get('user') and c.get('tenantId')
|
|
198
|
+
c.set('user', user);
|
|
199
|
+
c.set('tenantId', payload.tenantId);
|
|
200
|
+
return next();
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// ─── Hono Middleware: RBAC ────────────────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* Hono middleware factory that enforces role-based access control.
|
|
206
|
+
* MUST be used AFTER jwtAuthMiddleware on the same route.
|
|
207
|
+
*
|
|
208
|
+
* Usage:
|
|
209
|
+
* app.post('/api/admin/tenants', requireRole(['SUPER_ADMIN', 'TENANT_ADMIN']), handler);
|
|
210
|
+
*/
|
|
211
|
+
export function requireRole(allowedRoles) {
|
|
212
|
+
return async (c, next) => {
|
|
213
|
+
const user = c.get('user');
|
|
214
|
+
if (!user) {
|
|
215
|
+
return c.json({ success: false, error: 'Unauthorized: no authenticated user' }, 401);
|
|
216
|
+
}
|
|
217
|
+
if (!allowedRoles.includes(user.role)) {
|
|
218
|
+
return c.json({
|
|
219
|
+
success: false,
|
|
220
|
+
error: `Forbidden: requires one of [${allowedRoles.join(', ')}]`,
|
|
221
|
+
}, 403);
|
|
222
|
+
}
|
|
223
|
+
return next();
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Hono middleware factory that enforces permission-based access control.
|
|
228
|
+
* SUPER_ADMIN role bypasses all permission checks.
|
|
229
|
+
* MUST be used AFTER jwtAuthMiddleware.
|
|
230
|
+
*
|
|
231
|
+
* Usage:
|
|
232
|
+
* app.delete('/api/tenants/:id', requirePermissions(['delete:tenants']), handler);
|
|
233
|
+
*/
|
|
234
|
+
export function requirePermissions(requiredPermissions) {
|
|
235
|
+
return async (c, next) => {
|
|
236
|
+
const user = c.get('user');
|
|
237
|
+
if (!user) {
|
|
238
|
+
return c.json({ success: false, error: 'Unauthorized: no authenticated user' }, 401);
|
|
239
|
+
}
|
|
240
|
+
if (user.role === 'SUPER_ADMIN')
|
|
241
|
+
return next();
|
|
242
|
+
const hasAll = requiredPermissions.every((p) => user.permissions.includes(p));
|
|
243
|
+
if (!hasAll) {
|
|
244
|
+
return c.json({
|
|
245
|
+
success: false,
|
|
246
|
+
error: `Forbidden: missing required permissions [${requiredPermissions.join(', ')}]`,
|
|
247
|
+
}, 403);
|
|
248
|
+
}
|
|
249
|
+
return next();
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Environment-aware CORS middleware.
|
|
254
|
+
* - Production: restricts to an explicit allowlist of WebWaka domains.
|
|
255
|
+
* - Non-production (staging/dev/local): allows all origins for developer convenience.
|
|
256
|
+
*
|
|
257
|
+
* NEVER uses `origin: '*'` in production.
|
|
258
|
+
*
|
|
259
|
+
* Usage:
|
|
260
|
+
* app.use('*', secureCORS());
|
|
261
|
+
* app.use('*', secureCORS({ allowedOrigins: ['https://app.mywebwaka.com'] }));
|
|
262
|
+
*/
|
|
263
|
+
export function secureCORS(options = {}) {
|
|
264
|
+
const defaultProductionOrigins = [
|
|
265
|
+
'https://webwaka.com',
|
|
266
|
+
'https://app.webwaka.com',
|
|
267
|
+
'https://admin.webwaka.com',
|
|
268
|
+
'https://webwaka-super-admin.pages.dev',
|
|
269
|
+
];
|
|
270
|
+
const allowedOrigins = options.allowedOrigins ?? defaultProductionOrigins;
|
|
271
|
+
return cors({
|
|
272
|
+
origin: (origin, c) => {
|
|
273
|
+
const env = c.env.ENVIRONMENT ?? 'production';
|
|
274
|
+
const isProd = env === 'production';
|
|
275
|
+
if (!isProd) {
|
|
276
|
+
// Allow all origins in non-production environments
|
|
277
|
+
return origin;
|
|
278
|
+
}
|
|
279
|
+
// In production: only allow explicitly listed origins
|
|
280
|
+
if (allowedOrigins.includes(origin))
|
|
281
|
+
return origin;
|
|
282
|
+
// Block all other origins in production
|
|
283
|
+
return null;
|
|
284
|
+
},
|
|
285
|
+
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
286
|
+
allowHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
|
|
287
|
+
exposeHeaders: ['X-Request-ID'],
|
|
288
|
+
maxAge: 86400,
|
|
289
|
+
credentials: true,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* KV-backed sliding-window rate limiter for Cloudflare Workers.
|
|
294
|
+
* Requires a `RATE_LIMIT_KV` KV namespace binding.
|
|
295
|
+
*
|
|
296
|
+
* Usage (auth endpoints — strict):
|
|
297
|
+
* app.post('/auth/login', rateLimit({ limit: 10, windowSeconds: 60, keyPrefix: 'login' }), handler);
|
|
298
|
+
*
|
|
299
|
+
* Usage (general API):
|
|
300
|
+
* app.use('/api/*', rateLimit({ limit: 300, windowSeconds: 60 }));
|
|
301
|
+
*/
|
|
302
|
+
export function rateLimit(options = {}) {
|
|
303
|
+
const { limit = 60, windowSeconds = 60, keyPrefix = 'rl', keyExtractor, } = options;
|
|
304
|
+
return async (c, next) => {
|
|
305
|
+
const kv = c.env.RATE_LIMIT_KV;
|
|
306
|
+
if (!kv) {
|
|
307
|
+
// If KV is not configured, fail open (do not block) but log a warning
|
|
308
|
+
logger.warn('[rateLimit] RATE_LIMIT_KV binding not configured — rate limiting disabled');
|
|
309
|
+
return next();
|
|
310
|
+
}
|
|
311
|
+
const clientKey = keyExtractor
|
|
312
|
+
? keyExtractor(c)
|
|
313
|
+
: (c.req.header('CF-Connecting-IP') ?? c.req.header('X-Forwarded-For') ?? 'unknown');
|
|
314
|
+
const windowStart = Math.floor(Date.now() / 1000 / windowSeconds);
|
|
315
|
+
const kvKey = `${keyPrefix}:${clientKey}:${windowStart}`;
|
|
316
|
+
let count = 0;
|
|
317
|
+
try {
|
|
318
|
+
const existing = await kv.get(kvKey);
|
|
319
|
+
count = existing ? parseInt(existing, 10) : 0;
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// Fail open on KV errors
|
|
323
|
+
return next();
|
|
324
|
+
}
|
|
325
|
+
if (count >= limit) {
|
|
326
|
+
return c.json({
|
|
327
|
+
success: false,
|
|
328
|
+
error: `Rate limit exceeded. Maximum ${limit} requests per ${windowSeconds}s.`,
|
|
329
|
+
}, 429);
|
|
330
|
+
}
|
|
331
|
+
// Increment counter; set TTL to expire at end of window
|
|
332
|
+
try {
|
|
333
|
+
await kv.put(kvKey, String(count + 1), { expirationTtl: windowSeconds * 2 });
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Fail open on KV write errors
|
|
337
|
+
}
|
|
338
|
+
// Attach rate limit headers
|
|
339
|
+
c.header('X-RateLimit-Limit', String(limit));
|
|
340
|
+
c.header('X-RateLimit-Remaining', String(Math.max(0, limit - count - 1)));
|
|
341
|
+
c.header('X-RateLimit-Reset', String((windowStart + 1) * windowSeconds));
|
|
342
|
+
return next();
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
// ─── Tenant Utilities ─────────────────────────────────────────────────────────
|
|
346
|
+
/**
|
|
347
|
+
* Safely extract tenantId from the Hono context.
|
|
348
|
+
* Throws if tenantId is not present (i.e., jwtAuthMiddleware was not applied).
|
|
349
|
+
* Use this in route handlers to enforce that tenant context is always present.
|
|
350
|
+
*/
|
|
351
|
+
export function getTenantId(c) {
|
|
352
|
+
const tenantId = c.get('tenantId');
|
|
353
|
+
if (!tenantId) {
|
|
354
|
+
throw new Error('getTenantId() called without jwtAuthMiddleware — tenantId not in context');
|
|
355
|
+
}
|
|
356
|
+
return tenantId;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Safely extract the authenticated user from the Hono context.
|
|
360
|
+
* Throws if user is not present.
|
|
361
|
+
*/
|
|
362
|
+
export function getAuthUser(c) {
|
|
363
|
+
const user = c.get('user');
|
|
364
|
+
if (!user) {
|
|
365
|
+
throw new Error('getAuthUser() called without jwtAuthMiddleware — user not in context');
|
|
366
|
+
}
|
|
367
|
+
return user;
|
|
368
|
+
}
|
|
369
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/auth/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORE-8: Platform Billing & Usage Ledger
|
|
3
|
+
* Blueprint Reference: Part 10.1 (Central Management & Economics)
|
|
4
|
+
* Blueprint Reference: Part 9.1 #6 (Africa First - Integer Kobo Values)
|
|
5
|
+
*
|
|
6
|
+
* Implements internal ledger for tracking tenant API/AI usage and credits.
|
|
7
|
+
*/
|
|
8
|
+
export declare enum LedgerEntryType {
|
|
9
|
+
CREDIT = "CREDIT",
|
|
10
|
+
DEBIT = "DEBIT"
|
|
11
|
+
}
|
|
12
|
+
export declare enum UsageCategory {
|
|
13
|
+
AI_TOKENS = "AI_TOKENS",
|
|
14
|
+
SMS_SENT = "SMS_SENT",
|
|
15
|
+
EMAIL_SENT = "EMAIL_SENT",
|
|
16
|
+
SUBSCRIPTION_FEE = "SUBSCRIPTION_FEE",
|
|
17
|
+
SUBSCRIPTION_CREDIT = "SUBSCRIPTION_CREDIT"
|
|
18
|
+
}
|
|
19
|
+
export interface LedgerEntry {
|
|
20
|
+
id: string;
|
|
21
|
+
tenantId: string;
|
|
22
|
+
type: LedgerEntryType;
|
|
23
|
+
category: UsageCategory;
|
|
24
|
+
amountKobo: number;
|
|
25
|
+
description: string;
|
|
26
|
+
metadata?: Record<string, any>;
|
|
27
|
+
createdAt: Date;
|
|
28
|
+
deletedAt?: Date;
|
|
29
|
+
}
|
|
30
|
+
export declare class BillingLedger {
|
|
31
|
+
private db;
|
|
32
|
+
constructor(db: any);
|
|
33
|
+
/**
|
|
34
|
+
* Records a usage debit for a tenant.
|
|
35
|
+
*/
|
|
36
|
+
recordUsage(tenantId: string, category: UsageCategory, amountKobo: number, description: string, metadata?: Record<string, any>): Promise<LedgerEntry>;
|
|
37
|
+
/**
|
|
38
|
+
* Records a credit for a tenant (wallet top-up, refund, promotional credit,
|
|
39
|
+
* or subscription payment).
|
|
40
|
+
*/
|
|
41
|
+
recordCredit(tenantId: string, category: UsageCategory, amountKobo: number, description: string, metadata?: Record<string, any>): Promise<LedgerEntry>;
|
|
42
|
+
/**
|
|
43
|
+
* Calculates the current balance for a tenant.
|
|
44
|
+
*
|
|
45
|
+
* @stub This implementation always returns 0. Real balance calculation
|
|
46
|
+
* requires D1 database wiring (separate task). Any caller that gates
|
|
47
|
+
* actions on this value will silently allow unlimited usage until
|
|
48
|
+
* D1 integration is complete.
|
|
49
|
+
*/
|
|
50
|
+
getTenantBalance(tenantId: string): Promise<number>;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/billing/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,oBAAY,eAAe;IACzB,MAAM,WAAW;IACjB,KAAK,UAAU;CAChB;AAED,oBAAY,aAAa;IACvB,SAAS,cAAc;IACvB,QAAQ,aAAa;IACrB,UAAU,eAAe;IACzB,gBAAgB,qBAAqB;IACrC,mBAAmB,wBAAwB;CAC5C;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,EAAE,aAAa,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AASD,qBAAa,aAAa;IACxB,OAAO,CAAC,EAAE,CAAM;gBAEJ,EAAE,EAAE,GAAG;IAInB;;OAEG;IACG,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,aAAa,EACvB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC7B,OAAO,CAAC,WAAW,CAAC;IAoBvB;;;OAGG;IACG,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,aAAa,EACvB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC7B,OAAO,CAAC,WAAW,CAAC;IAoBvB;;;;;;;OAOG;IACG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAe1D"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORE-8: Platform Billing & Usage Ledger
|
|
3
|
+
* Blueprint Reference: Part 10.1 (Central Management & Economics)
|
|
4
|
+
* Blueprint Reference: Part 9.1 #6 (Africa First - Integer Kobo Values)
|
|
5
|
+
*
|
|
6
|
+
* Implements internal ledger for tracking tenant API/AI usage and credits.
|
|
7
|
+
*/
|
|
8
|
+
import { logger } from '../logger';
|
|
9
|
+
export var LedgerEntryType;
|
|
10
|
+
(function (LedgerEntryType) {
|
|
11
|
+
LedgerEntryType["CREDIT"] = "CREDIT";
|
|
12
|
+
LedgerEntryType["DEBIT"] = "DEBIT";
|
|
13
|
+
})(LedgerEntryType || (LedgerEntryType = {}));
|
|
14
|
+
export var UsageCategory;
|
|
15
|
+
(function (UsageCategory) {
|
|
16
|
+
UsageCategory["AI_TOKENS"] = "AI_TOKENS";
|
|
17
|
+
UsageCategory["SMS_SENT"] = "SMS_SENT";
|
|
18
|
+
UsageCategory["EMAIL_SENT"] = "EMAIL_SENT";
|
|
19
|
+
UsageCategory["SUBSCRIPTION_FEE"] = "SUBSCRIPTION_FEE";
|
|
20
|
+
UsageCategory["SUBSCRIPTION_CREDIT"] = "SUBSCRIPTION_CREDIT";
|
|
21
|
+
})(UsageCategory || (UsageCategory = {}));
|
|
22
|
+
/** Shared kobo validation used by both debit and credit paths. */
|
|
23
|
+
function validateKobo(amountKobo) {
|
|
24
|
+
if (!Number.isInteger(amountKobo) || amountKobo < 0) {
|
|
25
|
+
throw new Error('Amount must be a positive integer in kobo');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class BillingLedger {
|
|
29
|
+
db; // Type would be D1Database from @cloudflare/workers-types
|
|
30
|
+
constructor(db) {
|
|
31
|
+
this.db = db;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Records a usage debit for a tenant.
|
|
35
|
+
*/
|
|
36
|
+
async recordUsage(tenantId, category, amountKobo, description, metadata) {
|
|
37
|
+
validateKobo(amountKobo);
|
|
38
|
+
const entry = {
|
|
39
|
+
id: crypto.randomUUID(),
|
|
40
|
+
tenantId,
|
|
41
|
+
type: LedgerEntryType.DEBIT,
|
|
42
|
+
category,
|
|
43
|
+
amountKobo,
|
|
44
|
+
description,
|
|
45
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
46
|
+
createdAt: new Date(),
|
|
47
|
+
};
|
|
48
|
+
// In a real implementation, this would insert into D1:
|
|
49
|
+
// await this.db.prepare('INSERT INTO ledger_entries ...').bind(...).run();
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Records a credit for a tenant (wallet top-up, refund, promotional credit,
|
|
54
|
+
* or subscription payment).
|
|
55
|
+
*/
|
|
56
|
+
async recordCredit(tenantId, category, amountKobo, description, metadata) {
|
|
57
|
+
validateKobo(amountKobo);
|
|
58
|
+
const entry = {
|
|
59
|
+
id: crypto.randomUUID(),
|
|
60
|
+
tenantId,
|
|
61
|
+
type: LedgerEntryType.CREDIT,
|
|
62
|
+
category,
|
|
63
|
+
amountKobo,
|
|
64
|
+
description,
|
|
65
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
66
|
+
createdAt: new Date(),
|
|
67
|
+
};
|
|
68
|
+
// In a real implementation, this would insert into D1:
|
|
69
|
+
// await this.db.prepare('INSERT INTO ledger_entries ...').bind(...).run();
|
|
70
|
+
return entry;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Calculates the current balance for a tenant.
|
|
74
|
+
*
|
|
75
|
+
* @stub This implementation always returns 0. Real balance calculation
|
|
76
|
+
* requires D1 database wiring (separate task). Any caller that gates
|
|
77
|
+
* actions on this value will silently allow unlimited usage until
|
|
78
|
+
* D1 integration is complete.
|
|
79
|
+
*/
|
|
80
|
+
async getTenantBalance(tenantId) {
|
|
81
|
+
logger.warn('getTenantBalance is a stub and always returns 0. D1 integration is required for real balance calculation.', { tenantId });
|
|
82
|
+
// Real implementation:
|
|
83
|
+
// const result = await this.db.prepare(
|
|
84
|
+
// 'SELECT SUM(CASE WHEN type = "CREDIT" THEN amountKobo ELSE -amountKobo END) as balance ' +
|
|
85
|
+
// 'FROM ledger_entries WHERE tenantId = ? AND deletedAt IS NULL'
|
|
86
|
+
// ).bind(tenantId).first();
|
|
87
|
+
// return result.balance ?? 0;
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/billing/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnC,MAAM,CAAN,IAAY,eAGX;AAHD,WAAY,eAAe;IACzB,oCAAiB,CAAA;IACjB,kCAAe,CAAA;AACjB,CAAC,EAHW,eAAe,KAAf,eAAe,QAG1B;AAED,MAAM,CAAN,IAAY,aAMX;AAND,WAAY,aAAa;IACvB,wCAAuB,CAAA;IACvB,sCAAqB,CAAA;IACrB,0CAAyB,CAAA;IACzB,sDAAqC,CAAA;IACrC,4DAA2C,CAAA;AAC7C,CAAC,EANW,aAAa,KAAb,aAAa,QAMxB;AAcD,kEAAkE;AAClE,SAAS,YAAY,CAAC,UAAkB;IACtC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED,MAAM,OAAO,aAAa;IAChB,EAAE,CAAM,CAAC,0DAA0D;IAE3E,YAAY,EAAO;QACjB,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACf,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,WAAW,CACf,QAAgB,EAChB,QAAuB,EACvB,UAAkB,EAClB,WAAmB,EACnB,QAA8B;QAE9B,YAAY,CAAC,UAAU,CAAC,CAAC;QAEzB,MAAM,KAAK,GAAgB;YACzB,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;YACvB,QAAQ;YACR,IAAI,EAAE,eAAe,CAAC,KAAK;YAC3B,QAAQ;YACR,UAAU;YACV,WAAW;YACX,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/C,SAAS,EAAE,IAAI,IAAI,EAAE;SACtB,CAAC;QAEF,uDAAuD;QACvD,2EAA2E;QAE3E,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,YAAY,CAChB,QAAgB,EAChB,QAAuB,EACvB,UAAkB,EAClB,WAAmB,EACnB,QAA8B;QAE9B,YAAY,CAAC,UAAU,CAAC,CAAC;QAEzB,MAAM,KAAK,GAAgB;YACzB,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;YACvB,QAAQ;YACR,IAAI,EAAE,eAAe,CAAC,MAAM;YAC5B,QAAQ;YACR,UAAU;YACV,WAAW;YACX,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/C,SAAS,EAAE,IAAI,IAAI,EAAE;SACtB,CAAC;QAEF,uDAAuD;QACvD,2EAA2E;QAE3E,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,gBAAgB,CAAC,QAAgB;QACrC,MAAM,CAAC,IAAI,CACT,2GAA2G,EAC3G,EAAE,QAAQ,EAAE,CACb,CAAC;QAEF,uBAAuB;QACvB,wCAAwC;QACxC,+FAA+F;QAC/F,mEAAmE;QACnE,4BAA4B;QAC5B,8BAA8B;QAE9B,OAAO,CAAC,CAAC;IACX,CAAC;CACF"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORE-10: Universal Booking & Scheduling Engine
|
|
3
|
+
* Blueprint Reference: Part 10.3 (Transport), Part 10.7 (Health)
|
|
4
|
+
*
|
|
5
|
+
* Unified system for managing time slots, availability, and reservations.
|
|
6
|
+
*
|
|
7
|
+
* Tenant Isolation: every mutating and querying method requires a tenantId.
|
|
8
|
+
* All data is scoped per tenant — cross-tenant leakage is impossible by construction.
|
|
9
|
+
*/
|
|
10
|
+
export interface TimeSlot {
|
|
11
|
+
startTime: Date;
|
|
12
|
+
endTime: Date;
|
|
13
|
+
}
|
|
14
|
+
export interface Booking {
|
|
15
|
+
id: string;
|
|
16
|
+
tenantId: string;
|
|
17
|
+
resourceId: string;
|
|
18
|
+
userId: string;
|
|
19
|
+
slot: TimeSlot;
|
|
20
|
+
status: 'pending' | 'confirmed' | 'cancelled';
|
|
21
|
+
}
|
|
22
|
+
export declare class BookingEngine {
|
|
23
|
+
private bookings;
|
|
24
|
+
/**
|
|
25
|
+
* Checks if a resource is available for a given time slot within a tenant.
|
|
26
|
+
*/
|
|
27
|
+
isAvailable(tenantId: string, resourceId: string, requestedSlot: TimeSlot): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Creates a new booking if the resource is available within the tenant.
|
|
30
|
+
*/
|
|
31
|
+
createBooking(tenantId: string, resourceId: string, userId: string, slot: TimeSlot): Booking;
|
|
32
|
+
/**
|
|
33
|
+
* Cancels an existing booking, scoped to the tenant.
|
|
34
|
+
* Returns false if the booking does not exist within the tenant.
|
|
35
|
+
*/
|
|
36
|
+
cancelBooking(tenantId: string, bookingId: string): boolean;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/booking/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;CACf;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;CAC/C;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAiB;IAEjC;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,GAAG,OAAO;IAmBnF;;OAEG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO;IAqB5F;;;OAGG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;CAa5D"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORE-10: Universal Booking & Scheduling Engine
|
|
3
|
+
* Blueprint Reference: Part 10.3 (Transport), Part 10.7 (Health)
|
|
4
|
+
*
|
|
5
|
+
* Unified system for managing time slots, availability, and reservations.
|
|
6
|
+
*
|
|
7
|
+
* Tenant Isolation: every mutating and querying method requires a tenantId.
|
|
8
|
+
* All data is scoped per tenant — cross-tenant leakage is impossible by construction.
|
|
9
|
+
*/
|
|
10
|
+
export class BookingEngine {
|
|
11
|
+
bookings = [];
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a resource is available for a given time slot within a tenant.
|
|
14
|
+
*/
|
|
15
|
+
isAvailable(tenantId, resourceId, requestedSlot) {
|
|
16
|
+
const resourceBookings = this.bookings.filter(b => b.tenantId === tenantId &&
|
|
17
|
+
b.resourceId === resourceId &&
|
|
18
|
+
b.status !== 'cancelled');
|
|
19
|
+
for (const booking of resourceBookings) {
|
|
20
|
+
if (requestedSlot.startTime < booking.slot.endTime &&
|
|
21
|
+
requestedSlot.endTime > booking.slot.startTime) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates a new booking if the resource is available within the tenant.
|
|
29
|
+
*/
|
|
30
|
+
createBooking(tenantId, resourceId, userId, slot) {
|
|
31
|
+
if (!this.isAvailable(tenantId, resourceId, slot)) {
|
|
32
|
+
throw new Error('Resource is not available for the requested time slot');
|
|
33
|
+
}
|
|
34
|
+
const newBooking = {
|
|
35
|
+
id: `bk_${crypto.randomUUID()}`,
|
|
36
|
+
tenantId,
|
|
37
|
+
resourceId,
|
|
38
|
+
userId,
|
|
39
|
+
slot,
|
|
40
|
+
status: 'confirmed',
|
|
41
|
+
};
|
|
42
|
+
this.bookings.push(newBooking);
|
|
43
|
+
// eventBus.publish(WebWakaEventType.BOOKING_CONFIRMED, createEvent(WebWakaEventType.BOOKING_CONFIRMED, tenantId, newBooking));
|
|
44
|
+
return newBooking;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Cancels an existing booking, scoped to the tenant.
|
|
48
|
+
* Returns false if the booking does not exist within the tenant.
|
|
49
|
+
*/
|
|
50
|
+
cancelBooking(tenantId, bookingId) {
|
|
51
|
+
const booking = this.bookings.find(b => b.id === bookingId && b.tenantId === tenantId);
|
|
52
|
+
if (booking) {
|
|
53
|
+
booking.status = 'cancelled';
|
|
54
|
+
// eventBus.publish(WebWakaEventType.BOOKING_CANCELLED, createEvent(WebWakaEventType.BOOKING_CANCELLED, tenantId, booking));
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/booking/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgBH,MAAM,OAAO,aAAa;IAChB,QAAQ,GAAc,EAAE,CAAC;IAEjC;;OAEG;IACH,WAAW,CAAC,QAAgB,EAAE,UAAkB,EAAE,aAAuB;QACvE,MAAM,gBAAgB,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAChD,CAAC,CAAC,QAAQ,KAAK,QAAQ;YACvB,CAAC,CAAC,UAAU,KAAK,UAAU;YAC3B,CAAC,CAAC,MAAM,KAAK,WAAW,CACzB,CAAC;QAEF,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;YACvC,IACE,aAAa,CAAC,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO;gBAC9C,aAAa,CAAC,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,EAC9C,CAAC;gBACD,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,QAAgB,EAAE,UAAkB,EAAE,MAAc,EAAE,IAAc;QAChF,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;QAC3E,CAAC;QAED,MAAM,UAAU,GAAY;YAC1B,EAAE,EAAE,MAAM,MAAM,CAAC,UAAU,EAAE,EAAE;YAC/B,QAAQ;YACR,UAAU;YACV,MAAM;YACN,IAAI;YACJ,MAAM,EAAE,WAAW;SACpB,CAAC;QAEF,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAE/B,+HAA+H;QAE/H,OAAO,UAAU,CAAC;IACpB,CAAC;IAED;;;OAGG;IACH,aAAa,CAAC,QAAgB,EAAE,SAAiB;QAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAChC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,CACnD,CAAC;QACF,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,MAAM,GAAG,WAAW,CAAC;YAE7B,4HAA4H;YAE5H,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
|