@littlebearapps/platform-consumer-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +306 -0
- package/package.json +53 -0
- package/src/ai-gateway.ts +305 -0
- package/src/constants.ts +147 -0
- package/src/costs.ts +590 -0
- package/src/do-heartbeat.ts +249 -0
- package/src/dynamic-patterns.ts +273 -0
- package/src/errors.ts +285 -0
- package/src/features.ts +149 -0
- package/src/heartbeat.ts +27 -0
- package/src/index.ts +950 -0
- package/src/logging.ts +543 -0
- package/src/middleware.ts +447 -0
- package/src/patterns.ts +156 -0
- package/src/proxy.ts +732 -0
- package/src/retry.ts +19 -0
- package/src/service-client.ts +291 -0
- package/src/telemetry.ts +342 -0
- package/src/timeout.ts +212 -0
- package/src/tracing.ts +403 -0
- package/src/types.ts +465 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Platform SDK - Durable Object Heartbeat Mixin
|
|
5
|
+
*
|
|
6
|
+
* Provides alarm-based health monitoring for Durable Objects.
|
|
7
|
+
* Uses the Alarms API to send periodic heartbeats to the platform telemetry queue.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { withHeartbeat } from '../lib/platform-sdk';
|
|
12
|
+
*
|
|
13
|
+
* export class MyDurableObject extends withHeartbeat(DurableObject, {
|
|
14
|
+
* featureKey: 'scout:do:triage-workflow',
|
|
15
|
+
* intervalMs: 5 * 60 * 1000, // 5 minutes
|
|
16
|
+
* }) {
|
|
17
|
+
* // Existing implementation unchanged
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { TelemetryMessage } from './types';
|
|
23
|
+
import { createLogger, type Logger } from './logging';
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// MODULE LOGGER (lazy-initialized to avoid global scope crypto calls)
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
let _log: Logger | null = null;
|
|
30
|
+
function getLog(): Logger {
|
|
31
|
+
if (!_log) {
|
|
32
|
+
_log = createLogger({
|
|
33
|
+
worker: 'platform-sdk',
|
|
34
|
+
featureId: 'platform:sdk:do-heartbeat',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return _log;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// TYPES
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Configuration for the heartbeat mixin.
|
|
46
|
+
*/
|
|
47
|
+
export interface HeartbeatConfig {
|
|
48
|
+
/** Feature key in format 'project:category:feature' */
|
|
49
|
+
featureKey: string;
|
|
50
|
+
/** Heartbeat interval in milliseconds. Default: 5 minutes */
|
|
51
|
+
intervalMs?: number;
|
|
52
|
+
/** Whether heartbeats are enabled. Default: true */
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Required environment bindings for heartbeat functionality.
|
|
58
|
+
*/
|
|
59
|
+
export interface HeartbeatEnv {
|
|
60
|
+
/** Queue for telemetry messages */
|
|
61
|
+
PLATFORM_TELEMETRY: Queue<TelemetryMessage>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Base class type for Durable Objects.
|
|
66
|
+
* Uses rest parameters for TypeScript mixin compatibility.
|
|
67
|
+
*/
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
export type DOClass = new (...args: any[]) => DurableObject;
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// CONSTANTS
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
/** Default heartbeat interval: 5 minutes */
|
|
76
|
+
const DEFAULT_INTERVAL_MS = 5 * 60 * 1000;
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// MIXIN
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a feature key into its component parts.
|
|
84
|
+
*/
|
|
85
|
+
function parseFeatureKey(featureKey: string): {
|
|
86
|
+
project: string;
|
|
87
|
+
category: string;
|
|
88
|
+
feature: string;
|
|
89
|
+
} {
|
|
90
|
+
const parts = featureKey.split(':');
|
|
91
|
+
if (parts.length !== 3) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Invalid featureKey format: "${featureKey}". Expected "project:category:feature"`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
project: parts[0],
|
|
98
|
+
category: parts[1],
|
|
99
|
+
feature: parts[2],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Mixin that adds heartbeat functionality to a Durable Object.
|
|
105
|
+
*
|
|
106
|
+
* Uses the Cloudflare Alarms API to schedule periodic heartbeats.
|
|
107
|
+
* Heartbeats are sent to the PLATFORM_TELEMETRY queue with `is_heartbeat: true`.
|
|
108
|
+
*
|
|
109
|
+
* The mixin:
|
|
110
|
+
* 1. Schedules the first heartbeat on construction
|
|
111
|
+
* 2. Sends a heartbeat message when the alarm fires
|
|
112
|
+
* 3. Reschedules the next heartbeat
|
|
113
|
+
* 4. Calls the parent's alarm() method if it exists
|
|
114
|
+
*
|
|
115
|
+
* @param Base - The base Durable Object class to extend
|
|
116
|
+
* @param config - Heartbeat configuration
|
|
117
|
+
* @returns Extended class with heartbeat functionality
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* export class ScoutTriageWorkflow extends withHeartbeat(DurableObject, {
|
|
122
|
+
* featureKey: 'scout:do:triage-workflow',
|
|
123
|
+
* intervalMs: 5 * 60 * 1000,
|
|
124
|
+
* }) {
|
|
125
|
+
* async fetch(request: Request): Promise<Response> {
|
|
126
|
+
* // Your existing implementation
|
|
127
|
+
* }
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function withHeartbeat<TBase extends DOClass>(Base: TBase, config: HeartbeatConfig): TBase {
|
|
132
|
+
const { featureKey, intervalMs = DEFAULT_INTERVAL_MS, enabled = true } = config;
|
|
133
|
+
|
|
134
|
+
// Validate feature key format early
|
|
135
|
+
const { project, category, feature } = parseFeatureKey(featureKey);
|
|
136
|
+
|
|
137
|
+
return class extends Base {
|
|
138
|
+
// Store parsed config for use in methods
|
|
139
|
+
protected readonly _heartbeatConfig = {
|
|
140
|
+
featureKey,
|
|
141
|
+
project,
|
|
142
|
+
category,
|
|
143
|
+
feature,
|
|
144
|
+
intervalMs,
|
|
145
|
+
enabled,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
constructor(...args: any[]) {
|
|
150
|
+
super(...args);
|
|
151
|
+
|
|
152
|
+
// Schedule first heartbeat if enabled
|
|
153
|
+
// args[0] is the DurableObjectState
|
|
154
|
+
if (this._heartbeatConfig.enabled) {
|
|
155
|
+
void this._scheduleNextHeartbeat(args[0] as DurableObjectState);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handle alarm events.
|
|
161
|
+
* Sends heartbeat, reschedules next alarm, and calls parent alarm if present.
|
|
162
|
+
*/
|
|
163
|
+
async alarm(): Promise<void> {
|
|
164
|
+
const state = (this as unknown as { state: DurableObjectState }).state;
|
|
165
|
+
const env = (this as unknown as { env: HeartbeatEnv }).env;
|
|
166
|
+
|
|
167
|
+
if (this._heartbeatConfig.enabled) {
|
|
168
|
+
// Send heartbeat to telemetry queue
|
|
169
|
+
await this._sendHeartbeat(env);
|
|
170
|
+
|
|
171
|
+
// Schedule next heartbeat
|
|
172
|
+
await this._scheduleNextHeartbeat(state);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Call parent alarm() if it exists
|
|
176
|
+
// Note: In TypeScript, we need to check if the parent class has an alarm method
|
|
177
|
+
const parentAlarm = Object.getPrototypeOf(Object.getPrototypeOf(this)).alarm;
|
|
178
|
+
if (typeof parentAlarm === 'function' && parentAlarm !== this.alarm) {
|
|
179
|
+
await parentAlarm.call(this);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Schedule the next heartbeat alarm.
|
|
185
|
+
*/
|
|
186
|
+
protected async _scheduleNextHeartbeat(state: DurableObjectState): Promise<void> {
|
|
187
|
+
try {
|
|
188
|
+
const nextAlarmTime = Date.now() + this._heartbeatConfig.intervalMs;
|
|
189
|
+
await state.storage.setAlarm(nextAlarmTime);
|
|
190
|
+
getLog().debug('Heartbeat scheduled', {
|
|
191
|
+
featureKey: this._heartbeatConfig.featureKey,
|
|
192
|
+
scheduledAt: new Date(nextAlarmTime).toISOString(),
|
|
193
|
+
});
|
|
194
|
+
} catch (error) {
|
|
195
|
+
getLog().error('Failed to schedule heartbeat', error, {
|
|
196
|
+
featureKey: this._heartbeatConfig.featureKey,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Send a heartbeat message to the telemetry queue.
|
|
203
|
+
*/
|
|
204
|
+
protected async _sendHeartbeat(env: HeartbeatEnv): Promise<void> {
|
|
205
|
+
if (!env.PLATFORM_TELEMETRY) {
|
|
206
|
+
getLog().warn('No PLATFORM_TELEMETRY queue binding, heartbeat not sent', undefined, {
|
|
207
|
+
featureKey: this._heartbeatConfig.featureKey,
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const heartbeatMessage: TelemetryMessage = {
|
|
213
|
+
feature_key: this._heartbeatConfig.featureKey,
|
|
214
|
+
project: this._heartbeatConfig.project,
|
|
215
|
+
category: this._heartbeatConfig.category,
|
|
216
|
+
feature: this._heartbeatConfig.feature,
|
|
217
|
+
metrics: {},
|
|
218
|
+
timestamp: Date.now(),
|
|
219
|
+
is_heartbeat: true,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await env.PLATFORM_TELEMETRY.send(heartbeatMessage);
|
|
224
|
+
getLog().debug('Heartbeat sent', { featureKey: this._heartbeatConfig.featureKey });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
// Fail open - log error but don't throw
|
|
227
|
+
getLog().error('Failed to send heartbeat', error, {
|
|
228
|
+
featureKey: this._heartbeatConfig.featureKey,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Manually trigger a heartbeat (for testing or on-demand health checks).
|
|
235
|
+
*/
|
|
236
|
+
async sendHeartbeatNow(): Promise<void> {
|
|
237
|
+
const env = (this as unknown as { env: HeartbeatEnv }).env;
|
|
238
|
+
await this._sendHeartbeat(env);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Manually reschedule the next heartbeat (for testing or recovery).
|
|
243
|
+
*/
|
|
244
|
+
async rescheduleHeartbeat(): Promise<void> {
|
|
245
|
+
const state = (this as unknown as { state: DurableObjectState }).state;
|
|
246
|
+
await this._scheduleNextHeartbeat(state);
|
|
247
|
+
}
|
|
248
|
+
} as TBase;
|
|
249
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dynamic Patterns
|
|
5
|
+
*
|
|
6
|
+
* AI-discovered transient error patterns loaded from KV at runtime.
|
|
7
|
+
* Separate from static patterns (patterns.ts) because these require KV I/O.
|
|
8
|
+
*
|
|
9
|
+
* Pattern lifecycle: pending -> shadow -> ready for review -> approved -> KV cache -> runtime
|
|
10
|
+
*
|
|
11
|
+
* Supports a constrained DSL with 4 types: contains, startsWith, statusCode, regex.
|
|
12
|
+
* Regex patterns have a 200-char safety limit to prevent ReDoS.
|
|
13
|
+
*
|
|
14
|
+
* Multi-account utilities (exportDynamicPatterns/importDynamicPatterns) enable
|
|
15
|
+
* cross-account pattern sync via sync-config or platform-agent.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { loadDynamicPatterns, classifyWithDynamicPatterns } from '@littlebearapps/platform-consumer-sdk/dynamic-patterns';
|
|
20
|
+
*
|
|
21
|
+
* const patterns = await loadDynamicPatterns(env.PLATFORM_CACHE);
|
|
22
|
+
* const result = classifyWithDynamicPatterns('Custom error message', patterns);
|
|
23
|
+
* if (result) {
|
|
24
|
+
* console.log(`Dynamic match: ${result.category} (${result.patternId})`);
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// TYPES
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
/** Dynamic pattern rule from pattern-discovery system */
|
|
34
|
+
export interface DynamicPatternRule {
|
|
35
|
+
type: 'contains' | 'startsWith' | 'statusCode' | 'regex';
|
|
36
|
+
value: string;
|
|
37
|
+
category: string;
|
|
38
|
+
scope: string;
|
|
39
|
+
id?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Compiled pattern ready for classification */
|
|
43
|
+
export interface CompiledPattern {
|
|
44
|
+
test: (message: string) => boolean;
|
|
45
|
+
category: string;
|
|
46
|
+
id?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Classification result from dynamic pattern matching */
|
|
50
|
+
export interface ClassificationResult {
|
|
51
|
+
category: string;
|
|
52
|
+
source: 'dynamic';
|
|
53
|
+
patternId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// CONSTANTS
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
/** KV key for approved dynamic patterns — useful for sync-config and multi-account */
|
|
61
|
+
export const DYNAMIC_PATTERNS_KV_KEY = 'PATTERNS:DYNAMIC:APPROVED';
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// COMPILATION
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compile a single dynamic pattern rule to a test function.
|
|
69
|
+
* Uses the constrained DSL to ensure safety (no arbitrary regex execution).
|
|
70
|
+
*
|
|
71
|
+
* @returns Compiled pattern or null if compilation fails
|
|
72
|
+
*/
|
|
73
|
+
function compileDynamicPattern(rule: DynamicPatternRule): CompiledPattern | null {
|
|
74
|
+
try {
|
|
75
|
+
switch (rule.type) {
|
|
76
|
+
case 'contains': {
|
|
77
|
+
const tokens = rule.value.toLowerCase().split(/\s+/);
|
|
78
|
+
return {
|
|
79
|
+
test: (msg: string) => {
|
|
80
|
+
const lowerMsg = msg.toLowerCase();
|
|
81
|
+
return tokens.every((token) => lowerMsg.includes(token));
|
|
82
|
+
},
|
|
83
|
+
category: rule.category,
|
|
84
|
+
id: rule.id,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
case 'startsWith': {
|
|
88
|
+
const prefix = rule.value.toLowerCase();
|
|
89
|
+
return {
|
|
90
|
+
test: (msg: string) => msg.toLowerCase().startsWith(prefix),
|
|
91
|
+
category: rule.category,
|
|
92
|
+
id: rule.id,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
case 'statusCode': {
|
|
96
|
+
const code = rule.value;
|
|
97
|
+
const pattern = new RegExp(`\\b${code}\\b`);
|
|
98
|
+
return {
|
|
99
|
+
test: (msg: string) => pattern.test(msg),
|
|
100
|
+
category: rule.category,
|
|
101
|
+
id: rule.id,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
case 'regex': {
|
|
105
|
+
// Add safety limit to prevent ReDoS
|
|
106
|
+
if (rule.value.length > 200) {
|
|
107
|
+
console.warn(`Skipping overly long regex pattern: ${rule.category}`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const pattern = new RegExp(rule.value, 'i');
|
|
111
|
+
return {
|
|
112
|
+
test: (msg: string) => pattern.test(msg),
|
|
113
|
+
category: rule.category,
|
|
114
|
+
id: rule.id,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
default:
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.warn(`Failed to compile pattern for ${rule.category}:`, error);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compile an array of dynamic pattern rules into test functions.
|
|
128
|
+
* Invalid rules are silently dropped (logged via console.warn).
|
|
129
|
+
*
|
|
130
|
+
* @param rules - Array of dynamic pattern rules from D1/KV
|
|
131
|
+
* @returns Array of compiled patterns ready for classification
|
|
132
|
+
*/
|
|
133
|
+
export function compileDynamicPatterns(rules: DynamicPatternRule[]): CompiledPattern[] {
|
|
134
|
+
const compiled: CompiledPattern[] = [];
|
|
135
|
+
for (const rule of rules) {
|
|
136
|
+
const pattern = compileDynamicPattern(rule);
|
|
137
|
+
if (pattern) {
|
|
138
|
+
compiled.push(pattern);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return compiled;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// IN-MEMORY CACHE
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
let dynamicPatternsCache: CompiledPattern[] | null = null;
|
|
149
|
+
let dynamicPatternsCacheTime = 0;
|
|
150
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
151
|
+
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// LOADING
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Load dynamic patterns from KV and compile them.
|
|
158
|
+
* Uses in-memory cache (5-minute TTL) to avoid repeated KV reads within worker lifetime.
|
|
159
|
+
*
|
|
160
|
+
* @param kv - KV namespace containing approved patterns
|
|
161
|
+
* @returns Compiled patterns ready for classification
|
|
162
|
+
*/
|
|
163
|
+
export async function loadDynamicPatterns(kv: KVNamespace): Promise<CompiledPattern[]> {
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
if (dynamicPatternsCache && now - dynamicPatternsCacheTime < CACHE_TTL_MS) {
|
|
166
|
+
return dynamicPatternsCache;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const cached = await kv.get(DYNAMIC_PATTERNS_KV_KEY);
|
|
171
|
+
if (!cached) {
|
|
172
|
+
dynamicPatternsCache = [];
|
|
173
|
+
dynamicPatternsCacheTime = now;
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const rules = JSON.parse(cached) as DynamicPatternRule[];
|
|
178
|
+
const compiled = compileDynamicPatterns(rules);
|
|
179
|
+
|
|
180
|
+
dynamicPatternsCache = compiled;
|
|
181
|
+
dynamicPatternsCacheTime = now;
|
|
182
|
+
return compiled;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('Failed to load dynamic patterns:', error);
|
|
185
|
+
return dynamicPatternsCache || [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Clear the in-memory dynamic patterns cache.
|
|
191
|
+
* Call after pattern updates or for testing.
|
|
192
|
+
*/
|
|
193
|
+
export function clearDynamicPatternsCache(): void {
|
|
194
|
+
dynamicPatternsCache = null;
|
|
195
|
+
dynamicPatternsCacheTime = 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// =============================================================================
|
|
199
|
+
// CLASSIFICATION
|
|
200
|
+
// =============================================================================
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Classify an error message using dynamic patterns only.
|
|
204
|
+
* For combined static + dynamic classification, use classifyErrorWithSource() in fingerprint.ts.
|
|
205
|
+
*
|
|
206
|
+
* @param message - Error message to classify
|
|
207
|
+
* @param patterns - Pre-loaded compiled dynamic patterns
|
|
208
|
+
* @returns Classification result or null if no match
|
|
209
|
+
*/
|
|
210
|
+
export function classifyWithDynamicPatterns(
|
|
211
|
+
message: string,
|
|
212
|
+
patterns: CompiledPattern[]
|
|
213
|
+
): ClassificationResult | null {
|
|
214
|
+
for (const compiled of patterns) {
|
|
215
|
+
if (compiled.test(message)) {
|
|
216
|
+
return {
|
|
217
|
+
category: compiled.category,
|
|
218
|
+
source: 'dynamic',
|
|
219
|
+
patternId: compiled.id,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// =============================================================================
|
|
227
|
+
// MULTI-ACCOUNT UTILITIES
|
|
228
|
+
// =============================================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Export raw dynamic patterns from KV for cross-account transfer.
|
|
232
|
+
* Returns the JSON string as-is (no compilation) for writing to another KV.
|
|
233
|
+
*
|
|
234
|
+
* @param kv - Source KV namespace with approved patterns
|
|
235
|
+
* @returns Raw JSON string or null if no patterns are cached
|
|
236
|
+
*/
|
|
237
|
+
export async function exportDynamicPatterns(kv: KVNamespace): Promise<string | null> {
|
|
238
|
+
return await kv.get(DYNAMIC_PATTERNS_KV_KEY);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Import dynamic patterns into a KV namespace.
|
|
243
|
+
* Validates structure AND compiles each rule as a safety gate:
|
|
244
|
+
* - Checks required fields (type, value, category)
|
|
245
|
+
* - Compiles to verify regex validity + applies 200-char limit for regex type
|
|
246
|
+
* - Silently drops rules that fail compilation (matches loadDynamicPatterns behaviour)
|
|
247
|
+
*
|
|
248
|
+
* TTL matches pattern-discovery's 7-day safety net.
|
|
249
|
+
*
|
|
250
|
+
* @param kv - Target KV namespace
|
|
251
|
+
* @param patternsJson - Raw JSON string from exportDynamicPatterns()
|
|
252
|
+
* @returns Count of imported and dropped rules
|
|
253
|
+
*/
|
|
254
|
+
export async function importDynamicPatterns(
|
|
255
|
+
kv: KVNamespace,
|
|
256
|
+
patternsJson: string
|
|
257
|
+
): Promise<{ imported: number; dropped: number }> {
|
|
258
|
+
const rules = JSON.parse(patternsJson) as DynamicPatternRule[];
|
|
259
|
+
|
|
260
|
+
// Structural validation
|
|
261
|
+
const validTypes = new Set(['contains', 'startsWith', 'statusCode', 'regex']);
|
|
262
|
+
const structurallyValid = rules.filter(
|
|
263
|
+
(r) => r.type && r.value && r.category && validTypes.has(r.type)
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Compilation gate: verify each rule compiles (catches invalid regex, ReDoS-length limit)
|
|
267
|
+
const compilable = structurallyValid.filter((r) => compileDynamicPatterns([r]).length > 0);
|
|
268
|
+
|
|
269
|
+
await kv.put(DYNAMIC_PATTERNS_KV_KEY, JSON.stringify(compilable), { expirationTtl: 604800 });
|
|
270
|
+
clearDynamicPatternsCache();
|
|
271
|
+
|
|
272
|
+
return { imported: compilable.length, dropped: rules.length - compilable.length };
|
|
273
|
+
}
|