@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @webwaka/core Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package will be documented in this file.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## [1.0.0] — 2026-03-23
|
|
9
|
+
|
|
10
|
+
### Added — Auth Module (`src/core/auth/index.ts`)
|
|
11
|
+
|
|
12
|
+
This release introduces the **canonical authentication package** for all WebWaka OS v4 Cloudflare Workers, resolving the critical security vulnerabilities identified in the March 2026 cross-repo security audit.
|
|
13
|
+
|
|
14
|
+
#### New Exports
|
|
15
|
+
|
|
16
|
+
| Export | Type | Description |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `signJWT()` | `async function` | Issue a signed HS256 JWT using the Web Crypto API |
|
|
19
|
+
| `verifyJWT()` | `async function` | Verify & decode an HS256 JWT; returns `null` on failure |
|
|
20
|
+
| `jwtAuthMiddleware()` | Hono middleware factory | Verify Bearer JWT, inject `AuthUser` and `tenantId` into context |
|
|
21
|
+
| `requireRole()` | Hono middleware factory | Enforce RBAC after `jwtAuthMiddleware` |
|
|
22
|
+
| `requirePermissions()` | Hono middleware factory | Enforce permission-based access; SUPER_ADMIN bypasses |
|
|
23
|
+
| `secureCORS()` | Hono middleware factory | Environment-aware CORS — never `origin: '*'` in production |
|
|
24
|
+
| `rateLimit()` | Hono middleware factory | KV-backed sliding-window rate limiter |
|
|
25
|
+
| `getTenantId()` | utility | Safely extract `tenantId` from Hono context |
|
|
26
|
+
| `getAuthUser()` | utility | Safely extract `AuthUser` from Hono context |
|
|
27
|
+
| `JWTPayload` | TypeScript interface | Canonical JWT payload shape |
|
|
28
|
+
| `AuthUser` | TypeScript interface | Canonical user context shape |
|
|
29
|
+
| `AuthEnv` | TypeScript interface | Required Cloudflare Worker bindings for auth |
|
|
30
|
+
| `RateLimitEnv` | TypeScript interface | Required bindings for rate limiting (extends `AuthEnv`) |
|
|
31
|
+
|
|
32
|
+
#### Security Invariants Enforced
|
|
33
|
+
|
|
34
|
+
- **`tenantId` ALWAYS sourced from validated JWT payload** — `getTenantId()` throws if `jwtAuthMiddleware` was not applied, making cross-tenant data breaches impossible by construction.
|
|
35
|
+
- **CORS NEVER uses `origin: '*'` in production** — `secureCORS()` enforces an explicit origin allowlist in production environments.
|
|
36
|
+
- **Rate limiting on all auth endpoints** — `rateLimit()` provides KV-backed sliding-window rate limiting with configurable limits per endpoint class.
|
|
37
|
+
- **Cryptographic token verification** — `verifyJWT()` uses `crypto.subtle.verify()` (HMAC-SHA256) available natively in the Cloudflare Workers runtime.
|
|
38
|
+
|
|
39
|
+
#### Package Configuration
|
|
40
|
+
|
|
41
|
+
- Added `package.json` with `@webwaka/core` package name and sub-path exports
|
|
42
|
+
- Added `tsconfig.json` targeting ES2022 with `WebWorker` lib
|
|
43
|
+
- Added `vitest.config.ts` for unit testing
|
|
44
|
+
|
|
45
|
+
#### Tests Added
|
|
46
|
+
|
|
47
|
+
- 22 new unit tests in `src/core/auth/index.test.ts`
|
|
48
|
+
- Covers: sign/verify round-trip, expiry rejection, tamper detection, middleware public routes, RBAC enforcement, permission bypass, context helpers
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- `src/index.ts` — updated to re-export all auth primitives from the new auth module
|
|
53
|
+
- `src/core/rbac/index.ts` — `requireRole` and `requirePermissions` are now superseded by the auth module exports; the RBAC module's mock `verifyJwt` is deprecated
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## [0.1.0] — 2026-02-01 (Initial)
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- CORE-1: Offline Sync Engine (Dexie/IndexedDB)
|
|
62
|
+
- CORE-2: AI Engine abstraction (OpenRouter)
|
|
63
|
+
- CORE-3: Universal Billing Ledger (integer kobo)
|
|
64
|
+
- CORE-4: Notifications (Termii SMS/email)
|
|
65
|
+
- CORE-5: KYC module
|
|
66
|
+
- CORE-6: RBAC primitives (mock implementation — superseded by v1.0.0)
|
|
67
|
+
- CORE-7: Geolocation utilities
|
|
68
|
+
- CORE-8: Document management
|
|
69
|
+
- CORE-9: Chat module
|
|
70
|
+
- CORE-10: Booking engine
|
package/dist/ai.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface AiMessage {
|
|
2
|
+
role: 'system' | 'user' | 'assistant';
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AiCompletionOptions {
|
|
6
|
+
model?: string;
|
|
7
|
+
messages: AiMessage[];
|
|
8
|
+
maxTokens?: number;
|
|
9
|
+
temperature?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface AiCompletionResult {
|
|
12
|
+
content: string;
|
|
13
|
+
model: string;
|
|
14
|
+
tokensUsed: number;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class OpenRouterClient {
|
|
18
|
+
private apiKey;
|
|
19
|
+
private defaultModel;
|
|
20
|
+
private baseUrl;
|
|
21
|
+
constructor(apiKey: string, defaultModel?: string);
|
|
22
|
+
complete(opts: AiCompletionOptions): Promise<AiCompletionResult>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createAiClient(apiKey: string, defaultModel?: string): OpenRouterClient;
|
|
25
|
+
//# sourceMappingURL=ai.d.ts.map
|
package/dist/ai.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../src/ai.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,OAAO,CAAkC;gBAErC,MAAM,EAAE,MAAM,EAAE,YAAY,SAAuB;IAKzD,QAAQ,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAyCvE;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAEtF"}
|
package/dist/ai.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export class OpenRouterClient {
|
|
2
|
+
apiKey;
|
|
3
|
+
defaultModel;
|
|
4
|
+
baseUrl = 'https://openrouter.ai/api/v1';
|
|
5
|
+
constructor(apiKey, defaultModel = 'openai/gpt-4o-mini') {
|
|
6
|
+
this.apiKey = apiKey;
|
|
7
|
+
this.defaultModel = defaultModel;
|
|
8
|
+
}
|
|
9
|
+
async complete(opts) {
|
|
10
|
+
const model = opts.model ?? this.defaultModel;
|
|
11
|
+
try {
|
|
12
|
+
const body = {
|
|
13
|
+
model,
|
|
14
|
+
messages: opts.messages,
|
|
15
|
+
};
|
|
16
|
+
if (opts.maxTokens !== undefined)
|
|
17
|
+
body.max_tokens = opts.maxTokens;
|
|
18
|
+
if (opts.temperature !== undefined)
|
|
19
|
+
body.temperature = opts.temperature;
|
|
20
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
'HTTP-Referer': 'https://webwaka.com',
|
|
26
|
+
'X-Title': 'WebWaka Commerce',
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify(body),
|
|
29
|
+
});
|
|
30
|
+
const data = (await res.json());
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
return {
|
|
33
|
+
content: '',
|
|
34
|
+
model,
|
|
35
|
+
tokensUsed: 0,
|
|
36
|
+
error: data?.error?.message ?? 'OpenRouter request failed',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
content: data.choices?.[0]?.message?.content ?? '',
|
|
41
|
+
model: data.model ?? model,
|
|
42
|
+
tokensUsed: data.usage?.total_tokens ?? 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return { content: '', model, tokensUsed: 0, error: err?.message };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function createAiClient(apiKey, defaultModel) {
|
|
51
|
+
return new OpenRouterClient(apiKey, defaultModel);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=ai.js.map
|
package/dist/ai.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai.js","sourceRoot":"","sources":["../src/ai.ts"],"names":[],"mappings":"AAmBA,MAAM,OAAO,gBAAgB;IACnB,MAAM,CAAS;IACf,YAAY,CAAS;IACrB,OAAO,GAAG,8BAA8B,CAAC;IAEjD,YAAY,MAAc,EAAE,YAAY,GAAG,oBAAoB;QAC7D,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,IAAyB;QACtC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC;QAE9C,IAAI,CAAC;YACH,MAAM,IAAI,GAA4B;gBACpC,KAAK;gBACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;aACxB,CAAC;YACF,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;gBAAE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC;YACnE,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;gBAAE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;YAExE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,mBAAmB,EAAE;gBAC1D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;oBACtC,cAAc,EAAE,kBAAkB;oBAClC,cAAc,EAAE,qBAAqB;oBACrC,SAAS,EAAE,kBAAkB;iBAC9B;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;aAC3B,CAAC,CAAC;YAEH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAQ,CAAC;YACvC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,OAAO;oBACL,OAAO,EAAE,EAAE;oBACX,KAAK;oBACL,UAAU,EAAE,CAAC;oBACb,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,IAAI,2BAA2B;iBAC3D,CAAC;YACJ,CAAC;YAED,OAAO;gBACL,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE;gBAClD,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,KAAK;gBAC1B,UAAU,EAAE,IAAI,CAAC,KAAK,EAAE,YAAY,IAAI,CAAC;aAC1C,CAAC;QACJ,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;QACpE,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,YAAqB;IAClE,OAAO,IAAI,gBAAgB,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORE-5: AI/BYOK Abstraction Engine
|
|
3
|
+
* Blueprint Reference: Part 9.1 #7 (Vendor Neutral AI)
|
|
4
|
+
*
|
|
5
|
+
* Implements a three-tier fallback mechanism with per-tier retry and exponential backoff:
|
|
6
|
+
* 1. Tenant BYOK (Bring Your Own Key) via OpenRouter
|
|
7
|
+
* 2. Platform Key via OpenRouter
|
|
8
|
+
* 3. Cloudflare Workers AI (Ultimate Fallback)
|
|
9
|
+
*
|
|
10
|
+
* Each tier is retried up to `maxRetries` times (default 2) before escalating.
|
|
11
|
+
* Between attempts the engine sleeps for backoffMs * 2^attempt milliseconds.
|
|
12
|
+
*/
|
|
13
|
+
export interface AIRequest {
|
|
14
|
+
prompt: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
tenantId: string;
|
|
17
|
+
}
|
|
18
|
+
export interface AIResponse {
|
|
19
|
+
text: string;
|
|
20
|
+
provider: 'tenant-openrouter' | 'platform-openrouter' | 'cloudflare-ai';
|
|
21
|
+
modelUsed: string;
|
|
22
|
+
}
|
|
23
|
+
export interface TenantConfig {
|
|
24
|
+
openRouterKey?: string;
|
|
25
|
+
preferredModel?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface AIEngineOptions {
|
|
28
|
+
/** Maximum number of retry attempts per tier before escalating. Default: 2 */
|
|
29
|
+
maxRetries?: number;
|
|
30
|
+
/** Base delay in milliseconds for exponential backoff. Default: 200 */
|
|
31
|
+
backoffMs?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare class AIEngine {
|
|
34
|
+
private platformOpenRouterKey;
|
|
35
|
+
private cloudflareAiBinding;
|
|
36
|
+
private maxRetries;
|
|
37
|
+
private backoffMs;
|
|
38
|
+
constructor(platformOpenRouterKey: string, cloudflareAiBinding: any, options?: AIEngineOptions);
|
|
39
|
+
/**
|
|
40
|
+
* Executes an AI request using the three-tier fallback strategy.
|
|
41
|
+
* Each tier is retried up to `maxRetries` times with exponential backoff
|
|
42
|
+
* before escalating to the next tier.
|
|
43
|
+
*/
|
|
44
|
+
execute(request: AIRequest, tenantConfig: TenantConfig): Promise<AIResponse>;
|
|
45
|
+
/**
|
|
46
|
+
* Executes an AI request and returns a ReadableStream<Uint8Array> for
|
|
47
|
+
* server-sent events streaming.
|
|
48
|
+
*
|
|
49
|
+
* - Tier 1 & 2: calls OpenRouter with `stream: true` and pipes `Response.body`.
|
|
50
|
+
* - Tier 3 (CF AI fallback): calls `execute()` and wraps the text in a
|
|
51
|
+
* single-chunk ReadableStream (CF AI does not support streaming natively).
|
|
52
|
+
*/
|
|
53
|
+
executeStream(request: AIRequest, tenantConfig: TenantConfig): Promise<ReadableStream<Uint8Array>>;
|
|
54
|
+
/**
|
|
55
|
+
* Runs `fn` up to `maxRetries + 1` times (1 initial attempt + maxRetries retries).
|
|
56
|
+
* Emits logger.warn on each retry. Returns the result on success, or null after
|
|
57
|
+
* exhausting all attempts (so the caller can escalate to the next tier).
|
|
58
|
+
*/
|
|
59
|
+
private withRetry;
|
|
60
|
+
/**
|
|
61
|
+
* Delays execution for `ms` milliseconds.
|
|
62
|
+
* Defined as a protected method so tests can spy on it via vi.spyOn(engine, 'sleep').
|
|
63
|
+
*/
|
|
64
|
+
protected sleep(ms: number): Promise<void>;
|
|
65
|
+
private callOpenRouter;
|
|
66
|
+
private callOpenRouterStream;
|
|
67
|
+
private callCloudflareAI;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=AIEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AIEngine.d.ts","sourceRoot":"","sources":["../../../src/core/ai/AIEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,mBAAmB,GAAG,qBAAqB,GAAG,eAAe,CAAC;IACxE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,mBAAmB,CAAM;IACjC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAS;gBAGxB,qBAAqB,EAAE,MAAM,EAC7B,mBAAmB,EAAE,GAAG,EACxB,OAAO,GAAE,eAAoB;IAQ/B;;;;OAIG;IACG,OAAO,CAAC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC;IAmClF;;;;;;;OAOG;IACG,aAAa,CACjB,OAAO,EAAE,SAAS,EAClB,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;IA0CtC;;;;OAIG;YACW,SAAS;IAuCvB;;;OAGG;IACH,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAI5B,cAAc;YAgCd,oBAAoB;YA+BpB,gBAAgB;CAgB/B"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORE-5: AI/BYOK Abstraction Engine
|
|
3
|
+
* Blueprint Reference: Part 9.1 #7 (Vendor Neutral AI)
|
|
4
|
+
*
|
|
5
|
+
* Implements a three-tier fallback mechanism with per-tier retry and exponential backoff:
|
|
6
|
+
* 1. Tenant BYOK (Bring Your Own Key) via OpenRouter
|
|
7
|
+
* 2. Platform Key via OpenRouter
|
|
8
|
+
* 3. Cloudflare Workers AI (Ultimate Fallback)
|
|
9
|
+
*
|
|
10
|
+
* Each tier is retried up to `maxRetries` times (default 2) before escalating.
|
|
11
|
+
* Between attempts the engine sleeps for backoffMs * 2^attempt milliseconds.
|
|
12
|
+
*/
|
|
13
|
+
import { logger } from '../logger';
|
|
14
|
+
export class AIEngine {
|
|
15
|
+
platformOpenRouterKey;
|
|
16
|
+
cloudflareAiBinding; // Type would be Ai from @cloudflare/workers-types
|
|
17
|
+
maxRetries;
|
|
18
|
+
backoffMs;
|
|
19
|
+
constructor(platformOpenRouterKey, cloudflareAiBinding, options = {}) {
|
|
20
|
+
this.platformOpenRouterKey = platformOpenRouterKey;
|
|
21
|
+
this.cloudflareAiBinding = cloudflareAiBinding;
|
|
22
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
23
|
+
this.backoffMs = options.backoffMs ?? 200;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Executes an AI request using the three-tier fallback strategy.
|
|
27
|
+
* Each tier is retried up to `maxRetries` times with exponential backoff
|
|
28
|
+
* before escalating to the next tier.
|
|
29
|
+
*/
|
|
30
|
+
async execute(request, tenantConfig) {
|
|
31
|
+
// Tier 1: Tenant BYOK via OpenRouter
|
|
32
|
+
if (tenantConfig.openRouterKey) {
|
|
33
|
+
const tier1Result = await this.withRetry('Tier 1 (tenant BYOK)', request.tenantId, () => this.callOpenRouter(request.prompt, tenantConfig.openRouterKey, request.model ?? tenantConfig.preferredModel ?? 'openai/gpt-4o-mini', 'tenant-openrouter'));
|
|
34
|
+
if (tier1Result !== null)
|
|
35
|
+
return tier1Result;
|
|
36
|
+
}
|
|
37
|
+
// Tier 2: Platform Key via OpenRouter
|
|
38
|
+
if (this.platformOpenRouterKey) {
|
|
39
|
+
const tier2Result = await this.withRetry('Tier 2 (platform key)', request.tenantId, () => this.callOpenRouter(request.prompt, this.platformOpenRouterKey, request.model ?? 'openai/gpt-4o-mini', 'platform-openrouter'));
|
|
40
|
+
if (tier2Result !== null)
|
|
41
|
+
return tier2Result;
|
|
42
|
+
}
|
|
43
|
+
// Tier 3: Cloudflare Workers AI (Ultimate Fallback — no retry needed, CF AI is durable)
|
|
44
|
+
return await this.callCloudflareAI(request.prompt);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Executes an AI request and returns a ReadableStream<Uint8Array> for
|
|
48
|
+
* server-sent events streaming.
|
|
49
|
+
*
|
|
50
|
+
* - Tier 1 & 2: calls OpenRouter with `stream: true` and pipes `Response.body`.
|
|
51
|
+
* - Tier 3 (CF AI fallback): calls `execute()` and wraps the text in a
|
|
52
|
+
* single-chunk ReadableStream (CF AI does not support streaming natively).
|
|
53
|
+
*/
|
|
54
|
+
async executeStream(request, tenantConfig) {
|
|
55
|
+
// Tier 1: Tenant BYOK streaming
|
|
56
|
+
if (tenantConfig.openRouterKey) {
|
|
57
|
+
const tier1Result = await this.withRetry('Tier 1 stream (tenant BYOK)', request.tenantId, () => this.callOpenRouterStream(request.prompt, tenantConfig.openRouterKey, request.model ?? tenantConfig.preferredModel ?? 'openai/gpt-4o-mini'));
|
|
58
|
+
if (tier1Result !== null)
|
|
59
|
+
return tier1Result;
|
|
60
|
+
}
|
|
61
|
+
// Tier 2: Platform Key streaming
|
|
62
|
+
if (this.platformOpenRouterKey) {
|
|
63
|
+
const tier2Result = await this.withRetry('Tier 2 stream (platform key)', request.tenantId, () => this.callOpenRouterStream(request.prompt, this.platformOpenRouterKey, request.model ?? 'openai/gpt-4o-mini'));
|
|
64
|
+
if (tier2Result !== null)
|
|
65
|
+
return tier2Result;
|
|
66
|
+
}
|
|
67
|
+
// Tier 3: Cloudflare AI — wrap non-streaming execute() result in a single-chunk stream
|
|
68
|
+
// (CF AI does not support native streaming; this keeps fallback logic in one place)
|
|
69
|
+
const fallback = await this.execute(request, tenantConfig);
|
|
70
|
+
const encoder = new TextEncoder();
|
|
71
|
+
const chunk = encoder.encode(fallback.text);
|
|
72
|
+
return new ReadableStream({
|
|
73
|
+
start(controller) {
|
|
74
|
+
controller.enqueue(chunk);
|
|
75
|
+
controller.close();
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Runs `fn` up to `maxRetries + 1` times (1 initial attempt + maxRetries retries).
|
|
81
|
+
* Emits logger.warn on each retry. Returns the result on success, or null after
|
|
82
|
+
* exhausting all attempts (so the caller can escalate to the next tier).
|
|
83
|
+
*/
|
|
84
|
+
async withRetry(tierLabel, tenantId, fn) {
|
|
85
|
+
let lastError;
|
|
86
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
87
|
+
if (attempt > 0) {
|
|
88
|
+
const delayMs = this.backoffMs * Math.pow(2, attempt - 1);
|
|
89
|
+
logger.warn(`${tierLabel} retry attempt ${attempt} after ${delayMs}ms`, {
|
|
90
|
+
tenantId,
|
|
91
|
+
attempt,
|
|
92
|
+
delayMs,
|
|
93
|
+
});
|
|
94
|
+
await this.sleep(delayMs);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
return await fn();
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
lastError = error;
|
|
101
|
+
if (attempt < this.maxRetries) {
|
|
102
|
+
logger.warn(`${tierLabel} attempt ${attempt + 1} failed, will retry`, {
|
|
103
|
+
tenantId,
|
|
104
|
+
attempt: attempt + 1,
|
|
105
|
+
error,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
logger.warn(`${tierLabel} exhausted all ${this.maxRetries + 1} attempts, escalating`, {
|
|
111
|
+
tenantId,
|
|
112
|
+
error: lastError,
|
|
113
|
+
});
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Delays execution for `ms` milliseconds.
|
|
118
|
+
* Defined as a protected method so tests can spy on it via vi.spyOn(engine, 'sleep').
|
|
119
|
+
*/
|
|
120
|
+
sleep(ms) {
|
|
121
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
122
|
+
}
|
|
123
|
+
async callOpenRouter(prompt, apiKey, model, provider) {
|
|
124
|
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: {
|
|
127
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
'HTTP-Referer': 'https://webwaka.com',
|
|
130
|
+
'X-Title': 'WebWaka OS v4',
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
model,
|
|
134
|
+
messages: [{ role: 'user', content: prompt }],
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(`OpenRouter API error: ${response.statusText}`);
|
|
139
|
+
}
|
|
140
|
+
const data = await response.json();
|
|
141
|
+
const content = data.choices?.[0]?.message?.content;
|
|
142
|
+
if (typeof content !== 'string') {
|
|
143
|
+
throw new Error('OpenRouter returned an unexpected response structure (missing choices[0].message.content)');
|
|
144
|
+
}
|
|
145
|
+
return { text: content, provider, modelUsed: model };
|
|
146
|
+
}
|
|
147
|
+
async callOpenRouterStream(prompt, apiKey, model) {
|
|
148
|
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: {
|
|
151
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
'HTTP-Referer': 'https://webwaka.com',
|
|
154
|
+
'X-Title': 'WebWaka OS v4',
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
model,
|
|
158
|
+
messages: [{ role: 'user', content: prompt }],
|
|
159
|
+
stream: true,
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`OpenRouter streaming API error: ${response.statusText}`);
|
|
164
|
+
}
|
|
165
|
+
if (!response.body) {
|
|
166
|
+
throw new Error('OpenRouter returned no response body for streaming request');
|
|
167
|
+
}
|
|
168
|
+
return response.body;
|
|
169
|
+
}
|
|
170
|
+
async callCloudflareAI(prompt) {
|
|
171
|
+
if (!this.cloudflareAiBinding) {
|
|
172
|
+
throw new Error('Cloudflare AI binding not configured');
|
|
173
|
+
}
|
|
174
|
+
const model = '@cf/meta/llama-3-8b-instruct';
|
|
175
|
+
const response = await this.cloudflareAiBinding.run(model, {
|
|
176
|
+
messages: [{ role: 'user', content: prompt }],
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
text: response.response,
|
|
180
|
+
provider: 'cloudflare-ai',
|
|
181
|
+
modelUsed: model,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=AIEngine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AIEngine.js","sourceRoot":"","sources":["../../../src/core/ai/AIEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AA0BnC,MAAM,OAAO,QAAQ;IACX,qBAAqB,CAAS;IAC9B,mBAAmB,CAAM,CAAC,kDAAkD;IAC5E,UAAU,CAAS;IACnB,SAAS,CAAS;IAE1B,YACE,qBAA6B,EAC7B,mBAAwB,EACxB,UAA2B,EAAE;QAE7B,IAAI,CAAC,qBAAqB,GAAG,qBAAqB,CAAC;QACnD,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,GAAG,CAAC;IAC5C,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO,CAAC,OAAkB,EAAE,YAA0B;QAC1D,qCAAqC;QACrC,IAAI,YAAY,CAAC,aAAa,EAAE,CAAC;YAC/B,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,SAAS,CACtC,sBAAsB,EACtB,OAAO,CAAC,QAAQ,EAChB,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CACvB,OAAO,CAAC,MAAM,EACd,YAAY,CAAC,aAAc,EAC3B,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC,cAAc,IAAI,oBAAoB,EACpE,mBAAmB,CACpB,CACF,CAAC;YACF,IAAI,WAAW,KAAK,IAAI;gBAAE,OAAO,WAAW,CAAC;QAC/C,CAAC;QAED,sCAAsC;QACtC,IAAI,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC/B,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,SAAS,CACtC,uBAAuB,EACvB,OAAO,CAAC,QAAQ,EAChB,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CACvB,OAAO,CAAC,MAAM,EACd,IAAI,CAAC,qBAAqB,EAC1B,OAAO,CAAC,KAAK,IAAI,oBAAoB,EACrC,qBAAqB,CACtB,CACF,CAAC;YACF,IAAI,WAAW,KAAK,IAAI;gBAAE,OAAO,WAAW,CAAC;QAC/C,CAAC;QAED,wFAAwF;QACxF,OAAO,MAAM,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,aAAa,CACjB,OAAkB,EAClB,YAA0B;QAE1B,gCAAgC;QAChC,IAAI,YAAY,CAAC,aAAa,EAAE,CAAC;YAC/B,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,SAAS,CACtC,6BAA6B,EAC7B,OAAO,CAAC,QAAQ,EAChB,GAAG,EAAE,CAAC,IAAI,CAAC,oBAAoB,CAC7B,OAAO,CAAC,MAAM,EACd,YAAY,CAAC,aAAc,EAC3B,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC,cAAc,IAAI,oBAAoB,CACrE,CACF,CAAC;YACF,IAAI,WAAW,KAAK,IAAI;gBAAE,OAAO,WAAW,CAAC;QAC/C,CAAC;QAED,iCAAiC;QACjC,IAAI,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC/B,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,SAAS,CACtC,8BAA8B,EAC9B,OAAO,CAAC,QAAQ,EAChB,GAAG,EAAE,CAAC,IAAI,CAAC,oBAAoB,CAC7B,OAAO,CAAC,MAAM,EACd,IAAI,CAAC,qBAAqB,EAC1B,OAAO,CAAC,KAAK,IAAI,oBAAoB,CACtC,CACF,CAAC;YACF,IAAI,WAAW,KAAK,IAAI;gBAAE,OAAO,WAAW,CAAC;QAC/C,CAAC;QAED,uFAAuF;QACvF,oFAAoF;QACpF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAC3D,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC5C,OAAO,IAAI,cAAc,CAAa;YACpC,KAAK,CAAC,UAAU;gBACd,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC1B,UAAU,CAAC,KAAK,EAAE,CAAC;YACrB,CAAC;SACF,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,SAAS,CACrB,SAAiB,EACjB,QAAgB,EAChB,EAAoB;QAEpB,IAAI,SAAkB,CAAC;QAEvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;YAC5D,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBAChB,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;gBAC1D,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,kBAAkB,OAAO,UAAU,OAAO,IAAI,EAAE;oBACtE,QAAQ;oBACR,OAAO;oBACP,OAAO;iBACR,CAAC,CAAC;gBACH,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,CAAC;YAED,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,EAAE,CAAC;YACpB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,SAAS,GAAG,KAAK,CAAC;gBAClB,IAAI,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;oBAC9B,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,YAAY,OAAO,GAAG,CAAC,qBAAqB,EAAE;wBACpE,QAAQ;wBACR,OAAO,EAAE,OAAO,GAAG,CAAC;wBACpB,KAAK;qBACN,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,kBAAkB,IAAI,CAAC,UAAU,GAAG,CAAC,uBAAuB,EAAE;YACpF,QAAQ;YACR,KAAK,EAAE,SAAS;SACjB,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACO,KAAK,CAAC,EAAU;QACxB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IACzD,CAAC;IAEO,KAAK,CAAC,cAAc,CAC1B,MAAc,EACd,MAAc,EACd,KAAa,EACb,QAAqD;QAErD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,+CAA+C,EAAE;YAC5E,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,eAAe,EAAE,UAAU,MAAM,EAAE;gBACnC,cAAc,EAAE,kBAAkB;gBAClC,cAAc,EAAE,qBAAqB;gBACrC,SAAS,EAAE,eAAe;aAC3B;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK;gBACL,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;aAC9C,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;QAClE,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAS,CAAC;QAC1C,MAAM,OAAO,GAAuB,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC;QACxE,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,2FAA2F,CAAC,CAAC;QAC/G,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACvD,CAAC;IAEO,KAAK,CAAC,oBAAoB,CAChC,MAAc,EACd,MAAc,EACd,KAAa;QAEb,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,+CAA+C,EAAE;YAC5E,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,eAAe,EAAE,UAAU,MAAM,EAAE;gBACnC,cAAc,EAAE,kBAAkB;gBAClC,cAAc,EAAE,qBAAqB;gBACrC,SAAS,EAAE,eAAe;aAC3B;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK;gBACL,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;gBAC7C,MAAM,EAAE,IAAI;aACb,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,mCAAmC,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;QAC5E,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;QAChF,CAAC;QAED,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,MAAc;QAC3C,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAC1D,CAAC;QAED,MAAM,KAAK,GAAG,8BAA8B,CAAC;QAC7C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,KAAK,EAAE;YACzD,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;SAC9C,CAAC,CAAC;QAEH,OAAO;YACL,IAAI,EAAE,QAAQ,CAAC,QAAQ;YACvB,QAAQ,EAAE,eAAe;YACzB,SAAS,EAAE,KAAK;SACjB,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,183 @@
|
|
|
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 type { Context, MiddlewareHandler } from 'hono';
|
|
24
|
+
export interface JWTPayload {
|
|
25
|
+
/** Subject — user ID */
|
|
26
|
+
sub: string;
|
|
27
|
+
/** Tenant ID — ALWAYS sourced from here, never from headers */
|
|
28
|
+
tenantId: string;
|
|
29
|
+
/** User role */
|
|
30
|
+
role: string;
|
|
31
|
+
/** Granted permissions */
|
|
32
|
+
permissions: string[];
|
|
33
|
+
/** Issued-at (Unix seconds) */
|
|
34
|
+
iat: number;
|
|
35
|
+
/** Expiry (Unix seconds) */
|
|
36
|
+
exp: number;
|
|
37
|
+
/** User email */
|
|
38
|
+
email: string;
|
|
39
|
+
}
|
|
40
|
+
/** Canonical user context injected into every authenticated Hono request */
|
|
41
|
+
export interface AuthUser {
|
|
42
|
+
userId: string;
|
|
43
|
+
email: string;
|
|
44
|
+
role: string;
|
|
45
|
+
tenantId: string;
|
|
46
|
+
permissions: string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* User context for API key authenticated requests (B2B / third-party systems).
|
|
50
|
+
* Compatible with the 'user' context key set by jwtAuthMiddleware.
|
|
51
|
+
*/
|
|
52
|
+
export interface WakaUser {
|
|
53
|
+
id: string;
|
|
54
|
+
tenant_id: string;
|
|
55
|
+
role: string;
|
|
56
|
+
name: string;
|
|
57
|
+
phone: string;
|
|
58
|
+
operator_id: string;
|
|
59
|
+
}
|
|
60
|
+
export interface AuthEnv {
|
|
61
|
+
JWT_SECRET: string;
|
|
62
|
+
ENVIRONMENT?: string;
|
|
63
|
+
/** Optional: KV namespace for rate-limiting counters */
|
|
64
|
+
RATE_LIMIT_KV?: KVNamespace;
|
|
65
|
+
/** Optional: D1 database binding used by verifyApiKey */
|
|
66
|
+
DB?: D1Database;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Sign a new HS256 JWT using the Web Crypto API (Cloudflare Workers compatible).
|
|
70
|
+
* Returns a compact `header.payload.signature` string.
|
|
71
|
+
*/
|
|
72
|
+
export declare function signJWT(payload: Omit<JWTPayload, 'iat' | 'exp'>, secret: string, expiresInSeconds?: number): Promise<string>;
|
|
73
|
+
/**
|
|
74
|
+
* Verify an HS256 JWT and return its decoded payload.
|
|
75
|
+
* Returns null if the token is invalid, expired, or tampered with.
|
|
76
|
+
* Uses the Web Crypto API — safe for Cloudflare Workers edge runtime.
|
|
77
|
+
*/
|
|
78
|
+
export declare function verifyJWT(token: string, secret: string): Promise<JWTPayload | null>;
|
|
79
|
+
/**
|
|
80
|
+
* Verifies an API key by hashing it with SHA-256 and looking it up in the
|
|
81
|
+
* api_keys table. Returns a WakaUser on success or null if the key is invalid.
|
|
82
|
+
* Compatible with Cloudflare Workers (Web Crypto API only).
|
|
83
|
+
*/
|
|
84
|
+
export declare function verifyApiKey(rawKey: string, db: D1Database): Promise<WakaUser | null>;
|
|
85
|
+
export interface JwtAuthOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Routes that bypass authentication entirely.
|
|
88
|
+
* Each entry is matched as a prefix against the request path.
|
|
89
|
+
* Method defaults to '*' (any method).
|
|
90
|
+
*/
|
|
91
|
+
publicRoutes?: Array<{
|
|
92
|
+
method?: string;
|
|
93
|
+
path: string;
|
|
94
|
+
}>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Hono middleware that verifies the Bearer JWT, rejects invalid/expired tokens,
|
|
98
|
+
* and injects the canonical `AuthUser` into the Hono context as `c.get('user')`.
|
|
99
|
+
*
|
|
100
|
+
* Also sets `c.get('tenantId')` from the JWT payload — NEVER from headers.
|
|
101
|
+
*
|
|
102
|
+
* Usage:
|
|
103
|
+
* app.use('/api/*', jwtAuthMiddleware({ publicRoutes: [{ path: '/health' }] }));
|
|
104
|
+
*/
|
|
105
|
+
export declare function jwtAuthMiddleware(options?: JwtAuthOptions): MiddlewareHandler<{
|
|
106
|
+
Bindings: AuthEnv;
|
|
107
|
+
}>;
|
|
108
|
+
/**
|
|
109
|
+
* Hono middleware factory that enforces role-based access control.
|
|
110
|
+
* MUST be used AFTER jwtAuthMiddleware on the same route.
|
|
111
|
+
*
|
|
112
|
+
* Usage:
|
|
113
|
+
* app.post('/api/admin/tenants', requireRole(['SUPER_ADMIN', 'TENANT_ADMIN']), handler);
|
|
114
|
+
*/
|
|
115
|
+
export declare function requireRole(allowedRoles: string[]): MiddlewareHandler;
|
|
116
|
+
/**
|
|
117
|
+
* Hono middleware factory that enforces permission-based access control.
|
|
118
|
+
* SUPER_ADMIN role bypasses all permission checks.
|
|
119
|
+
* MUST be used AFTER jwtAuthMiddleware.
|
|
120
|
+
*
|
|
121
|
+
* Usage:
|
|
122
|
+
* app.delete('/api/tenants/:id', requirePermissions(['delete:tenants']), handler);
|
|
123
|
+
*/
|
|
124
|
+
export declare function requirePermissions(requiredPermissions: string[]): MiddlewareHandler;
|
|
125
|
+
export interface SecureCORSOptions {
|
|
126
|
+
/**
|
|
127
|
+
* Allowed origins for production. Defaults to WebWaka production domains.
|
|
128
|
+
* In non-production environments, all origins are allowed.
|
|
129
|
+
*/
|
|
130
|
+
allowedOrigins?: string[];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Environment-aware CORS middleware.
|
|
134
|
+
* - Production: restricts to an explicit allowlist of WebWaka domains.
|
|
135
|
+
* - Non-production (staging/dev/local): allows all origins for developer convenience.
|
|
136
|
+
*
|
|
137
|
+
* NEVER uses `origin: '*'` in production.
|
|
138
|
+
*
|
|
139
|
+
* Usage:
|
|
140
|
+
* app.use('*', secureCORS());
|
|
141
|
+
* app.use('*', secureCORS({ allowedOrigins: ['https://app.mywebwaka.com'] }));
|
|
142
|
+
*/
|
|
143
|
+
export declare function secureCORS(options?: SecureCORSOptions): MiddlewareHandler<{
|
|
144
|
+
Bindings: AuthEnv;
|
|
145
|
+
}>;
|
|
146
|
+
export interface RateLimitOptions {
|
|
147
|
+
/** Maximum requests allowed per window. Default: 60 */
|
|
148
|
+
limit?: number;
|
|
149
|
+
/** Window duration in seconds. Default: 60 */
|
|
150
|
+
windowSeconds?: number;
|
|
151
|
+
/** Key prefix to namespace different rate limit buckets. Default: 'rl' */
|
|
152
|
+
keyPrefix?: string;
|
|
153
|
+
/** Custom key extractor. Default: uses CF-Connecting-IP header or 'unknown' */
|
|
154
|
+
keyExtractor?: (c: Context) => string;
|
|
155
|
+
}
|
|
156
|
+
export interface RateLimitEnv extends AuthEnv {
|
|
157
|
+
RATE_LIMIT_KV: KVNamespace;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* KV-backed sliding-window rate limiter for Cloudflare Workers.
|
|
161
|
+
* Requires a `RATE_LIMIT_KV` KV namespace binding.
|
|
162
|
+
*
|
|
163
|
+
* Usage (auth endpoints — strict):
|
|
164
|
+
* app.post('/auth/login', rateLimit({ limit: 10, windowSeconds: 60, keyPrefix: 'login' }), handler);
|
|
165
|
+
*
|
|
166
|
+
* Usage (general API):
|
|
167
|
+
* app.use('/api/*', rateLimit({ limit: 300, windowSeconds: 60 }));
|
|
168
|
+
*/
|
|
169
|
+
export declare function rateLimit(options?: RateLimitOptions): MiddlewareHandler<{
|
|
170
|
+
Bindings: RateLimitEnv;
|
|
171
|
+
}>;
|
|
172
|
+
/**
|
|
173
|
+
* Safely extract tenantId from the Hono context.
|
|
174
|
+
* Throws if tenantId is not present (i.e., jwtAuthMiddleware was not applied).
|
|
175
|
+
* Use this in route handlers to enforce that tenant context is always present.
|
|
176
|
+
*/
|
|
177
|
+
export declare function getTenantId(c: Context): string;
|
|
178
|
+
/**
|
|
179
|
+
* Safely extract the authenticated user from the Hono context.
|
|
180
|
+
* Throws if user is not present.
|
|
181
|
+
*/
|
|
182
|
+
export declare function getAuthUser(c: Context): AuthUser;
|
|
183
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/auth/index.ts"],"names":[],"mappings":"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"}
|