@almadar/integrations 2.22.0 → 2.24.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/dist/{contracts-CYtxZXrR.d.ts → contracts-Dv9PM_Cz.d.ts} +26 -1
- package/dist/index.d.ts +25 -3
- package/dist/index.js +108 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime/index.d.ts +12 -1
- package/dist/runtime/index.js +3555 -183
- package/dist/runtime/index.js.map +1 -1
- package/package.json +3 -3
package/dist/runtime/index.js
CHANGED
|
@@ -1,11 +1,217 @@
|
|
|
1
|
-
import { createRequire } from 'module';
|
|
2
1
|
import { createLogger } from '@almadar/logger';
|
|
3
2
|
import { integratorsRegistry } from '@almadar/core/patterns';
|
|
3
|
+
import Stripe2 from 'stripe';
|
|
4
|
+
import { google } from 'googleapis';
|
|
5
|
+
import twilio from 'twilio';
|
|
6
|
+
import sgMail from '@sendgrid/mail';
|
|
7
|
+
import { Resend } from 'resend';
|
|
8
|
+
import { createHmac } from 'crypto';
|
|
9
|
+
import { getAvailableProvider, LLMClient, EmbeddingClient } from '@almadar/llm';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { execSync, spawn } from 'child_process';
|
|
12
|
+
import { mkdtempSync, writeFileSync, rmSync, promises } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { tmpdir } from 'os';
|
|
15
|
+
import { Pool } from 'pg';
|
|
16
|
+
import { createRequire } from 'module';
|
|
4
17
|
|
|
5
|
-
// src/
|
|
18
|
+
// src/types.ts
|
|
19
|
+
var IntegrationError = class extends Error {
|
|
20
|
+
constructor(message, code = "UNKNOWN_ERROR", details) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "IntegrationError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.details = details;
|
|
25
|
+
}
|
|
26
|
+
toJSON() {
|
|
27
|
+
return {
|
|
28
|
+
name: this.name,
|
|
29
|
+
message: this.message,
|
|
30
|
+
code: this.code,
|
|
31
|
+
integration: this.integration,
|
|
32
|
+
action: this.action,
|
|
33
|
+
details: this.details
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var ConsoleLogger = class {
|
|
38
|
+
constructor(_level = "info") {
|
|
39
|
+
this.log = createLogger("almadar:integrations");
|
|
40
|
+
}
|
|
41
|
+
debug(message, meta) {
|
|
42
|
+
this.log.debug(message, meta);
|
|
43
|
+
}
|
|
44
|
+
info(message, meta) {
|
|
45
|
+
this.log.info(message, meta);
|
|
46
|
+
}
|
|
47
|
+
warn(message, meta) {
|
|
48
|
+
this.log.warn(message, meta);
|
|
49
|
+
}
|
|
50
|
+
error(message, meta) {
|
|
51
|
+
this.log.error(message, meta);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function validateParams(integration, action, params) {
|
|
55
|
+
const typedRegistry = integratorsRegistry;
|
|
56
|
+
const registry = typedRegistry.integrators[integration];
|
|
57
|
+
if (!registry) {
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
errors: [
|
|
61
|
+
{
|
|
62
|
+
param: "integration",
|
|
63
|
+
message: `Unknown integration: ${integration}`
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const actionDef = registry.actions.find((a) => a.name === action);
|
|
69
|
+
if (!actionDef) {
|
|
70
|
+
return {
|
|
71
|
+
valid: false,
|
|
72
|
+
errors: [{ param: "action", message: `Unknown action: ${action}` }]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const errors = [];
|
|
76
|
+
for (const paramDef of actionDef.params) {
|
|
77
|
+
if (paramDef.required && !(paramDef.name in params)) {
|
|
78
|
+
errors.push({
|
|
79
|
+
param: paramDef.name,
|
|
80
|
+
message: `Missing required parameter: ${paramDef.name}`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (paramDef.name in params) {
|
|
84
|
+
const value = params[paramDef.name];
|
|
85
|
+
const expectedType = paramDef.type;
|
|
86
|
+
const actualType = typeof value;
|
|
87
|
+
if (expectedType === "number" && actualType !== "number") {
|
|
88
|
+
errors.push({
|
|
89
|
+
param: paramDef.name,
|
|
90
|
+
message: `Expected ${expectedType}, got ${actualType}`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (expectedType === "string" && actualType !== "string") {
|
|
94
|
+
errors.push({
|
|
95
|
+
param: paramDef.name,
|
|
96
|
+
message: `Expected ${expectedType}, got ${actualType}`
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (expectedType === "array" && !Array.isArray(value)) {
|
|
100
|
+
errors.push({
|
|
101
|
+
param: paramDef.name,
|
|
102
|
+
message: `Expected array, got ${actualType}`
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (expectedType === "object" && (actualType !== "object" || Array.isArray(value) || value === null)) {
|
|
106
|
+
errors.push({
|
|
107
|
+
param: paramDef.name,
|
|
108
|
+
message: `Expected object, got ${actualType}`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
valid: errors.length === 0,
|
|
115
|
+
errors
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/core/retry.ts
|
|
120
|
+
async function withRetry(fn, config) {
|
|
121
|
+
const {
|
|
122
|
+
maxAttempts,
|
|
123
|
+
backoffMs,
|
|
124
|
+
maxBackoffMs = 3e4,
|
|
125
|
+
retryableErrors
|
|
126
|
+
} = config;
|
|
127
|
+
let lastError;
|
|
128
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
129
|
+
try {
|
|
130
|
+
return await fn();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
lastError = error;
|
|
133
|
+
if (error && typeof error === "object" && "code" in error && retryableErrors) {
|
|
134
|
+
const integrationError = error;
|
|
135
|
+
if (!retryableErrors.includes(integrationError.code)) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (attempt === maxAttempts) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
const delay = Math.min(
|
|
143
|
+
backoffMs * Math.pow(2, attempt - 1),
|
|
144
|
+
maxBackoffMs
|
|
145
|
+
);
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw lastError;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/core/BaseIntegration.ts
|
|
153
|
+
var BaseIntegration = class {
|
|
154
|
+
constructor(config) {
|
|
155
|
+
this.config = config;
|
|
156
|
+
this.logger = config.logger || new ConsoleLogger();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Validate action params against registry
|
|
160
|
+
*/
|
|
161
|
+
validateParams(action, params) {
|
|
162
|
+
return validateParams(this.config.name, action, params);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Handle errors uniformly
|
|
166
|
+
*/
|
|
167
|
+
handleError(action, error) {
|
|
168
|
+
this.logger.error(`Integration error in ${this.config.name}.${action}`, {
|
|
169
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
170
|
+
});
|
|
171
|
+
const integrationError = error instanceof Error ? error : new Error(String(error));
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: integrationError,
|
|
175
|
+
metadata: this.createMetadata(action, 0, 0)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Create metadata for result
|
|
180
|
+
*/
|
|
181
|
+
createMetadata(action, duration, retries = 0) {
|
|
182
|
+
return {
|
|
183
|
+
integration: this.config.name,
|
|
184
|
+
action,
|
|
185
|
+
duration,
|
|
186
|
+
retries,
|
|
187
|
+
timestamp: Date.now()
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Execute with retry logic
|
|
192
|
+
*/
|
|
193
|
+
async executeWithRetry(fn) {
|
|
194
|
+
if (!this.config.retry) {
|
|
195
|
+
return fn();
|
|
196
|
+
}
|
|
197
|
+
return withRetry(fn, {
|
|
198
|
+
maxAttempts: this.config.retry.maxAttempts,
|
|
199
|
+
backoffMs: this.config.retry.backoffMs,
|
|
200
|
+
maxBackoffMs: this.config.retry.maxBackoffMs,
|
|
201
|
+
retryableErrors: [
|
|
202
|
+
"TIMEOUT_ERROR",
|
|
203
|
+
"NETWORK_ERROR",
|
|
204
|
+
"RATE_LIMIT_ERROR"
|
|
205
|
+
]
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
};
|
|
6
209
|
|
|
7
210
|
// src/registry.ts
|
|
8
211
|
var INTEGRATION_REGISTRY = {};
|
|
212
|
+
function registerIntegration(name, constructor) {
|
|
213
|
+
INTEGRATION_REGISTRY[name] = constructor;
|
|
214
|
+
}
|
|
9
215
|
function getIntegration(name) {
|
|
10
216
|
return INTEGRATION_REGISTRY[name];
|
|
11
217
|
}
|
|
@@ -76,198 +282,3345 @@ var IntegrationFactory = class {
|
|
|
76
282
|
this.configs.clear();
|
|
77
283
|
}
|
|
78
284
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
285
|
+
|
|
286
|
+
// src/integrations/stripe/index.ts
|
|
287
|
+
var STRIPE_API_VERSION = "2025-02-24.acacia";
|
|
288
|
+
function isoFromUnix(seconds) {
|
|
289
|
+
return new Date(seconds * 1e3).toISOString();
|
|
290
|
+
}
|
|
291
|
+
function priceToTier(priceId, prices) {
|
|
292
|
+
if (priceId === prices.solo) return "solo";
|
|
293
|
+
if (priceId === prices.teams) return "teams";
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
function mapStatus(status) {
|
|
297
|
+
switch (status) {
|
|
298
|
+
case "active":
|
|
299
|
+
case "past_due":
|
|
300
|
+
case "canceled":
|
|
301
|
+
case "incomplete":
|
|
302
|
+
case "incomplete_expired":
|
|
303
|
+
case "trialing":
|
|
304
|
+
case "unpaid":
|
|
305
|
+
return status;
|
|
306
|
+
case "paused":
|
|
307
|
+
return "unpaid";
|
|
308
|
+
default: {
|
|
309
|
+
return "incomplete";
|
|
310
|
+
}
|
|
82
311
|
}
|
|
83
|
-
|
|
84
|
-
|
|
312
|
+
}
|
|
313
|
+
function shapeSubscription(sub, prices) {
|
|
314
|
+
const firstItem = sub.items.data[0];
|
|
315
|
+
const priceId = firstItem?.price.id ?? "";
|
|
316
|
+
const quantity = firstItem?.quantity ?? 1;
|
|
317
|
+
const customerId = typeof sub.customer === "string" ? sub.customer : sub.customer.id;
|
|
318
|
+
return {
|
|
319
|
+
subscriptionId: sub.id,
|
|
320
|
+
customerId,
|
|
321
|
+
status: mapStatus(sub.status),
|
|
322
|
+
priceId,
|
|
323
|
+
quantity,
|
|
324
|
+
currentPeriodStart: isoFromUnix(sub.current_period_start),
|
|
325
|
+
currentPeriodEnd: isoFromUnix(sub.current_period_end),
|
|
326
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end,
|
|
327
|
+
tier: priceToTier(priceId, prices)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function shapeCustomer(customer) {
|
|
331
|
+
return {
|
|
332
|
+
customerId: customer.id,
|
|
333
|
+
email: customer.email ?? null,
|
|
334
|
+
almadarUid: customer.metadata?.almadarUid ?? null
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
var StripeIntegration = class extends BaseIntegration {
|
|
338
|
+
constructor(config) {
|
|
339
|
+
super(config);
|
|
340
|
+
const apiKey = config.env.STRIPE_SECRET_KEY;
|
|
341
|
+
if (!apiKey) {
|
|
342
|
+
throw new Error("STRIPE_SECRET_KEY not configured");
|
|
343
|
+
}
|
|
344
|
+
this.client = new Stripe2(apiKey, {
|
|
345
|
+
apiVersion: STRIPE_API_VERSION
|
|
346
|
+
});
|
|
347
|
+
this.prices = {
|
|
348
|
+
solo: config.env.STRIPE_PRICE_SOLO ?? "",
|
|
349
|
+
teams: config.env.STRIPE_PRICE_TEAMS ?? ""
|
|
350
|
+
};
|
|
351
|
+
this.logger.info("Stripe integration initialized");
|
|
85
352
|
}
|
|
86
|
-
|
|
87
|
-
|
|
353
|
+
/** Provisioned Price IDs the integration was constructed with. */
|
|
354
|
+
getPrices() {
|
|
355
|
+
return this.prices;
|
|
88
356
|
}
|
|
89
|
-
|
|
90
|
-
this.
|
|
357
|
+
async execute(action, params) {
|
|
358
|
+
const validation = this.validateParams(action, params);
|
|
359
|
+
if (!validation.valid) {
|
|
360
|
+
return {
|
|
361
|
+
success: false,
|
|
362
|
+
error: {
|
|
363
|
+
name: "IntegrationError",
|
|
364
|
+
message: "Validation failed",
|
|
365
|
+
code: "VALIDATION_ERROR",
|
|
366
|
+
details: validation.errors
|
|
367
|
+
},
|
|
368
|
+
metadata: this.createMetadata(action, 0)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const startTime = Date.now();
|
|
372
|
+
const retries = 0;
|
|
373
|
+
try {
|
|
374
|
+
let data;
|
|
375
|
+
switch (action) {
|
|
376
|
+
case "createPaymentIntent":
|
|
377
|
+
data = await this.executeWithRetry(
|
|
378
|
+
() => this.createPaymentIntent(params)
|
|
379
|
+
);
|
|
380
|
+
break;
|
|
381
|
+
case "confirmPayment":
|
|
382
|
+
data = await this.executeWithRetry(() => this.confirmPayment(params));
|
|
383
|
+
break;
|
|
384
|
+
case "refund":
|
|
385
|
+
data = await this.executeWithRetry(() => this.refund(params));
|
|
386
|
+
break;
|
|
387
|
+
default:
|
|
388
|
+
throw new Error(`Unknown action: ${action}`);
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
success: true,
|
|
392
|
+
data,
|
|
393
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
394
|
+
};
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return this.handleError(action, error);
|
|
397
|
+
}
|
|
91
398
|
}
|
|
92
|
-
|
|
93
|
-
|
|
399
|
+
async createPaymentIntent(params) {
|
|
400
|
+
const { amount, currency, metadata } = params;
|
|
401
|
+
this.logger.debug("Creating payment intent", {
|
|
402
|
+
amount: Number(amount),
|
|
403
|
+
currency: String(currency ?? "")
|
|
404
|
+
});
|
|
405
|
+
return await this.client.paymentIntents.create({
|
|
406
|
+
amount,
|
|
407
|
+
currency,
|
|
408
|
+
metadata
|
|
409
|
+
});
|
|
94
410
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return
|
|
101
|
-
valid: false,
|
|
102
|
-
errors: [
|
|
103
|
-
{
|
|
104
|
-
param: "integration",
|
|
105
|
-
message: `Unknown integration: ${integration}`
|
|
106
|
-
}
|
|
107
|
-
]
|
|
108
|
-
};
|
|
411
|
+
async confirmPayment(params) {
|
|
412
|
+
const { paymentIntentId } = params;
|
|
413
|
+
this.logger.debug("Confirming payment", {
|
|
414
|
+
paymentIntentId: String(paymentIntentId ?? "")
|
|
415
|
+
});
|
|
416
|
+
return await this.client.paymentIntents.confirm(paymentIntentId);
|
|
109
417
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
418
|
+
async refund(params) {
|
|
419
|
+
const { paymentIntentId, amount } = params;
|
|
420
|
+
this.logger.debug("Creating refund", {
|
|
421
|
+
paymentIntentId: String(paymentIntentId ?? ""),
|
|
422
|
+
amount: Number(amount)
|
|
423
|
+
});
|
|
424
|
+
return await this.client.refunds.create({
|
|
425
|
+
payment_intent: paymentIntentId,
|
|
426
|
+
amount
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
// ───────────────────────────────────────────────────────────────────
|
|
430
|
+
// Typed action surface — canonical Almadar shapes only.
|
|
431
|
+
// ───────────────────────────────────────────────────────────────────
|
|
432
|
+
/** Look up an existing customer by Stripe ID. */
|
|
433
|
+
async getCustomer(customerId) {
|
|
434
|
+
this.logger.debug("Fetching customer", { customerId });
|
|
435
|
+
const customer = await this.client.customers.retrieve(customerId);
|
|
436
|
+
if (customer.deleted === true) return null;
|
|
437
|
+
return shapeCustomer(customer);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Create a Stripe Customer for the given Almadar user. `almadarUid` is
|
|
441
|
+
* stored as Stripe metadata so webhook handlers can resolve back to
|
|
442
|
+
* the right `users/{uid}` document.
|
|
443
|
+
*/
|
|
444
|
+
async createCustomer(input) {
|
|
445
|
+
this.logger.debug("Creating customer", { almadarUid: input.almadarUid });
|
|
446
|
+
const customer = await this.client.customers.create({
|
|
447
|
+
email: input.email,
|
|
448
|
+
name: input.displayName ?? void 0,
|
|
449
|
+
metadata: { almadarUid: input.almadarUid }
|
|
450
|
+
});
|
|
451
|
+
return shapeCustomer(customer);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Create a Stripe-hosted Checkout Session for the given tier. Client
|
|
455
|
+
* redirects the user to the returned `url`; on success Stripe fires
|
|
456
|
+
* `customer.subscription.created`, which the apps/builder webhook
|
|
457
|
+
* handler turns into a `users/{uid}.tier` write.
|
|
458
|
+
*/
|
|
459
|
+
async createCheckoutSession(input) {
|
|
460
|
+
const price = input.tier === "solo" ? this.prices.solo : this.prices.teams;
|
|
461
|
+
if (!price) {
|
|
462
|
+
throw new Error(`STRIPE_PRICE_${input.tier.toUpperCase()} not configured`);
|
|
463
|
+
}
|
|
464
|
+
this.logger.debug("Creating Checkout session", {
|
|
465
|
+
tier: input.tier,
|
|
466
|
+
quantity: input.quantity,
|
|
467
|
+
hasCustomer: input.customerId !== null
|
|
468
|
+
});
|
|
469
|
+
const session = await this.client.checkout.sessions.create({
|
|
470
|
+
mode: "subscription",
|
|
471
|
+
customer: input.customerId ?? void 0,
|
|
472
|
+
line_items: [{ price, quantity: input.quantity }],
|
|
473
|
+
success_url: input.successUrl,
|
|
474
|
+
cancel_url: input.cancelUrl,
|
|
475
|
+
subscription_data: {
|
|
476
|
+
metadata: input.metadata
|
|
477
|
+
},
|
|
478
|
+
automatic_tax: { enabled: true },
|
|
479
|
+
allow_promotion_codes: true
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
url: session.url ?? "",
|
|
483
|
+
sessionId: session.id,
|
|
484
|
+
customerId: typeof session.customer === "string" ? session.customer : session.customer?.id ?? null
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
/** Create a Billing Portal session for self-service plan management. */
|
|
488
|
+
async createBillingPortalSession(input) {
|
|
489
|
+
this.logger.debug("Creating Portal session", { customerId: input.customerId });
|
|
490
|
+
const session = await this.client.billingPortal.sessions.create({
|
|
491
|
+
customer: input.customerId,
|
|
492
|
+
return_url: input.returnUrl
|
|
493
|
+
});
|
|
494
|
+
return {
|
|
495
|
+
url: session.url,
|
|
496
|
+
customerId: input.customerId
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/** Fetch a subscription and shape it into the canonical form. */
|
|
500
|
+
async getSubscription(subscriptionId) {
|
|
501
|
+
this.logger.debug("Fetching subscription", { subscriptionId });
|
|
502
|
+
const sub = await this.client.subscriptions.retrieve(subscriptionId);
|
|
503
|
+
return shapeSubscription(sub, this.prices);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Create a subscription directly (server-side, no Checkout). Used by
|
|
507
|
+
* P13.3 Solo → Teams upgrade flow.
|
|
508
|
+
*/
|
|
509
|
+
async createSubscription(input) {
|
|
510
|
+
const price = input.tier === "solo" ? this.prices.solo : this.prices.teams;
|
|
511
|
+
if (!price) {
|
|
512
|
+
throw new Error(`STRIPE_PRICE_${input.tier.toUpperCase()} not configured`);
|
|
513
|
+
}
|
|
514
|
+
this.logger.debug("Creating subscription", {
|
|
515
|
+
customerId: input.customerId,
|
|
516
|
+
tier: input.tier,
|
|
517
|
+
quantity: input.quantity
|
|
518
|
+
});
|
|
519
|
+
const sub = await this.client.subscriptions.create({
|
|
520
|
+
customer: input.customerId,
|
|
521
|
+
items: [{ price, quantity: input.quantity }],
|
|
522
|
+
metadata: input.metadata,
|
|
523
|
+
proration_behavior: "create_prorations",
|
|
524
|
+
automatic_tax: { enabled: true }
|
|
525
|
+
});
|
|
526
|
+
return shapeSubscription(sub, this.prices);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Update quantity or cancel-at-period-end. Used for Teams seat resize
|
|
530
|
+
* and Solo → Teams transition.
|
|
531
|
+
*/
|
|
532
|
+
async updateSubscription(input) {
|
|
533
|
+
this.logger.debug("Updating subscription", { subscriptionId: input.subscriptionId });
|
|
534
|
+
const update = {
|
|
535
|
+
proration_behavior: "create_prorations"
|
|
536
|
+
};
|
|
537
|
+
if (typeof input.quantity === "number") {
|
|
538
|
+
const existing = await this.client.subscriptions.retrieve(input.subscriptionId);
|
|
539
|
+
const itemId = existing.items.data[0]?.id;
|
|
540
|
+
if (itemId !== void 0) {
|
|
541
|
+
update.items = [{ id: itemId, quantity: input.quantity }];
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (typeof input.cancelAtPeriodEnd === "boolean") {
|
|
545
|
+
update.cancel_at_period_end = input.cancelAtPeriodEnd;
|
|
546
|
+
}
|
|
547
|
+
const sub = await this.client.subscriptions.update(
|
|
548
|
+
input.subscriptionId,
|
|
549
|
+
update
|
|
550
|
+
);
|
|
551
|
+
return shapeSubscription(sub, this.prices);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Cancel a subscription. Defaults to `atPeriodEnd: true` so the user
|
|
555
|
+
* keeps access until the current period ends.
|
|
556
|
+
*/
|
|
557
|
+
async cancelSubscription(input) {
|
|
558
|
+
this.logger.debug("Canceling subscription", {
|
|
559
|
+
subscriptionId: input.subscriptionId,
|
|
560
|
+
atPeriodEnd: input.atPeriodEnd
|
|
561
|
+
});
|
|
562
|
+
if (input.atPeriodEnd) {
|
|
563
|
+
const sub2 = await this.client.subscriptions.update(input.subscriptionId, {
|
|
564
|
+
cancel_at_period_end: true
|
|
565
|
+
});
|
|
566
|
+
return shapeSubscription(sub2, this.prices);
|
|
567
|
+
}
|
|
568
|
+
const sub = await this.client.subscriptions.cancel(input.subscriptionId);
|
|
569
|
+
return shapeSubscription(sub, this.prices);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
registerIntegration("stripe", StripeIntegration);
|
|
573
|
+
var YouTubeIntegration = class extends BaseIntegration {
|
|
574
|
+
constructor(config) {
|
|
575
|
+
super(config);
|
|
576
|
+
const apiKey = config.env.YOUTUBE_API_KEY;
|
|
577
|
+
if (!apiKey) {
|
|
578
|
+
throw new Error("YOUTUBE_API_KEY not configured");
|
|
579
|
+
}
|
|
580
|
+
this.client = google.youtube({
|
|
581
|
+
version: "v3",
|
|
582
|
+
auth: apiKey
|
|
583
|
+
});
|
|
584
|
+
this.logger.info("YouTube integration initialized");
|
|
585
|
+
}
|
|
586
|
+
async execute(action, params) {
|
|
587
|
+
const validation = this.validateParams(action, params);
|
|
588
|
+
if (!validation.valid) {
|
|
589
|
+
return {
|
|
590
|
+
success: false,
|
|
591
|
+
error: {
|
|
592
|
+
name: "IntegrationError",
|
|
593
|
+
message: "Validation failed",
|
|
594
|
+
code: "VALIDATION_ERROR",
|
|
595
|
+
details: validation.errors
|
|
596
|
+
},
|
|
597
|
+
metadata: this.createMetadata(action, 0)
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const startTime = Date.now();
|
|
601
|
+
let retries = 0;
|
|
602
|
+
try {
|
|
603
|
+
let data;
|
|
604
|
+
switch (action) {
|
|
605
|
+
case "search":
|
|
606
|
+
data = await this.executeWithRetry(() => this.search(params));
|
|
607
|
+
break;
|
|
608
|
+
case "getVideo":
|
|
609
|
+
data = await this.executeWithRetry(() => this.getVideo(params));
|
|
610
|
+
break;
|
|
611
|
+
case "getChannel":
|
|
612
|
+
data = await this.executeWithRetry(() => this.getChannel(params));
|
|
613
|
+
break;
|
|
614
|
+
default:
|
|
615
|
+
throw new Error(`Unknown action: ${action}`);
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
success: true,
|
|
619
|
+
data,
|
|
620
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
621
|
+
};
|
|
622
|
+
} catch (error) {
|
|
623
|
+
return this.handleError(action, error);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async search(params) {
|
|
627
|
+
const { query, maxResults, type } = params;
|
|
628
|
+
this.logger.debug("Searching YouTube", { query: String(query ?? ""), maxResults: Number(maxResults ?? 10), type: String(type ?? "") });
|
|
629
|
+
const response = await this.client.search.list({
|
|
630
|
+
part: ["snippet"],
|
|
631
|
+
q: query,
|
|
632
|
+
maxResults: maxResults || 10,
|
|
633
|
+
type: type ? [type] : void 0
|
|
634
|
+
});
|
|
635
|
+
return response.data.items?.map((item) => ({
|
|
636
|
+
videoId: item.id?.videoId,
|
|
637
|
+
title: item.snippet?.title,
|
|
638
|
+
thumbnail: item.snippet?.thumbnails?.default?.url,
|
|
639
|
+
description: item.snippet?.description
|
|
640
|
+
}));
|
|
641
|
+
}
|
|
642
|
+
async getVideo(params) {
|
|
643
|
+
const { videoId } = params;
|
|
644
|
+
this.logger.debug("Getting video details", { videoId: String(videoId ?? "") });
|
|
645
|
+
const response = await this.client.videos.list({
|
|
646
|
+
part: ["snippet", "statistics"],
|
|
647
|
+
id: [videoId]
|
|
648
|
+
});
|
|
649
|
+
const video = response.data.items?.[0];
|
|
650
|
+
if (!video) {
|
|
651
|
+
throw new Error(`Video not found: ${videoId}`);
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
title: video.snippet?.title,
|
|
655
|
+
description: video.snippet?.description,
|
|
656
|
+
viewCount: video.statistics?.viewCount,
|
|
657
|
+
likeCount: video.statistics?.likeCount
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
async getChannel(params) {
|
|
661
|
+
const { channelId } = params;
|
|
662
|
+
this.logger.debug("Getting channel details", { channelId: String(channelId ?? "") });
|
|
663
|
+
const response = await this.client.channels.list({
|
|
664
|
+
part: ["snippet", "statistics"],
|
|
665
|
+
id: [channelId]
|
|
666
|
+
});
|
|
667
|
+
const channel = response.data.items?.[0];
|
|
668
|
+
if (!channel) {
|
|
669
|
+
throw new Error(`Channel not found: ${channelId}`);
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
name: channel.snippet?.title,
|
|
673
|
+
description: channel.snippet?.description,
|
|
674
|
+
subscriberCount: channel.statistics?.subscriberCount
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
registerIntegration("youtube", YouTubeIntegration);
|
|
679
|
+
var TwilioIntegration = class extends BaseIntegration {
|
|
680
|
+
constructor(config) {
|
|
681
|
+
super(config);
|
|
682
|
+
const accountSid = config.env.TWILIO_ACCOUNT_SID;
|
|
683
|
+
const authToken = config.env.TWILIO_AUTH_TOKEN;
|
|
684
|
+
this.phoneNumber = config.env.TWILIO_PHONE_NUMBER || "";
|
|
685
|
+
if (!accountSid || !authToken) {
|
|
686
|
+
throw new Error("TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN not configured");
|
|
687
|
+
}
|
|
688
|
+
this.client = twilio(accountSid, authToken);
|
|
689
|
+
this.logger.info("Twilio integration initialized");
|
|
690
|
+
}
|
|
691
|
+
async execute(action, params) {
|
|
692
|
+
const validation = this.validateParams(action, params);
|
|
693
|
+
if (!validation.valid) {
|
|
694
|
+
return {
|
|
695
|
+
success: false,
|
|
696
|
+
error: {
|
|
697
|
+
name: "IntegrationError",
|
|
698
|
+
message: "Validation failed",
|
|
699
|
+
code: "VALIDATION_ERROR",
|
|
700
|
+
details: validation.errors
|
|
701
|
+
},
|
|
702
|
+
metadata: this.createMetadata(action, 0)
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
const startTime = Date.now();
|
|
706
|
+
let retries = 0;
|
|
707
|
+
try {
|
|
708
|
+
let data;
|
|
709
|
+
switch (action) {
|
|
710
|
+
case "sendSMS":
|
|
711
|
+
data = await this.executeWithRetry(() => this.sendSMS(params));
|
|
712
|
+
break;
|
|
713
|
+
case "sendWhatsApp":
|
|
714
|
+
data = await this.executeWithRetry(() => this.sendWhatsApp(params));
|
|
715
|
+
break;
|
|
716
|
+
default:
|
|
717
|
+
throw new Error(`Unknown action: ${action}`);
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
success: true,
|
|
721
|
+
data,
|
|
722
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
723
|
+
};
|
|
724
|
+
} catch (error) {
|
|
725
|
+
return this.handleError(action, error);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async sendSMS(params) {
|
|
729
|
+
const { to, body } = params;
|
|
730
|
+
this.logger.debug("Sending SMS", { to: String(to ?? "") });
|
|
731
|
+
const message = await this.client.messages.create({
|
|
732
|
+
from: this.phoneNumber,
|
|
733
|
+
to,
|
|
734
|
+
body
|
|
735
|
+
});
|
|
736
|
+
return {
|
|
737
|
+
sid: message.sid,
|
|
738
|
+
status: message.status
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
async sendWhatsApp(params) {
|
|
742
|
+
const { to, body } = params;
|
|
743
|
+
this.logger.debug("Sending WhatsApp message", { to: String(to ?? "") });
|
|
744
|
+
const message = await this.client.messages.create({
|
|
745
|
+
from: `whatsapp:${this.phoneNumber}`,
|
|
746
|
+
to: `whatsapp:${to}`,
|
|
747
|
+
body
|
|
748
|
+
});
|
|
749
|
+
return {
|
|
750
|
+
sid: message.sid,
|
|
751
|
+
status: message.status
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
registerIntegration("twilio", TwilioIntegration);
|
|
756
|
+
var EmailIntegration = class extends BaseIntegration {
|
|
757
|
+
constructor(config) {
|
|
758
|
+
super(config);
|
|
759
|
+
this.provider = config.env.PROVIDER || "sendgrid";
|
|
760
|
+
this.fromEmail = config.env.FROM_EMAIL || "noreply@example.com";
|
|
761
|
+
if (this.provider === "sendgrid") {
|
|
762
|
+
const apiKey = config.env.SENDGRID_API_KEY;
|
|
763
|
+
if (!apiKey) {
|
|
764
|
+
throw new Error("SENDGRID_API_KEY not configured");
|
|
765
|
+
}
|
|
766
|
+
sgMail.setApiKey(apiKey);
|
|
767
|
+
} else if (this.provider === "resend") {
|
|
768
|
+
const apiKey = config.env.RESEND_API_KEY;
|
|
769
|
+
if (!apiKey) {
|
|
770
|
+
throw new Error("RESEND_API_KEY not configured");
|
|
771
|
+
}
|
|
772
|
+
this.resendClient = new Resend(apiKey);
|
|
773
|
+
}
|
|
774
|
+
this.logger.info(`Email integration initialized (${this.provider})`);
|
|
775
|
+
}
|
|
776
|
+
async execute(action, params) {
|
|
777
|
+
const validation = this.validateParams(action, params);
|
|
778
|
+
if (!validation.valid) {
|
|
779
|
+
return {
|
|
780
|
+
success: false,
|
|
781
|
+
error: {
|
|
782
|
+
name: "IntegrationError",
|
|
783
|
+
message: "Validation failed",
|
|
784
|
+
code: "VALIDATION_ERROR",
|
|
785
|
+
details: validation.errors
|
|
786
|
+
},
|
|
787
|
+
metadata: this.createMetadata(action, 0)
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
const startTime = Date.now();
|
|
791
|
+
let retries = 0;
|
|
792
|
+
try {
|
|
793
|
+
let data;
|
|
794
|
+
switch (action) {
|
|
795
|
+
case "send":
|
|
796
|
+
data = await this.executeWithRetry(() => this.send(params));
|
|
797
|
+
break;
|
|
798
|
+
default:
|
|
799
|
+
throw new Error(`Unknown action: ${action}`);
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
success: true,
|
|
803
|
+
data,
|
|
804
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
805
|
+
};
|
|
806
|
+
} catch (error) {
|
|
807
|
+
return this.handleError(action, error);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async send(params) {
|
|
811
|
+
const { to, subject, body, from } = params;
|
|
812
|
+
this.logger.debug("Sending email", { to: String(to), subject: String(subject), provider: this.provider });
|
|
813
|
+
if (this.provider === "sendgrid") {
|
|
814
|
+
return await this.sendViaSendGrid(
|
|
815
|
+
to,
|
|
816
|
+
subject,
|
|
817
|
+
body,
|
|
818
|
+
from || this.fromEmail
|
|
819
|
+
);
|
|
820
|
+
} else if (this.provider === "resend") {
|
|
821
|
+
return await this.sendViaResend(
|
|
822
|
+
to,
|
|
823
|
+
subject,
|
|
824
|
+
body,
|
|
825
|
+
from || this.fromEmail
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
throw new Error(`Unknown email provider: ${this.provider}`);
|
|
829
|
+
}
|
|
830
|
+
async sendViaSendGrid(to, subject, body, from) {
|
|
831
|
+
const msg = {
|
|
832
|
+
to,
|
|
833
|
+
from,
|
|
834
|
+
subject,
|
|
835
|
+
html: body
|
|
836
|
+
};
|
|
837
|
+
const response = await sgMail.send(msg);
|
|
838
|
+
return {
|
|
839
|
+
id: response[0].headers["x-message-id"],
|
|
840
|
+
status: "sent"
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
async sendViaResend(to, subject, body, from) {
|
|
844
|
+
if (!this.resendClient) {
|
|
845
|
+
throw new Error("Resend client not initialized");
|
|
846
|
+
}
|
|
847
|
+
const response = await this.resendClient.emails.send({
|
|
848
|
+
from,
|
|
849
|
+
to,
|
|
850
|
+
subject,
|
|
851
|
+
html: body
|
|
852
|
+
});
|
|
853
|
+
return {
|
|
854
|
+
id: response.data?.id,
|
|
855
|
+
status: "sent"
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
registerIntegration("email", EmailIntegration);
|
|
860
|
+
var WebhookIntegration = class extends BaseIntegration {
|
|
861
|
+
constructor(config) {
|
|
862
|
+
super(config);
|
|
863
|
+
this.signingSecret = config.env.WEBHOOK_SIGNING_SECRET || "";
|
|
864
|
+
this.timeoutMs = Number(config.env.WEBHOOK_TIMEOUT_MS) || 1e4;
|
|
865
|
+
this.logger.info("Webhook integration initialized");
|
|
866
|
+
}
|
|
867
|
+
async execute(action, params) {
|
|
868
|
+
const validation = this.validateParams(action, params);
|
|
869
|
+
if (!validation.valid) {
|
|
870
|
+
return {
|
|
871
|
+
success: false,
|
|
872
|
+
error: {
|
|
873
|
+
name: "IntegrationError",
|
|
874
|
+
message: "Validation failed",
|
|
875
|
+
code: "VALIDATION_ERROR",
|
|
876
|
+
details: validation.errors
|
|
877
|
+
},
|
|
878
|
+
metadata: this.createMetadata(action, 0)
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const startTime = Date.now();
|
|
882
|
+
let retries = 0;
|
|
883
|
+
try {
|
|
884
|
+
let data;
|
|
885
|
+
switch (action) {
|
|
886
|
+
case "send":
|
|
887
|
+
data = await this.executeWithRetry(() => this.send(params));
|
|
888
|
+
break;
|
|
889
|
+
default:
|
|
890
|
+
throw new Error(`Unknown action: ${action}`);
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
success: true,
|
|
894
|
+
data,
|
|
895
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
896
|
+
};
|
|
897
|
+
} catch (error) {
|
|
898
|
+
return this.handleError(action, error);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async send(params) {
|
|
902
|
+
const { url, event, payload, secret } = params;
|
|
903
|
+
const body = JSON.stringify({
|
|
904
|
+
event,
|
|
905
|
+
payload: payload ?? {},
|
|
906
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
907
|
+
});
|
|
908
|
+
const headers = {
|
|
909
|
+
"Content-Type": "application/json",
|
|
910
|
+
"X-Almadar-Event": event
|
|
911
|
+
};
|
|
912
|
+
const signingSecret = secret || this.signingSecret;
|
|
913
|
+
if (signingSecret) {
|
|
914
|
+
headers["X-Almadar-Signature"] = "sha256=" + createHmac("sha256", signingSecret).update(body).digest("hex");
|
|
915
|
+
}
|
|
916
|
+
this.logger.debug("Sending webhook", { url: String(url), event: String(event) });
|
|
917
|
+
const startTime = Date.now();
|
|
918
|
+
const response = await fetch(url, {
|
|
919
|
+
method: "POST",
|
|
920
|
+
headers,
|
|
921
|
+
body,
|
|
922
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
923
|
+
});
|
|
924
|
+
if (response.status >= 500) {
|
|
925
|
+
throw new Error(`Webhook endpoint returned ${response.status}`);
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
status: response.status,
|
|
929
|
+
ok: response.ok,
|
|
930
|
+
durationMs: Date.now() - startTime
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
registerIntegration("webhook", WebhookIntegration);
|
|
935
|
+
var LLMIntegration = class extends BaseIntegration {
|
|
936
|
+
constructor(config) {
|
|
937
|
+
super(config);
|
|
938
|
+
this.client = null;
|
|
939
|
+
this.embeddings = null;
|
|
940
|
+
const configuredProvider = config.env.PROVIDER;
|
|
941
|
+
this.provider = configuredProvider || getAvailableProvider() || "anthropic";
|
|
942
|
+
this.logger.info(`LLM integration initialized (provider: ${this.provider})`);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Lazily create client — avoids throwing on construction if API key is missing.
|
|
946
|
+
* The client will throw a clear error when actually used without a key.
|
|
947
|
+
*/
|
|
948
|
+
getClient() {
|
|
949
|
+
if (!this.client) {
|
|
950
|
+
this.client = new LLMClient({
|
|
951
|
+
provider: this.provider,
|
|
952
|
+
temperature: 0.7,
|
|
953
|
+
trackTokens: true
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
return this.client;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Lazily create the embeddings client. Embeddings route through a dedicated
|
|
960
|
+
* embedding model (default OpenRouter `baai/bge-base-en-v1.5`, matching
|
|
961
|
+
* `@almadar/curation-core`), independent of the chat provider/key, because
|
|
962
|
+
* most chat providers do not expose an embeddings endpoint. Override via
|
|
963
|
+
* `EMBEDDING_PROVIDER` / `EMBEDDING_MODEL` / `EMBEDDING_API_KEY`.
|
|
964
|
+
*/
|
|
965
|
+
getEmbeddings() {
|
|
966
|
+
if (!this.embeddings) {
|
|
967
|
+
this.embeddings = new EmbeddingClient({
|
|
968
|
+
provider: this.config.env.EMBEDDING_PROVIDER ?? "openrouter",
|
|
969
|
+
model: this.config.env.EMBEDDING_MODEL ?? "baai/bge-base-en-v1.5",
|
|
970
|
+
apiKey: this.config.env.EMBEDDING_API_KEY
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
return this.embeddings;
|
|
974
|
+
}
|
|
975
|
+
async execute(action, params) {
|
|
976
|
+
const validation = this.validateParams(action, params);
|
|
977
|
+
if (!validation.valid) {
|
|
978
|
+
return {
|
|
979
|
+
success: false,
|
|
980
|
+
error: {
|
|
981
|
+
name: "IntegrationError",
|
|
982
|
+
message: "Validation failed",
|
|
983
|
+
code: "VALIDATION_ERROR",
|
|
984
|
+
details: validation.errors
|
|
985
|
+
},
|
|
986
|
+
metadata: this.createMetadata(action, 0)
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const startTime = Date.now();
|
|
990
|
+
try {
|
|
991
|
+
let data;
|
|
992
|
+
switch (action) {
|
|
993
|
+
case "generate":
|
|
994
|
+
data = await this.executeWithRetry(() => this.generate(params));
|
|
995
|
+
break;
|
|
996
|
+
case "classify":
|
|
997
|
+
data = await this.executeWithRetry(() => this.classify(params));
|
|
998
|
+
break;
|
|
999
|
+
case "extract":
|
|
1000
|
+
data = await this.executeWithRetry(() => this.extract(params));
|
|
1001
|
+
break;
|
|
1002
|
+
case "summarize":
|
|
1003
|
+
data = await this.executeWithRetry(() => this.summarize(params));
|
|
1004
|
+
break;
|
|
1005
|
+
case "embed":
|
|
1006
|
+
data = await this.executeWithRetry(() => this.embed(params));
|
|
1007
|
+
break;
|
|
1008
|
+
default:
|
|
1009
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1010
|
+
}
|
|
1011
|
+
return {
|
|
1012
|
+
success: true,
|
|
1013
|
+
data,
|
|
1014
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1015
|
+
};
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
return this.handleError(action, error);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async generate(params) {
|
|
1021
|
+
const {
|
|
1022
|
+
systemPrompt,
|
|
1023
|
+
userPrompt,
|
|
1024
|
+
model,
|
|
1025
|
+
temperature,
|
|
1026
|
+
maxTokens
|
|
1027
|
+
} = params;
|
|
1028
|
+
this.logger.debug("Generating content", { model, temperature });
|
|
1029
|
+
const client = model ? new LLMClient({ provider: this.provider, model, temperature: temperature ?? 0.7 }) : this.getClient();
|
|
1030
|
+
const response = await client.callWithMetadata({
|
|
1031
|
+
systemPrompt: systemPrompt || "You are a helpful assistant.",
|
|
1032
|
+
userPrompt,
|
|
1033
|
+
maxTokens: maxTokens ?? 1024,
|
|
1034
|
+
temperature,
|
|
1035
|
+
skipSchemaValidation: true
|
|
1036
|
+
});
|
|
1037
|
+
return {
|
|
1038
|
+
content: response.data,
|
|
1039
|
+
usage: response.usage ? {
|
|
1040
|
+
promptTokens: response.usage.promptTokens,
|
|
1041
|
+
completionTokens: response.usage.completionTokens,
|
|
1042
|
+
totalTokens: response.usage.totalTokens
|
|
1043
|
+
} : { tokens: 0 }
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
async classify(params) {
|
|
1047
|
+
const { text, categories, model } = params;
|
|
1048
|
+
this.logger.debug("Classifying text", { categories: categories.join(", ") });
|
|
1049
|
+
const ClassificationSchema = z.object({
|
|
1050
|
+
category: z.enum(categories),
|
|
1051
|
+
confidence: z.number().min(0).max(1),
|
|
1052
|
+
reasoning: z.string()
|
|
1053
|
+
});
|
|
1054
|
+
const client = model ? new LLMClient({ provider: this.provider, model, temperature: 0.1 }) : this.getClient();
|
|
1055
|
+
const result = await client.call({
|
|
1056
|
+
systemPrompt: `You are a text classifier. Classify the given text into one of these categories: ${categories.join(", ")}. Return JSON with: category, confidence (0-1), and reasoning.`,
|
|
1057
|
+
userPrompt: text,
|
|
1058
|
+
schema: ClassificationSchema,
|
|
1059
|
+
temperature: 0.1
|
|
1060
|
+
});
|
|
1061
|
+
return result;
|
|
1062
|
+
}
|
|
1063
|
+
async extract(params) {
|
|
1064
|
+
const { text, schema, model } = params;
|
|
1065
|
+
this.logger.debug("Extracting structured data", { schema: String(schema ?? "") });
|
|
1066
|
+
const ExtractSchema = z.record(z.unknown());
|
|
1067
|
+
const client = model ? new LLMClient({ provider: this.provider, model, temperature: 0.2 }) : this.getClient();
|
|
1068
|
+
const schemaDescription = JSON.stringify(schema, null, 2);
|
|
1069
|
+
const result = await client.call({
|
|
1070
|
+
systemPrompt: `You are a structured data extractor. Extract data from the given text according to this schema:
|
|
1071
|
+
|
|
1072
|
+
${schemaDescription}
|
|
1073
|
+
|
|
1074
|
+
Return ONLY valid JSON matching the schema.`,
|
|
1075
|
+
userPrompt: text,
|
|
1076
|
+
schema: ExtractSchema,
|
|
1077
|
+
temperature: 0.2,
|
|
1078
|
+
skipSchemaValidation: true
|
|
1079
|
+
});
|
|
1080
|
+
return result;
|
|
1081
|
+
}
|
|
1082
|
+
async summarize(params) {
|
|
1083
|
+
const { text, maxLength, style, model } = params;
|
|
1084
|
+
this.logger.debug("Summarizing text", { maxLength, style });
|
|
1085
|
+
const SummarySchema = z.object({
|
|
1086
|
+
summary: z.string(),
|
|
1087
|
+
keyPoints: z.array(z.string())
|
|
1088
|
+
});
|
|
1089
|
+
const styleInstructions = style === "bullet" ? "Use bullet points." : style === "detailed" ? "Be thorough and detailed." : "Be concise.";
|
|
1090
|
+
const lengthInstructions = maxLength ? `Keep the summary under ${maxLength} words.` : "";
|
|
1091
|
+
const client = model ? new LLMClient({ provider: this.provider, model, temperature: 0.3 }) : this.getClient();
|
|
1092
|
+
const result = await client.call({
|
|
1093
|
+
systemPrompt: `You are a text summarizer. ${styleInstructions} ${lengthInstructions} Return JSON with: summary (string) and keyPoints (array of strings).`,
|
|
1094
|
+
userPrompt: text,
|
|
1095
|
+
schema: SummarySchema,
|
|
1096
|
+
temperature: 0.3
|
|
1097
|
+
});
|
|
1098
|
+
return result;
|
|
1099
|
+
}
|
|
1100
|
+
async embed(params) {
|
|
1101
|
+
const { texts } = params;
|
|
1102
|
+
if (!Array.isArray(texts)) {
|
|
1103
|
+
throw new Error("llm.embed: `texts` must be an array of strings");
|
|
1104
|
+
}
|
|
1105
|
+
this.logger.debug("Embedding texts", { count: texts.length });
|
|
1106
|
+
const batch = await this.getEmbeddings().embedBatch(texts);
|
|
1107
|
+
return { embeddings: batch.embeddings };
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
registerIntegration("llm", LLMIntegration);
|
|
1111
|
+
|
|
1112
|
+
// src/integrations/ml/index.ts
|
|
1113
|
+
var INFERRED_EVENT = "INFERRED";
|
|
1114
|
+
var INFER_FAILED_EVENT = "INFER_FAILED";
|
|
1115
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
1116
|
+
function isServiceParams(value) {
|
|
1117
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
1118
|
+
}
|
|
1119
|
+
function extractInferResult(response) {
|
|
1120
|
+
const emitted = (response.effectResults ?? []).find(
|
|
1121
|
+
(result) => result.effect === "emit" && result.success && isServiceParams(result.data) && result.data.event === INFERRED_EVENT
|
|
1122
|
+
);
|
|
1123
|
+
if (!emitted || !isServiceParams(emitted.data)) {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
const payload = emitted.data.payload;
|
|
1127
|
+
if (!isServiceParams(payload)) {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
if (!("output" in payload) || typeof payload.confidence !== "number" || !Array.isArray(payload.violations)) {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
output: payload.output,
|
|
1135
|
+
confidence: payload.confidence,
|
|
1136
|
+
violations: payload.violations
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
var MLIntegration = class extends BaseIntegration {
|
|
1140
|
+
constructor(config) {
|
|
1141
|
+
super(config);
|
|
1142
|
+
const baseUrl = config.env.MASAR_URL;
|
|
1143
|
+
const traitPrefix = config.env.MASAR_ML_TRAIT;
|
|
1144
|
+
if (!baseUrl || !traitPrefix) {
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
"ML integration requires MASAR_URL (serving orbital host) and MASAR_ML_TRAIT (kebab-case trait name of its /events route) to be configured"
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
this.eventsUrl = `${baseUrl.replace(/\/+$/, "")}/api/${traitPrefix}/events`;
|
|
1150
|
+
this.timeoutMs = config.env.MASAR_ML_TIMEOUT_MS ? Number(config.env.MASAR_ML_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
1151
|
+
this.logger.info("ML integration initialized", { eventsUrl: this.eventsUrl });
|
|
1152
|
+
}
|
|
1153
|
+
async execute(action, params) {
|
|
1154
|
+
const validation = this.validateParams(action, params);
|
|
1155
|
+
if (!validation.valid) {
|
|
1156
|
+
return {
|
|
1157
|
+
success: false,
|
|
1158
|
+
error: {
|
|
1159
|
+
name: "IntegrationError",
|
|
1160
|
+
message: "Validation failed",
|
|
1161
|
+
code: "VALIDATION_ERROR",
|
|
1162
|
+
details: validation.errors
|
|
1163
|
+
},
|
|
1164
|
+
metadata: this.createMetadata(action, 0)
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
const startTime = Date.now();
|
|
1168
|
+
try {
|
|
1169
|
+
let data;
|
|
1170
|
+
switch (action) {
|
|
1171
|
+
case "infer":
|
|
1172
|
+
data = await this.executeWithRetry(() => this.infer(params));
|
|
1173
|
+
break;
|
|
1174
|
+
default:
|
|
1175
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1176
|
+
}
|
|
1177
|
+
return {
|
|
1178
|
+
success: true,
|
|
1179
|
+
data,
|
|
1180
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1181
|
+
};
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
return this.handleError(action, error);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
async infer(params) {
|
|
1187
|
+
const { model, input } = params;
|
|
1188
|
+
this.logger.debug("Running ML inference", { model });
|
|
1189
|
+
const response = await this.postEvent(model, input);
|
|
1190
|
+
if (!response.success) {
|
|
1191
|
+
throw new IntegrationError(
|
|
1192
|
+
`ML service reported failure: ${response.error ?? "unknown error"}`,
|
|
1193
|
+
"SERVICE_ERROR",
|
|
1194
|
+
{ model, response }
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const failed = (response.effectResults ?? []).find(
|
|
1198
|
+
(result) => result.effect === "emit" && isServiceParams(result.data) && result.data.event === INFER_FAILED_EVENT
|
|
1199
|
+
);
|
|
1200
|
+
if (failed) {
|
|
1201
|
+
throw new IntegrationError(
|
|
1202
|
+
`ML service could not infer: ${failed.error ?? "model unavailable"}`,
|
|
1203
|
+
"SERVICE_ERROR",
|
|
1204
|
+
{ model, response }
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
const inferred = extractInferResult(response);
|
|
1208
|
+
if (!inferred) {
|
|
1209
|
+
throw new IntegrationError(
|
|
1210
|
+
"ML service response did not include a valid inference result",
|
|
1211
|
+
"SERVICE_ERROR",
|
|
1212
|
+
{ model, response }
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
return inferred;
|
|
1216
|
+
}
|
|
1217
|
+
async postEvent(model, input) {
|
|
1218
|
+
const controller = new AbortController();
|
|
1219
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1220
|
+
let httpResponse;
|
|
1221
|
+
try {
|
|
1222
|
+
httpResponse = await fetch(this.eventsUrl, {
|
|
1223
|
+
method: "POST",
|
|
1224
|
+
headers: { "Content-Type": "application/json" },
|
|
1225
|
+
body: JSON.stringify({ event: "INFER", payload: { model, input } }),
|
|
1226
|
+
signal: controller.signal
|
|
1227
|
+
});
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
1230
|
+
throw new IntegrationError(
|
|
1231
|
+
`ML service request timed out after ${this.timeoutMs}ms (model may be cold-starting)`,
|
|
1232
|
+
"TIMEOUT_ERROR",
|
|
1233
|
+
{ model, eventsUrl: this.eventsUrl }
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
throw new IntegrationError(
|
|
1237
|
+
`ML service request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1238
|
+
"NETWORK_ERROR",
|
|
1239
|
+
{ model, eventsUrl: this.eventsUrl }
|
|
1240
|
+
);
|
|
1241
|
+
} finally {
|
|
1242
|
+
clearTimeout(timer);
|
|
1243
|
+
}
|
|
1244
|
+
if (!httpResponse.ok) {
|
|
1245
|
+
const body = await httpResponse.text().catch(() => "");
|
|
1246
|
+
throw new IntegrationError(
|
|
1247
|
+
`ML service responded with status ${httpResponse.status}`,
|
|
1248
|
+
"SERVICE_ERROR",
|
|
1249
|
+
{ model, status: httpResponse.status, body }
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
return await httpResponse.json();
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
throw new IntegrationError(
|
|
1256
|
+
`ML service returned a malformed response body: ${error instanceof Error ? error.message : String(error)}`,
|
|
1257
|
+
"SERVICE_ERROR",
|
|
1258
|
+
{ model }
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
registerIntegration("ml", MLIntegration);
|
|
1264
|
+
|
|
1265
|
+
// src/integrations/deepagent/index.ts
|
|
1266
|
+
var DeepAgentIntegration = class extends BaseIntegration {
|
|
1267
|
+
constructor(config) {
|
|
1268
|
+
super(config);
|
|
1269
|
+
this.apiUrl = config.env.DEEPAGENT_API_URL || "http://localhost:3000";
|
|
1270
|
+
this.apiKey = config.env.DEEPAGENT_API_KEY || "";
|
|
1271
|
+
this.logger.info("DeepAgent integration initialized", { apiUrl: this.apiUrl });
|
|
1272
|
+
}
|
|
1273
|
+
async execute(action, params) {
|
|
1274
|
+
const validation = this.validateParams(action, params);
|
|
1275
|
+
if (!validation.valid) {
|
|
1276
|
+
return {
|
|
1277
|
+
success: false,
|
|
1278
|
+
error: {
|
|
1279
|
+
name: "IntegrationError",
|
|
1280
|
+
message: "Validation failed",
|
|
1281
|
+
code: "VALIDATION_ERROR",
|
|
1282
|
+
details: validation.errors
|
|
1283
|
+
},
|
|
1284
|
+
metadata: this.createMetadata(action, 0)
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
const startTime = Date.now();
|
|
1288
|
+
let retries = 0;
|
|
1289
|
+
try {
|
|
1290
|
+
let data;
|
|
1291
|
+
switch (action) {
|
|
1292
|
+
case "sendMessage":
|
|
1293
|
+
data = await this.executeWithRetry(() => this.sendMessage(params));
|
|
1294
|
+
break;
|
|
1295
|
+
case "cancelGeneration":
|
|
1296
|
+
data = await this.executeWithRetry(
|
|
1297
|
+
() => this.cancelGeneration(params)
|
|
1298
|
+
);
|
|
1299
|
+
break;
|
|
1300
|
+
case "validateSchema":
|
|
1301
|
+
data = await this.executeWithRetry(() => this.validateSchema(params));
|
|
1302
|
+
break;
|
|
1303
|
+
case "compileSchema":
|
|
1304
|
+
data = await this.executeWithRetry(() => this.compileSchema(params));
|
|
1305
|
+
break;
|
|
1306
|
+
case "getThreadHistory":
|
|
1307
|
+
data = await this.executeWithRetry(
|
|
1308
|
+
() => this.getThreadHistory(params)
|
|
1309
|
+
);
|
|
1310
|
+
break;
|
|
1311
|
+
default:
|
|
1312
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
success: true,
|
|
1316
|
+
data,
|
|
1317
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1318
|
+
};
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
return this.handleError(action, error);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
async sendMessage(params) {
|
|
1324
|
+
const { message, threadId, skill, context } = params;
|
|
1325
|
+
this.logger.debug("Sending message to DeepAgent", { threadId: String(threadId ?? ""), skill: String(skill ?? "") });
|
|
1326
|
+
const response = await this.request("/api/agent/message", {
|
|
1327
|
+
message,
|
|
1328
|
+
threadId,
|
|
1329
|
+
skill,
|
|
1330
|
+
context
|
|
1331
|
+
});
|
|
1332
|
+
return response;
|
|
1333
|
+
}
|
|
1334
|
+
async cancelGeneration(params) {
|
|
1335
|
+
const { threadId } = params;
|
|
1336
|
+
this.logger.debug("Cancelling generation", { threadId: String(threadId ?? "") });
|
|
1337
|
+
const response = await this.request("/api/agent/cancel", {
|
|
1338
|
+
threadId
|
|
1339
|
+
});
|
|
1340
|
+
return response;
|
|
1341
|
+
}
|
|
1342
|
+
async validateSchema(params) {
|
|
1343
|
+
const { schema } = params;
|
|
1344
|
+
this.logger.debug("Validating schema");
|
|
1345
|
+
const response = await this.request("/api/schema/validate", {
|
|
1346
|
+
schema
|
|
1347
|
+
});
|
|
1348
|
+
return response;
|
|
1349
|
+
}
|
|
1350
|
+
async compileSchema(params) {
|
|
1351
|
+
const { schema, shell } = params;
|
|
1352
|
+
this.logger.debug("Compiling schema", { shell: String(shell ?? "") });
|
|
1353
|
+
const response = await this.request("/api/schema/compile", {
|
|
1354
|
+
schema,
|
|
1355
|
+
shell
|
|
1356
|
+
});
|
|
1357
|
+
return response;
|
|
1358
|
+
}
|
|
1359
|
+
async getThreadHistory(params) {
|
|
1360
|
+
const { threadId } = params;
|
|
1361
|
+
this.logger.debug("Getting thread history", { threadId: String(threadId ?? "") });
|
|
1362
|
+
const response = await this.request("/api/agent/history", {
|
|
1363
|
+
threadId
|
|
1364
|
+
});
|
|
1365
|
+
return response;
|
|
1366
|
+
}
|
|
1367
|
+
async request(endpoint, body) {
|
|
1368
|
+
const url = `${this.apiUrl}${endpoint}`;
|
|
1369
|
+
const response = await fetch(url, {
|
|
1370
|
+
method: "POST",
|
|
1371
|
+
headers: {
|
|
1372
|
+
"Content-Type": "application/json",
|
|
1373
|
+
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
|
|
1374
|
+
},
|
|
1375
|
+
body: JSON.stringify(body)
|
|
1376
|
+
});
|
|
1377
|
+
if (!response.ok) {
|
|
1378
|
+
throw new Error(
|
|
1379
|
+
`DeepAgent request failed: ${response.status} ${response.statusText}`
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
return await response.json();
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
registerIntegration("deepagent", DeepAgentIntegration);
|
|
1386
|
+
async function execGit(args, cwd, env) {
|
|
1387
|
+
return new Promise((resolve, reject) => {
|
|
1388
|
+
const proc = spawn("git", args, {
|
|
1389
|
+
cwd,
|
|
1390
|
+
env: { ...process.env, ...env },
|
|
1391
|
+
stdio: "pipe"
|
|
1392
|
+
});
|
|
1393
|
+
let stdout = "";
|
|
1394
|
+
let stderr = "";
|
|
1395
|
+
proc.stdout?.on("data", (data) => {
|
|
1396
|
+
stdout += data.toString();
|
|
1397
|
+
});
|
|
1398
|
+
proc.stderr?.on("data", (data) => {
|
|
1399
|
+
stderr += data.toString();
|
|
1400
|
+
});
|
|
1401
|
+
proc.on("close", (code) => {
|
|
1402
|
+
if (code === 0) {
|
|
1403
|
+
resolve({ stdout, stderr });
|
|
1404
|
+
} else {
|
|
1405
|
+
reject(
|
|
1406
|
+
new IntegrationError(
|
|
1407
|
+
`Git command failed: ${args.join(" ")}
|
|
1408
|
+
${stderr}`,
|
|
1409
|
+
"SERVICE_ERROR",
|
|
1410
|
+
{ code, stderr }
|
|
1411
|
+
)
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
proc.on("error", (error) => {
|
|
1416
|
+
reject(
|
|
1417
|
+
new IntegrationError(
|
|
1418
|
+
`Failed to execute git: ${error.message}`,
|
|
1419
|
+
"SERVICE_ERROR",
|
|
1420
|
+
{ error }
|
|
1421
|
+
)
|
|
1422
|
+
);
|
|
1423
|
+
});
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
async function cloneRepo(params, token) {
|
|
1427
|
+
const { repoUrl, targetDir, branch, depth = 1 } = params;
|
|
1428
|
+
const authUrl = injectTokenIntoUrl(repoUrl, token);
|
|
1429
|
+
const args = ["clone"];
|
|
1430
|
+
if (depth > 0) {
|
|
1431
|
+
args.push("--depth", depth.toString());
|
|
1432
|
+
}
|
|
1433
|
+
if (branch) {
|
|
1434
|
+
args.push("--branch", branch);
|
|
1435
|
+
}
|
|
1436
|
+
args.push(authUrl, targetDir);
|
|
1437
|
+
try {
|
|
1438
|
+
await execGit(args, process.cwd());
|
|
1439
|
+
await scrubTokenFromRemote(targetDir, repoUrl);
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
throw new IntegrationError(
|
|
1442
|
+
`Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`,
|
|
1443
|
+
"SERVICE_ERROR",
|
|
1444
|
+
{ repoUrl, error }
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
async function createBranch(params, workDir) {
|
|
1449
|
+
const { branchName, baseBranch } = params;
|
|
1450
|
+
const cwd = params.workDir || workDir;
|
|
1451
|
+
try {
|
|
1452
|
+
if (baseBranch) {
|
|
1453
|
+
await execGit(["checkout", baseBranch], cwd);
|
|
1454
|
+
}
|
|
1455
|
+
await execGit(["checkout", "-b", branchName], cwd);
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
throw new IntegrationError(
|
|
1458
|
+
`Failed to create branch: ${error instanceof Error ? error.message : String(error)}`,
|
|
1459
|
+
"SERVICE_ERROR",
|
|
1460
|
+
{ branchName, baseBranch, error }
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
async function commit(params, workDir) {
|
|
1465
|
+
const { message, files } = params;
|
|
1466
|
+
const cwd = params.workDir || workDir;
|
|
1467
|
+
try {
|
|
1468
|
+
if (files && files.length > 0) {
|
|
1469
|
+
await execGit(["add", ...files], cwd);
|
|
1470
|
+
} else {
|
|
1471
|
+
await execGit(["add", "."], cwd);
|
|
1472
|
+
}
|
|
1473
|
+
await execGit(["commit", "-m", message], cwd);
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
throw new IntegrationError(
|
|
1476
|
+
`Failed to commit: ${error instanceof Error ? error.message : String(error)}`,
|
|
1477
|
+
"SERVICE_ERROR",
|
|
1478
|
+
{ message, error }
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
async function push(params, workDir, token) {
|
|
1483
|
+
const { branchName, force = false } = params;
|
|
1484
|
+
const cwd = params.workDir || workDir;
|
|
1485
|
+
const protectedBranches = ["main", "master", "production", "prod"];
|
|
1486
|
+
if (force && protectedBranches.includes(branchName.toLowerCase())) {
|
|
1487
|
+
throw new IntegrationError(
|
|
1488
|
+
`Force push to protected branch '${branchName}' is not allowed`,
|
|
1489
|
+
"VALIDATION_ERROR"
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
const credHelper = await createTempCredentialHelper(token);
|
|
1494
|
+
const args = ["push"];
|
|
1495
|
+
if (force) {
|
|
1496
|
+
args.push("--force");
|
|
1497
|
+
}
|
|
1498
|
+
args.push("-u", "origin", branchName);
|
|
1499
|
+
await execGit(args, cwd, {
|
|
1500
|
+
GIT_ASKPASS: credHelper,
|
|
1501
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
1502
|
+
});
|
|
1503
|
+
await promises.unlink(credHelper);
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
throw new IntegrationError(
|
|
1506
|
+
`Failed to push: ${error instanceof Error ? error.message : String(error)}`,
|
|
1507
|
+
"SERVICE_ERROR",
|
|
1508
|
+
{ branchName, error }
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function injectTokenIntoUrl(repoUrl, token) {
|
|
1513
|
+
const url = new URL(repoUrl);
|
|
1514
|
+
url.username = "x-access-token";
|
|
1515
|
+
url.password = token;
|
|
1516
|
+
return url.toString();
|
|
1517
|
+
}
|
|
1518
|
+
async function scrubTokenFromRemote(workDir, originalUrl) {
|
|
1519
|
+
try {
|
|
1520
|
+
const url = new URL(originalUrl);
|
|
1521
|
+
url.username = "";
|
|
1522
|
+
url.password = "";
|
|
1523
|
+
const cleanUrl = url.toString();
|
|
1524
|
+
await execGit(["remote", "set-url", "origin", cleanUrl], workDir);
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
console.error("Warning: Failed to scrub token from remote URL:", error);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
async function createTempCredentialHelper(token) {
|
|
1530
|
+
const tmpDir = process.env.TMPDIR || "/tmp";
|
|
1531
|
+
const helperPath = join(tmpDir, `git-cred-${Date.now()}.sh`);
|
|
1532
|
+
const script = `#!/bin/sh
|
|
1533
|
+
echo "${token}"`;
|
|
1534
|
+
await promises.writeFile(helperPath, script, { mode: 448 });
|
|
1535
|
+
return helperPath;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/integrations/github/github-api.ts
|
|
1539
|
+
async function githubFetch(endpoint, config, options = {}) {
|
|
1540
|
+
const url = `https://api.github.com${endpoint}`;
|
|
1541
|
+
const headers = {
|
|
1542
|
+
"Authorization": `Bearer ${config.token}`,
|
|
1543
|
+
"Accept": "application/vnd.github+json",
|
|
1544
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
1545
|
+
...options.headers
|
|
1546
|
+
};
|
|
1547
|
+
try {
|
|
1548
|
+
const response = await fetch(url, {
|
|
1549
|
+
...options,
|
|
1550
|
+
headers
|
|
1551
|
+
});
|
|
1552
|
+
const rateLimit = {
|
|
1553
|
+
remaining: parseInt(response.headers.get("x-ratelimit-remaining") || "0", 10),
|
|
1554
|
+
limit: parseInt(response.headers.get("x-ratelimit-limit") || "5000", 10),
|
|
1555
|
+
reset: parseInt(response.headers.get("x-ratelimit-reset") || "0", 10)
|
|
1556
|
+
};
|
|
1557
|
+
if (!response.ok) {
|
|
1558
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
1559
|
+
if (response.status === 429) {
|
|
1560
|
+
throw new IntegrationError(
|
|
1561
|
+
"GitHub API rate limit exceeded",
|
|
1562
|
+
"RATE_LIMIT_ERROR",
|
|
1563
|
+
{ rateLimit, error }
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
if (response.status === 401 || response.status === 403) {
|
|
1567
|
+
throw new IntegrationError(
|
|
1568
|
+
`GitHub API authentication failed: ${error.message || response.statusText}`,
|
|
1569
|
+
"AUTH_ERROR",
|
|
1570
|
+
{ status: response.status, error }
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
throw new IntegrationError(
|
|
1574
|
+
`GitHub API request failed: ${error.message || response.statusText}`,
|
|
1575
|
+
"SERVICE_ERROR",
|
|
1576
|
+
{ status: response.status, error }
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
const data = await response.json();
|
|
1580
|
+
return { data, rateLimit };
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
if (error instanceof IntegrationError) {
|
|
1583
|
+
throw error;
|
|
1584
|
+
}
|
|
1585
|
+
throw new IntegrationError(
|
|
1586
|
+
`GitHub API request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1587
|
+
"NETWORK_ERROR",
|
|
1588
|
+
{ error }
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
async function createPR(params, config) {
|
|
1593
|
+
const { title, body, baseBranch, headBranch, draft = false } = params;
|
|
1594
|
+
const endpoint = `/repos/${config.owner}/${config.repo}/pulls`;
|
|
1595
|
+
const { data } = await githubFetch(endpoint, config, {
|
|
1596
|
+
method: "POST",
|
|
1597
|
+
body: JSON.stringify({
|
|
1598
|
+
title,
|
|
1599
|
+
body,
|
|
1600
|
+
base: baseBranch,
|
|
1601
|
+
head: headBranch,
|
|
1602
|
+
draft
|
|
1603
|
+
})
|
|
1604
|
+
});
|
|
1605
|
+
return data;
|
|
1606
|
+
}
|
|
1607
|
+
async function getPRComments(params, config) {
|
|
1608
|
+
const { prNumber } = params;
|
|
1609
|
+
const endpoint = `/repos/${config.owner}/${config.repo}/pulls/${prNumber}/comments`;
|
|
1610
|
+
const { data } = await githubFetch(endpoint, config);
|
|
1611
|
+
return data;
|
|
1612
|
+
}
|
|
1613
|
+
async function listIssues(params, config) {
|
|
1614
|
+
const { state = "open", labels = [], limit = 30 } = params;
|
|
1615
|
+
const queryParams = new URLSearchParams({
|
|
1616
|
+
state,
|
|
1617
|
+
per_page: Math.min(limit, 100).toString()
|
|
1618
|
+
});
|
|
1619
|
+
if (labels.length > 0) {
|
|
1620
|
+
queryParams.append("labels", labels.join(","));
|
|
1621
|
+
}
|
|
1622
|
+
const endpoint = `/repos/${config.owner}/${config.repo}/issues?${queryParams}`;
|
|
1623
|
+
const { data } = await githubFetch(endpoint, config);
|
|
1624
|
+
return data;
|
|
1625
|
+
}
|
|
1626
|
+
async function getIssue(params, config) {
|
|
1627
|
+
const { issueNumber } = params;
|
|
1628
|
+
const issueEndpoint = `/repos/${config.owner}/${config.repo}/issues/${issueNumber}`;
|
|
1629
|
+
const { data: issue } = await githubFetch(issueEndpoint, config);
|
|
1630
|
+
const commentsEndpoint = `/repos/${config.owner}/${config.repo}/issues/${issueNumber}/comments`;
|
|
1631
|
+
const { data: comments } = await githubFetch(commentsEndpoint, config);
|
|
1632
|
+
return { issue, comments };
|
|
1633
|
+
}
|
|
1634
|
+
function parseRepoUrl(repoUrl) {
|
|
1635
|
+
try {
|
|
1636
|
+
const url = new URL(repoUrl);
|
|
1637
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
1638
|
+
if (pathParts.length < 2) {
|
|
1639
|
+
throw new Error("Invalid repository URL format");
|
|
1640
|
+
}
|
|
1641
|
+
const owner = pathParts[0];
|
|
1642
|
+
const repo = pathParts[1].replace(/\.git$/, "");
|
|
1643
|
+
return { owner, repo };
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
throw new IntegrationError(
|
|
1646
|
+
`Failed to parse repository URL: ${repoUrl}`,
|
|
1647
|
+
"VALIDATION_ERROR",
|
|
1648
|
+
{ error }
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// src/integrations/github/index.ts
|
|
1654
|
+
var GitHubIntegration = class extends BaseIntegration {
|
|
1655
|
+
constructor(config) {
|
|
1656
|
+
super(config);
|
|
1657
|
+
this.token = config.env.GITHUB_TOKEN;
|
|
1658
|
+
if (!this.token) {
|
|
1659
|
+
throw new Error("GITHUB_TOKEN not configured");
|
|
1660
|
+
}
|
|
1661
|
+
this.owner = config.env.GITHUB_OWNER || "";
|
|
1662
|
+
this.repo = config.env.GITHUB_REPO || "";
|
|
1663
|
+
this.workDir = config.env.GITHUB_WORK_DIR || process.cwd();
|
|
1664
|
+
this.logger.info("GitHub integration initialized", {
|
|
1665
|
+
owner: this.owner,
|
|
1666
|
+
repo: this.repo
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Execute a GitHub action
|
|
1671
|
+
*/
|
|
1672
|
+
async execute(action, params) {
|
|
1673
|
+
const validation = this.validateParams(action, params);
|
|
1674
|
+
if (!validation.valid) {
|
|
1675
|
+
return {
|
|
1676
|
+
success: false,
|
|
1677
|
+
error: {
|
|
1678
|
+
name: "IntegrationError",
|
|
1679
|
+
message: "Validation failed",
|
|
1680
|
+
code: "VALIDATION_ERROR",
|
|
1681
|
+
details: validation.errors
|
|
1682
|
+
},
|
|
1683
|
+
metadata: this.createMetadata(action, 0)
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
const startTime = Date.now();
|
|
1687
|
+
let retries = 0;
|
|
1688
|
+
try {
|
|
1689
|
+
let data;
|
|
1690
|
+
switch (action) {
|
|
1691
|
+
case "cloneRepo":
|
|
1692
|
+
data = await this.executeWithRetry(
|
|
1693
|
+
() => this.cloneRepo(params)
|
|
1694
|
+
);
|
|
1695
|
+
break;
|
|
1696
|
+
case "createBranch":
|
|
1697
|
+
data = await this.executeWithRetry(
|
|
1698
|
+
() => this.createBranch(params)
|
|
1699
|
+
);
|
|
1700
|
+
break;
|
|
1701
|
+
case "commit":
|
|
1702
|
+
data = await this.executeWithRetry(
|
|
1703
|
+
() => this.commit(params)
|
|
1704
|
+
);
|
|
1705
|
+
break;
|
|
1706
|
+
case "push":
|
|
1707
|
+
data = await this.executeWithRetry(
|
|
1708
|
+
() => this.push(params)
|
|
1709
|
+
);
|
|
1710
|
+
break;
|
|
1711
|
+
case "createPR":
|
|
1712
|
+
data = await this.executeWithRetry(
|
|
1713
|
+
() => this.createPR(params)
|
|
1714
|
+
);
|
|
1715
|
+
break;
|
|
1716
|
+
case "getPRComments":
|
|
1717
|
+
data = await this.executeWithRetry(
|
|
1718
|
+
() => this.getPRComments(params)
|
|
1719
|
+
);
|
|
1720
|
+
break;
|
|
1721
|
+
case "listIssues":
|
|
1722
|
+
data = await this.executeWithRetry(
|
|
1723
|
+
() => this.listIssues(params)
|
|
1724
|
+
);
|
|
1725
|
+
break;
|
|
1726
|
+
case "getIssue":
|
|
1727
|
+
data = await this.executeWithRetry(
|
|
1728
|
+
() => this.getIssue(params)
|
|
1729
|
+
);
|
|
1730
|
+
break;
|
|
1731
|
+
default:
|
|
1732
|
+
throw new Error(`Unknown GitHub action: ${action}`);
|
|
1733
|
+
}
|
|
1734
|
+
return {
|
|
1735
|
+
success: true,
|
|
1736
|
+
data,
|
|
1737
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1738
|
+
};
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
return this.handleError(action, error);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Clone a repository
|
|
1745
|
+
*/
|
|
1746
|
+
async cloneRepo(params) {
|
|
1747
|
+
this.logger.debug("Cloning repository", { repoUrl: params.repoUrl });
|
|
1748
|
+
if (!this.owner || !this.repo) {
|
|
1749
|
+
const parsed = parseRepoUrl(params.repoUrl);
|
|
1750
|
+
this.owner = parsed.owner;
|
|
1751
|
+
this.repo = parsed.repo;
|
|
1752
|
+
}
|
|
1753
|
+
await cloneRepo(params, this.token);
|
|
1754
|
+
return {
|
|
1755
|
+
message: `Successfully cloned ${params.repoUrl} to ${params.targetDir}`
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Create a branch
|
|
1760
|
+
*/
|
|
1761
|
+
async createBranch(params) {
|
|
1762
|
+
this.logger.debug("Creating branch", { branchName: params.branchName });
|
|
1763
|
+
await createBranch(params, this.workDir);
|
|
1764
|
+
return {
|
|
1765
|
+
message: `Successfully created branch: ${params.branchName}`
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Commit changes
|
|
1770
|
+
*/
|
|
1771
|
+
async commit(params) {
|
|
1772
|
+
this.logger.debug("Committing changes", { message: params.message });
|
|
1773
|
+
await commit(params, this.workDir);
|
|
1774
|
+
return {
|
|
1775
|
+
message: `Successfully committed changes: ${params.message}`
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Push branch
|
|
1780
|
+
*/
|
|
1781
|
+
async push(params) {
|
|
1782
|
+
this.logger.debug("Pushing branch", { branchName: params.branchName });
|
|
1783
|
+
await push(params, this.workDir, this.token);
|
|
1784
|
+
return {
|
|
1785
|
+
message: `Successfully pushed branch: ${params.branchName}`
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Create a pull request
|
|
1790
|
+
*/
|
|
1791
|
+
async createPR(params) {
|
|
1792
|
+
this.logger.debug("Creating pull request", { title: params.title });
|
|
1793
|
+
const apiConfig = this.getAPIConfig();
|
|
1794
|
+
const pr = await createPR(params, apiConfig);
|
|
1795
|
+
this.logger.info("Pull request created", { number: pr.number, url: pr.url });
|
|
1796
|
+
return pr;
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Get PR comments
|
|
1800
|
+
*/
|
|
1801
|
+
async getPRComments(params) {
|
|
1802
|
+
this.logger.debug("Getting PR comments", { prNumber: params.prNumber });
|
|
1803
|
+
const apiConfig = this.getAPIConfig();
|
|
1804
|
+
const comments = await getPRComments(params, apiConfig);
|
|
1805
|
+
return { comments };
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* List issues
|
|
1809
|
+
*/
|
|
1810
|
+
async listIssues(params) {
|
|
1811
|
+
this.logger.debug("Listing issues", { state: params.state, labels: params.labels?.join(", "), limit: params.limit });
|
|
1812
|
+
const apiConfig = this.getAPIConfig();
|
|
1813
|
+
const issues = await listIssues(params, apiConfig);
|
|
1814
|
+
return { issues };
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Get issue details
|
|
1818
|
+
*/
|
|
1819
|
+
async getIssue(params) {
|
|
1820
|
+
this.logger.debug("Getting issue", { issueNumber: params.issueNumber });
|
|
1821
|
+
const apiConfig = this.getAPIConfig();
|
|
1822
|
+
const result = await getIssue(params, apiConfig);
|
|
1823
|
+
return result;
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Get API config for GitHub API calls
|
|
1827
|
+
*/
|
|
1828
|
+
getAPIConfig() {
|
|
1829
|
+
if (!this.owner || !this.repo) {
|
|
1830
|
+
throw new Error(
|
|
1831
|
+
"GitHub owner and repo must be configured. Either set GITHUB_OWNER/GITHUB_REPO or clone a repository first."
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
return {
|
|
1835
|
+
token: this.token,
|
|
1836
|
+
owner: this.owner,
|
|
1837
|
+
repo: this.repo
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
registerIntegration("github", GitHubIntegration);
|
|
1842
|
+
var CLIIntegration = class extends BaseIntegration {
|
|
1843
|
+
constructor(config) {
|
|
1844
|
+
super(config);
|
|
1845
|
+
this.logger.info("CLI integration initialized");
|
|
1846
|
+
}
|
|
1847
|
+
async execute(action, params) {
|
|
1848
|
+
const startTime = Date.now();
|
|
1849
|
+
try {
|
|
1850
|
+
let data;
|
|
1851
|
+
switch (action) {
|
|
1852
|
+
case "validate":
|
|
1853
|
+
data = await this.validate(params);
|
|
1854
|
+
break;
|
|
1855
|
+
default:
|
|
1856
|
+
throw new Error(`Unknown CLI action: ${action}`);
|
|
1857
|
+
}
|
|
1858
|
+
return {
|
|
1859
|
+
success: true,
|
|
1860
|
+
data,
|
|
1861
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1862
|
+
};
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
return this.handleError(action, error);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
async validate(params) {
|
|
1868
|
+
const { schema } = params;
|
|
1869
|
+
if (!schema || typeof schema !== "string") {
|
|
1870
|
+
throw new Error('validate requires a "schema" parameter (string)');
|
|
1871
|
+
}
|
|
1872
|
+
const tempDir = mkdtempSync(join(tmpdir(), "almadar-validate-"));
|
|
1873
|
+
const schemaPath = join(tempDir, "schema.orb");
|
|
1874
|
+
try {
|
|
1875
|
+
writeFileSync(schemaPath, schema, "utf-8");
|
|
1876
|
+
this.logger.debug("Validating schema", { path: schemaPath });
|
|
1877
|
+
const output = execSync(
|
|
1878
|
+
`npx @almadar/cli validate "${schemaPath}" --format=json`,
|
|
1879
|
+
{
|
|
1880
|
+
encoding: "utf-8",
|
|
1881
|
+
timeout: 3e4,
|
|
1882
|
+
cwd: tempDir,
|
|
1883
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1884
|
+
}
|
|
1885
|
+
);
|
|
1886
|
+
const result = JSON.parse(output.trim());
|
|
1887
|
+
return {
|
|
1888
|
+
valid: result.valid ?? true,
|
|
1889
|
+
errors: result.errors || [],
|
|
1890
|
+
warnings: result.warnings || [],
|
|
1891
|
+
summary: result.summary || ""
|
|
1892
|
+
};
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
const execError = error;
|
|
1895
|
+
if (execError.stdout) {
|
|
1896
|
+
try {
|
|
1897
|
+
const result = JSON.parse(execError.stdout.trim());
|
|
1898
|
+
return {
|
|
1899
|
+
valid: false,
|
|
1900
|
+
errors: result.errors || [],
|
|
1901
|
+
warnings: result.warnings || [],
|
|
1902
|
+
summary: result.summary || ""
|
|
1903
|
+
};
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
throw new Error(
|
|
1908
|
+
`CLI validation failed: ${execError.message || String(error)}`
|
|
1909
|
+
);
|
|
1910
|
+
} finally {
|
|
1911
|
+
try {
|
|
1912
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
1913
|
+
} catch {
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
registerIntegration("cli", CLIIntegration);
|
|
1919
|
+
|
|
1920
|
+
// src/integrations/redis/index.ts
|
|
1921
|
+
var RedisIntegration = class extends BaseIntegration {
|
|
1922
|
+
constructor(config) {
|
|
1923
|
+
super(config);
|
|
1924
|
+
this.store = /* @__PURE__ */ new Map();
|
|
1925
|
+
this.locks = /* @__PURE__ */ new Map();
|
|
1926
|
+
this.channels = /* @__PURE__ */ new Map();
|
|
1927
|
+
const redisUrl = config.env.REDIS_URL;
|
|
1928
|
+
if (redisUrl) {
|
|
1929
|
+
this.logger.warn(
|
|
1930
|
+
"REDIS_URL is configured but real Redis client is not yet implemented. Falling back to in-memory store.",
|
|
1931
|
+
{ redisUrl }
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
this.logger.info("Redis integration initialized (in-memory backend)");
|
|
1935
|
+
}
|
|
1936
|
+
async execute(action, params) {
|
|
1937
|
+
const validation = this.validateParams(action, params);
|
|
1938
|
+
if (!validation.valid) {
|
|
1939
|
+
return {
|
|
1940
|
+
success: false,
|
|
1941
|
+
error: {
|
|
1942
|
+
name: "IntegrationError",
|
|
1943
|
+
message: "Validation failed",
|
|
1944
|
+
code: "VALIDATION_ERROR",
|
|
1945
|
+
details: validation.errors
|
|
1946
|
+
},
|
|
1947
|
+
metadata: this.createMetadata(action, 0)
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
const startTime = Date.now();
|
|
1951
|
+
try {
|
|
1952
|
+
let data;
|
|
1953
|
+
switch (action) {
|
|
1954
|
+
case "get":
|
|
1955
|
+
data = await this.executeWithRetry(() => this.getKey(params));
|
|
1956
|
+
break;
|
|
1957
|
+
case "set":
|
|
1958
|
+
data = await this.executeWithRetry(() => this.setKey(params));
|
|
1959
|
+
break;
|
|
1960
|
+
case "delete":
|
|
1961
|
+
data = await this.executeWithRetry(() => this.deleteKey(params));
|
|
1962
|
+
break;
|
|
1963
|
+
case "lock":
|
|
1964
|
+
data = await this.executeWithRetry(() => this.acquireLock(params));
|
|
1965
|
+
break;
|
|
1966
|
+
case "unlock":
|
|
1967
|
+
data = await this.executeWithRetry(() => this.releaseLock(params));
|
|
1968
|
+
break;
|
|
1969
|
+
case "increment":
|
|
1970
|
+
data = await this.executeWithRetry(() => this.incrementKey(params));
|
|
1971
|
+
break;
|
|
1972
|
+
case "expire":
|
|
1973
|
+
data = await this.executeWithRetry(() => this.expireKey(params));
|
|
1974
|
+
break;
|
|
1975
|
+
case "publish":
|
|
1976
|
+
data = await this.executeWithRetry(() => this.publishMessage(params));
|
|
1977
|
+
break;
|
|
1978
|
+
case "subscribe":
|
|
1979
|
+
data = await this.executeWithRetry(
|
|
1980
|
+
() => this.subscribeChannel(params)
|
|
1981
|
+
);
|
|
1982
|
+
break;
|
|
1983
|
+
default:
|
|
1984
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
success: true,
|
|
1988
|
+
data,
|
|
1989
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1990
|
+
};
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
return this.handleError(action, error);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
// ---------------------------------------------------------------------------
|
|
1996
|
+
// Helpers
|
|
1997
|
+
// ---------------------------------------------------------------------------
|
|
1998
|
+
/** Remove expired entries on access and return whether a key is alive. */
|
|
1999
|
+
isAlive(key) {
|
|
2000
|
+
const entry = this.store.get(key);
|
|
2001
|
+
if (!entry) return false;
|
|
2002
|
+
if (entry.expiresAt !== void 0 && Date.now() > entry.expiresAt) {
|
|
2003
|
+
this.store.delete(key);
|
|
2004
|
+
return false;
|
|
2005
|
+
}
|
|
2006
|
+
return true;
|
|
2007
|
+
}
|
|
2008
|
+
/** Generate a unique lock identifier. */
|
|
2009
|
+
generateLockId() {
|
|
2010
|
+
return `lock_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
2011
|
+
}
|
|
2012
|
+
// ---------------------------------------------------------------------------
|
|
2013
|
+
// Actions
|
|
2014
|
+
// ---------------------------------------------------------------------------
|
|
2015
|
+
async getKey(params) {
|
|
2016
|
+
const key = params.key;
|
|
2017
|
+
this.logger.debug("Cache GET", { key });
|
|
2018
|
+
if (!this.isAlive(key)) {
|
|
2019
|
+
return { value: null };
|
|
2020
|
+
}
|
|
2021
|
+
const entry = this.store.get(key);
|
|
2022
|
+
return { value: entry ? entry.value : null };
|
|
2023
|
+
}
|
|
2024
|
+
async setKey(params) {
|
|
2025
|
+
const key = params.key;
|
|
2026
|
+
const value = params.value;
|
|
2027
|
+
const ttl = params.ttl;
|
|
2028
|
+
this.logger.debug("Cache SET", { key, ttl });
|
|
2029
|
+
const entry = { value };
|
|
2030
|
+
if (ttl !== void 0 && ttl > 0) {
|
|
2031
|
+
entry.expiresAt = Date.now() + ttl * 1e3;
|
|
2032
|
+
}
|
|
2033
|
+
this.store.set(key, entry);
|
|
2034
|
+
return { ok: true };
|
|
2035
|
+
}
|
|
2036
|
+
async deleteKey(params) {
|
|
2037
|
+
const key = params.key;
|
|
2038
|
+
this.logger.debug("Cache DELETE", { key });
|
|
2039
|
+
const existed = this.store.has(key);
|
|
2040
|
+
this.store.delete(key);
|
|
2041
|
+
return { deleted: existed };
|
|
2042
|
+
}
|
|
2043
|
+
async acquireLock(params) {
|
|
2044
|
+
const key = params.key;
|
|
2045
|
+
const ttl = params.ttl ?? 3e4;
|
|
2046
|
+
this.logger.debug("Cache LOCK", { key, ttl });
|
|
2047
|
+
const existingLockId = this.locks.get(key);
|
|
2048
|
+
if (existingLockId) {
|
|
2049
|
+
if (this.isAlive(`__lock:${key}`)) {
|
|
2050
|
+
return { acquired: false, lockId: "" };
|
|
2051
|
+
}
|
|
2052
|
+
this.locks.delete(key);
|
|
2053
|
+
this.store.delete(`__lock:${key}`);
|
|
2054
|
+
}
|
|
2055
|
+
const lockId = this.generateLockId();
|
|
2056
|
+
this.locks.set(key, lockId);
|
|
2057
|
+
this.store.set(`__lock:${key}`, {
|
|
2058
|
+
value: lockId,
|
|
2059
|
+
expiresAt: Date.now() + ttl
|
|
2060
|
+
});
|
|
2061
|
+
return { acquired: true, lockId };
|
|
2062
|
+
}
|
|
2063
|
+
async releaseLock(params) {
|
|
2064
|
+
const key = params.key;
|
|
2065
|
+
const lockId = params.lockId;
|
|
2066
|
+
this.logger.debug("Cache UNLOCK", { key, lockId });
|
|
2067
|
+
const currentLockId = this.locks.get(key);
|
|
2068
|
+
if (currentLockId !== lockId) {
|
|
2069
|
+
return { released: false };
|
|
2070
|
+
}
|
|
2071
|
+
this.locks.delete(key);
|
|
2072
|
+
this.store.delete(`__lock:${key}`);
|
|
2073
|
+
return { released: true };
|
|
2074
|
+
}
|
|
2075
|
+
async incrementKey(params) {
|
|
2076
|
+
const key = params.key;
|
|
2077
|
+
const by = params.by ?? 1;
|
|
2078
|
+
this.logger.debug("Cache INCR", { key, by });
|
|
2079
|
+
let current = 0;
|
|
2080
|
+
if (this.isAlive(key)) {
|
|
2081
|
+
const entry = this.store.get(key);
|
|
2082
|
+
if (entry) {
|
|
2083
|
+
current = typeof entry.value === "number" ? entry.value : 0;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
const newValue = current + by;
|
|
2087
|
+
const existing = this.store.get(key);
|
|
2088
|
+
this.store.set(key, {
|
|
2089
|
+
value: newValue,
|
|
2090
|
+
expiresAt: existing?.expiresAt
|
|
2091
|
+
});
|
|
2092
|
+
return { value: newValue };
|
|
2093
|
+
}
|
|
2094
|
+
async expireKey(params) {
|
|
2095
|
+
const key = params.key;
|
|
2096
|
+
const ttl = params.ttl;
|
|
2097
|
+
this.logger.debug("Cache EXPIRE", { key, ttl });
|
|
2098
|
+
if (!this.isAlive(key)) {
|
|
2099
|
+
return { set: false };
|
|
2100
|
+
}
|
|
2101
|
+
const entry = this.store.get(key);
|
|
2102
|
+
if (!entry) {
|
|
2103
|
+
return { set: false };
|
|
2104
|
+
}
|
|
2105
|
+
entry.expiresAt = Date.now() + ttl * 1e3;
|
|
2106
|
+
this.store.set(key, entry);
|
|
2107
|
+
return { set: true };
|
|
2108
|
+
}
|
|
2109
|
+
async publishMessage(params) {
|
|
2110
|
+
const channel = params.channel;
|
|
2111
|
+
const message = params.message;
|
|
2112
|
+
this.logger.debug("Cache PUBLISH", { channel });
|
|
2113
|
+
const subscribers = this.channels.get(channel);
|
|
2114
|
+
if (!subscribers || subscribers.size === 0) {
|
|
2115
|
+
return { receivers: 0 };
|
|
2116
|
+
}
|
|
2117
|
+
for (const callback of subscribers) {
|
|
2118
|
+
try {
|
|
2119
|
+
callback(message);
|
|
2120
|
+
} catch (err) {
|
|
2121
|
+
this.logger.error("Subscriber callback error", {
|
|
2122
|
+
channel,
|
|
2123
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
return { receivers: subscribers.size };
|
|
2128
|
+
}
|
|
2129
|
+
async subscribeChannel(params) {
|
|
2130
|
+
const channel = params.channel;
|
|
2131
|
+
this.logger.debug("Cache SUBSCRIBE", { channel });
|
|
2132
|
+
if (!this.channels.has(channel)) {
|
|
2133
|
+
this.channels.set(channel, /* @__PURE__ */ new Set());
|
|
2134
|
+
}
|
|
2135
|
+
return { subscribed: true };
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
registerIntegration("redis", RedisIntegration);
|
|
2139
|
+
|
|
2140
|
+
// src/integrations/queue/index.ts
|
|
2141
|
+
var QueueIntegration = class extends BaseIntegration {
|
|
2142
|
+
constructor(config) {
|
|
2143
|
+
super(config);
|
|
2144
|
+
/** Map from queue name to ordered list of job IDs */
|
|
2145
|
+
this.queues = /* @__PURE__ */ new Map();
|
|
2146
|
+
/** Map from job ID to job data */
|
|
2147
|
+
this.jobs = /* @__PURE__ */ new Map();
|
|
2148
|
+
this.logger.info("Queue integration initialized (in-memory backend)");
|
|
2149
|
+
}
|
|
2150
|
+
async execute(action, params) {
|
|
2151
|
+
const validation = this.validateParams(action, params);
|
|
2152
|
+
if (!validation.valid) {
|
|
2153
|
+
return {
|
|
2154
|
+
success: false,
|
|
2155
|
+
error: {
|
|
2156
|
+
name: "IntegrationError",
|
|
2157
|
+
message: "Validation failed",
|
|
2158
|
+
code: "VALIDATION_ERROR",
|
|
2159
|
+
details: validation.errors
|
|
2160
|
+
},
|
|
2161
|
+
metadata: this.createMetadata(action, 0)
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
const startTime = Date.now();
|
|
2165
|
+
try {
|
|
2166
|
+
let data;
|
|
2167
|
+
switch (action) {
|
|
2168
|
+
case "enqueue":
|
|
2169
|
+
data = await this.executeWithRetry(() => this.enqueue(params));
|
|
2170
|
+
break;
|
|
2171
|
+
case "dequeue":
|
|
2172
|
+
data = await this.executeWithRetry(() => this.dequeue(params));
|
|
2173
|
+
break;
|
|
2174
|
+
case "status":
|
|
2175
|
+
data = await this.executeWithRetry(() => this.status(params));
|
|
2176
|
+
break;
|
|
2177
|
+
case "complete":
|
|
2178
|
+
data = await this.executeWithRetry(() => this.complete(params));
|
|
2179
|
+
break;
|
|
2180
|
+
case "fail":
|
|
2181
|
+
data = await this.executeWithRetry(() => this.failJob(params));
|
|
2182
|
+
break;
|
|
2183
|
+
case "cancel":
|
|
2184
|
+
data = await this.executeWithRetry(() => this.cancel(params));
|
|
2185
|
+
break;
|
|
2186
|
+
case "size":
|
|
2187
|
+
data = await this.executeWithRetry(() => this.size(params));
|
|
2188
|
+
break;
|
|
2189
|
+
default:
|
|
2190
|
+
throw new Error(`Unknown action: ${action}`);
|
|
2191
|
+
}
|
|
2192
|
+
return {
|
|
2193
|
+
success: true,
|
|
2194
|
+
data,
|
|
2195
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
2196
|
+
};
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
return this.handleError(action, error);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
// ---------------------------------------------------------------------------
|
|
2202
|
+
// Helpers
|
|
2203
|
+
// ---------------------------------------------------------------------------
|
|
2204
|
+
/** Generate a unique job identifier. */
|
|
2205
|
+
generateJobId() {
|
|
2206
|
+
return `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2207
|
+
}
|
|
2208
|
+
/** Get or create the queue array for a given queue name. */
|
|
2209
|
+
getQueue(name) {
|
|
2210
|
+
let queue = this.queues.get(name);
|
|
2211
|
+
if (!queue) {
|
|
2212
|
+
queue = [];
|
|
2213
|
+
this.queues.set(name, queue);
|
|
2214
|
+
}
|
|
2215
|
+
return queue;
|
|
2216
|
+
}
|
|
2217
|
+
// ---------------------------------------------------------------------------
|
|
2218
|
+
// Actions
|
|
2219
|
+
// ---------------------------------------------------------------------------
|
|
2220
|
+
async enqueue(params) {
|
|
2221
|
+
const queueName = params.queue;
|
|
2222
|
+
const payload = params.payload;
|
|
2223
|
+
const delay = params.delay ?? 0;
|
|
2224
|
+
const priority = params.priority ?? 0;
|
|
2225
|
+
this.logger.debug("Queue ENQUEUE", { queue: queueName, delay, priority });
|
|
2226
|
+
const jobId = this.generateJobId();
|
|
2227
|
+
const job = {
|
|
2228
|
+
id: jobId,
|
|
2229
|
+
queue: queueName,
|
|
2230
|
+
payload,
|
|
2231
|
+
priority,
|
|
2232
|
+
status: "pending",
|
|
2233
|
+
enqueuedAt: Date.now(),
|
|
2234
|
+
delay
|
|
115
2235
|
};
|
|
2236
|
+
this.jobs.set(jobId, job);
|
|
2237
|
+
const queue = this.getQueue(queueName);
|
|
2238
|
+
let insertIndex = queue.length;
|
|
2239
|
+
for (let i = 0; i < queue.length; i++) {
|
|
2240
|
+
const existingJob = this.jobs.get(queue[i]);
|
|
2241
|
+
if (existingJob && existingJob.priority < priority) {
|
|
2242
|
+
insertIndex = i;
|
|
2243
|
+
break;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
queue.splice(insertIndex, 0, jobId);
|
|
2247
|
+
return { jobId, position: insertIndex };
|
|
116
2248
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
2249
|
+
async dequeue(params) {
|
|
2250
|
+
const queueName = params.queue;
|
|
2251
|
+
this.logger.debug("Queue DEQUEUE", { queue: queueName });
|
|
2252
|
+
const queue = this.getQueue(queueName);
|
|
2253
|
+
const now = Date.now();
|
|
2254
|
+
for (let i = 0; i < queue.length; i++) {
|
|
2255
|
+
const job = this.jobs.get(queue[i]);
|
|
2256
|
+
if (!job || job.status !== "pending") continue;
|
|
2257
|
+
const readyAt = job.enqueuedAt + job.delay;
|
|
2258
|
+
if (now < readyAt) continue;
|
|
2259
|
+
job.status = "processing";
|
|
2260
|
+
queue.splice(i, 1);
|
|
2261
|
+
return {
|
|
2262
|
+
job: {
|
|
2263
|
+
id: job.id,
|
|
2264
|
+
payload: job.payload,
|
|
2265
|
+
enqueuedAt: job.enqueuedAt,
|
|
2266
|
+
priority: job.priority
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
return { job: null };
|
|
2271
|
+
}
|
|
2272
|
+
async status(params) {
|
|
2273
|
+
const jobId = params.jobId;
|
|
2274
|
+
this.logger.debug("Queue STATUS", { jobId });
|
|
2275
|
+
const job = this.jobs.get(jobId);
|
|
2276
|
+
if (!job) {
|
|
2277
|
+
return { status: "pending", job: null };
|
|
2278
|
+
}
|
|
2279
|
+
return {
|
|
2280
|
+
status: job.status,
|
|
2281
|
+
job: {
|
|
2282
|
+
id: job.id,
|
|
2283
|
+
payload: job.payload,
|
|
2284
|
+
enqueuedAt: job.enqueuedAt,
|
|
2285
|
+
priority: job.priority
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
async complete(params) {
|
|
2290
|
+
const jobId = params.jobId;
|
|
2291
|
+
const result = params.result;
|
|
2292
|
+
this.logger.debug("Queue COMPLETE", { jobId });
|
|
2293
|
+
const job = this.jobs.get(jobId);
|
|
2294
|
+
if (!job || job.status !== "processing") {
|
|
2295
|
+
return { completed: false };
|
|
2296
|
+
}
|
|
2297
|
+
job.status = "completed";
|
|
2298
|
+
job.result = result;
|
|
2299
|
+
return { completed: true };
|
|
2300
|
+
}
|
|
2301
|
+
async failJob(params) {
|
|
2302
|
+
const jobId = params.jobId;
|
|
2303
|
+
const error = params.error;
|
|
2304
|
+
this.logger.debug("Queue FAIL", { jobId });
|
|
2305
|
+
const job = this.jobs.get(jobId);
|
|
2306
|
+
if (!job || job.status !== "processing") {
|
|
2307
|
+
return { failed: false };
|
|
2308
|
+
}
|
|
2309
|
+
job.status = "failed";
|
|
2310
|
+
job.error = error;
|
|
2311
|
+
return { failed: true };
|
|
2312
|
+
}
|
|
2313
|
+
async cancel(params) {
|
|
2314
|
+
const jobId = params.jobId;
|
|
2315
|
+
this.logger.debug("Queue CANCEL", { jobId });
|
|
2316
|
+
const job = this.jobs.get(jobId);
|
|
2317
|
+
if (!job || job.status !== "pending") {
|
|
2318
|
+
return { cancelled: false };
|
|
2319
|
+
}
|
|
2320
|
+
const queue = this.queues.get(job.queue);
|
|
2321
|
+
if (queue) {
|
|
2322
|
+
const idx = queue.indexOf(jobId);
|
|
2323
|
+
if (idx !== -1) {
|
|
2324
|
+
queue.splice(idx, 1);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
this.jobs.delete(jobId);
|
|
2328
|
+
return { cancelled: true };
|
|
2329
|
+
}
|
|
2330
|
+
async size(params) {
|
|
2331
|
+
const queueName = params.queue;
|
|
2332
|
+
this.logger.debug("Queue SIZE", { queue: queueName });
|
|
2333
|
+
let pending = 0;
|
|
2334
|
+
let processing = 0;
|
|
2335
|
+
for (const job of this.jobs.values()) {
|
|
2336
|
+
if (job.queue !== queueName) continue;
|
|
2337
|
+
if (job.status === "pending") pending++;
|
|
2338
|
+
if (job.status === "processing") processing++;
|
|
2339
|
+
}
|
|
2340
|
+
return { size: pending + processing, pending, processing };
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
registerIntegration("queue", QueueIntegration);
|
|
2344
|
+
|
|
2345
|
+
// src/integrations/otel/index.ts
|
|
2346
|
+
function randomHexId(bytes) {
|
|
2347
|
+
const arr = [];
|
|
2348
|
+
for (let i = 0; i < bytes; i++) {
|
|
2349
|
+
arr.push(Math.floor(Math.random() * 256).toString(16).padStart(2, "0"));
|
|
2350
|
+
}
|
|
2351
|
+
return arr.join("");
|
|
2352
|
+
}
|
|
2353
|
+
var OtelIntegration = class extends BaseIntegration {
|
|
2354
|
+
constructor(config) {
|
|
2355
|
+
super(config);
|
|
2356
|
+
this.spans = /* @__PURE__ */ new Map();
|
|
2357
|
+
this.metrics = /* @__PURE__ */ new Map();
|
|
2358
|
+
this.logger.info("OTel integration initialized (in-memory backend)");
|
|
2359
|
+
}
|
|
2360
|
+
async execute(action, params) {
|
|
2361
|
+
const validation = this.validateParams(action, params);
|
|
2362
|
+
if (!validation.valid) {
|
|
2363
|
+
return {
|
|
2364
|
+
success: false,
|
|
2365
|
+
error: {
|
|
2366
|
+
name: "IntegrationError",
|
|
2367
|
+
message: "Validation failed",
|
|
2368
|
+
code: "VALIDATION_ERROR",
|
|
2369
|
+
details: validation.errors
|
|
2370
|
+
},
|
|
2371
|
+
metadata: this.createMetadata(action, 0)
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
const startTime = Date.now();
|
|
2375
|
+
try {
|
|
2376
|
+
let data;
|
|
2377
|
+
switch (action) {
|
|
2378
|
+
case "startSpan":
|
|
2379
|
+
data = await this.executeWithRetry(() => this.startSpan(params));
|
|
2380
|
+
break;
|
|
2381
|
+
case "endSpan":
|
|
2382
|
+
data = await this.executeWithRetry(() => this.endSpan(params));
|
|
2383
|
+
break;
|
|
2384
|
+
case "addEvent":
|
|
2385
|
+
data = await this.executeWithRetry(() => this.addEvent(params));
|
|
2386
|
+
break;
|
|
2387
|
+
case "recordMetric":
|
|
2388
|
+
data = await this.executeWithRetry(() => this.recordMetric(params));
|
|
2389
|
+
break;
|
|
2390
|
+
case "getSpan":
|
|
2391
|
+
data = await this.executeWithRetry(() => this.getSpan(params));
|
|
2392
|
+
break;
|
|
2393
|
+
case "getMetrics":
|
|
2394
|
+
data = await this.executeWithRetry(() => this.getAllMetrics());
|
|
2395
|
+
break;
|
|
2396
|
+
default:
|
|
2397
|
+
throw new Error(`Unknown action: ${action}`);
|
|
2398
|
+
}
|
|
2399
|
+
return {
|
|
2400
|
+
success: true,
|
|
2401
|
+
data,
|
|
2402
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
2403
|
+
};
|
|
2404
|
+
} catch (error) {
|
|
2405
|
+
return this.handleError(action, error);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
// ---------------------------------------------------------------------------
|
|
2409
|
+
// Actions
|
|
2410
|
+
// ---------------------------------------------------------------------------
|
|
2411
|
+
async startSpan(params) {
|
|
2412
|
+
const name = params.name;
|
|
2413
|
+
const attributes = params.attributes ?? {};
|
|
2414
|
+
const spanId = randomHexId(8);
|
|
2415
|
+
const traceId = params.traceId ?? randomHexId(16);
|
|
2416
|
+
const span = {
|
|
2417
|
+
spanId,
|
|
2418
|
+
traceId,
|
|
2419
|
+
name,
|
|
2420
|
+
startTime: Date.now(),
|
|
2421
|
+
attributes,
|
|
2422
|
+
events: []
|
|
2423
|
+
};
|
|
2424
|
+
this.spans.set(spanId, span);
|
|
2425
|
+
this.logger.debug("Span started", { spanId, traceId, name });
|
|
2426
|
+
return { spanId, traceId };
|
|
2427
|
+
}
|
|
2428
|
+
async endSpan(params) {
|
|
2429
|
+
const spanId = params.spanId;
|
|
2430
|
+
const status = params.status;
|
|
2431
|
+
const span = this.spans.get(spanId);
|
|
2432
|
+
if (!span) {
|
|
2433
|
+
this.logger.warn("endSpan called for unknown span", { spanId });
|
|
2434
|
+
return { ended: false };
|
|
2435
|
+
}
|
|
2436
|
+
span.endTime = Date.now();
|
|
2437
|
+
if (status) {
|
|
2438
|
+
span.status = status;
|
|
2439
|
+
}
|
|
2440
|
+
this.logger.debug("Span ended", { spanId, status, duration: span.endTime - span.startTime });
|
|
2441
|
+
return { ended: true };
|
|
2442
|
+
}
|
|
2443
|
+
async addEvent(params) {
|
|
2444
|
+
const spanId = params.spanId;
|
|
2445
|
+
const name = params.name;
|
|
2446
|
+
const attributes = params.attributes;
|
|
2447
|
+
const span = this.spans.get(spanId);
|
|
2448
|
+
if (!span) {
|
|
2449
|
+
this.logger.warn("addEvent called for unknown span", { spanId });
|
|
2450
|
+
return { added: false };
|
|
2451
|
+
}
|
|
2452
|
+
span.events.push({
|
|
2453
|
+
name,
|
|
2454
|
+
timestamp: Date.now(),
|
|
2455
|
+
attributes
|
|
2456
|
+
});
|
|
2457
|
+
this.logger.debug("Event added to span", { spanId, eventName: name });
|
|
2458
|
+
return { added: true };
|
|
2459
|
+
}
|
|
2460
|
+
async recordMetric(params) {
|
|
2461
|
+
const name = params.name;
|
|
2462
|
+
const value = params.value;
|
|
2463
|
+
const type = params.type ?? "counter";
|
|
2464
|
+
const labels = params.labels ?? {};
|
|
2465
|
+
const existing = this.metrics.get(name);
|
|
2466
|
+
if (existing) {
|
|
2467
|
+
switch (type) {
|
|
2468
|
+
case "counter":
|
|
2469
|
+
existing.value += value;
|
|
2470
|
+
break;
|
|
2471
|
+
case "gauge":
|
|
2472
|
+
existing.value = value;
|
|
2473
|
+
break;
|
|
2474
|
+
case "histogram":
|
|
2475
|
+
existing.values.push(value);
|
|
2476
|
+
break;
|
|
2477
|
+
}
|
|
2478
|
+
existing.labels = labels;
|
|
2479
|
+
} else {
|
|
2480
|
+
this.metrics.set(name, {
|
|
2481
|
+
name,
|
|
2482
|
+
type,
|
|
2483
|
+
value: type === "histogram" ? 0 : value,
|
|
2484
|
+
values: type === "histogram" ? [value] : [],
|
|
2485
|
+
labels
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
this.logger.debug("Metric recorded", { name, value, type });
|
|
2489
|
+
return { recorded: true };
|
|
2490
|
+
}
|
|
2491
|
+
async getSpan(params) {
|
|
2492
|
+
const spanId = params.spanId;
|
|
2493
|
+
const span = this.spans.get(spanId);
|
|
2494
|
+
return span ?? null;
|
|
2495
|
+
}
|
|
2496
|
+
async getAllMetrics() {
|
|
2497
|
+
const result = {};
|
|
2498
|
+
for (const [key, metric] of this.metrics) {
|
|
2499
|
+
result[key] = metric;
|
|
2500
|
+
}
|
|
2501
|
+
return result;
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
registerIntegration("otel", OtelIntegration);
|
|
2505
|
+
|
|
2506
|
+
// src/integrations/oauth/index.ts
|
|
2507
|
+
var PROVIDER_AUTH_URLS = {
|
|
2508
|
+
google: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
2509
|
+
github: "https://github.com/login/oauth/authorize",
|
|
2510
|
+
auth0: "https://auth.example.com/authorize"
|
|
2511
|
+
};
|
|
2512
|
+
var OAuthIntegration = class extends BaseIntegration {
|
|
2513
|
+
constructor(config) {
|
|
2514
|
+
super(config);
|
|
2515
|
+
/** Maps state token -> provider for pending authorization flows */
|
|
2516
|
+
this.states = /* @__PURE__ */ new Map();
|
|
2517
|
+
/** Maps access token -> token set */
|
|
2518
|
+
this.tokens = /* @__PURE__ */ new Map();
|
|
2519
|
+
/** Maps refresh token -> access token for refresh lookups */
|
|
2520
|
+
this.refreshIndex = /* @__PURE__ */ new Map();
|
|
2521
|
+
/** Maps access token -> mock user session */
|
|
2522
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
2523
|
+
this.logger.info("OAuth integration initialized (mock backend)");
|
|
2524
|
+
}
|
|
2525
|
+
async execute(action, params) {
|
|
2526
|
+
const validation = this.validateParams(action, params);
|
|
2527
|
+
if (!validation.valid) {
|
|
2528
|
+
return {
|
|
2529
|
+
success: false,
|
|
2530
|
+
error: {
|
|
2531
|
+
name: "IntegrationError",
|
|
2532
|
+
message: "Validation failed",
|
|
2533
|
+
code: "VALIDATION_ERROR",
|
|
2534
|
+
details: validation.errors
|
|
2535
|
+
},
|
|
2536
|
+
metadata: this.createMetadata(action, 0)
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
const startTime = Date.now();
|
|
2540
|
+
try {
|
|
2541
|
+
let data;
|
|
2542
|
+
switch (action) {
|
|
2543
|
+
case "authorize":
|
|
2544
|
+
data = await this.executeWithRetry(() => this.authorize(params));
|
|
2545
|
+
break;
|
|
2546
|
+
case "token":
|
|
2547
|
+
data = await this.executeWithRetry(() => this.token(params));
|
|
2548
|
+
break;
|
|
2549
|
+
case "refresh":
|
|
2550
|
+
data = await this.executeWithRetry(() => this.refresh(params));
|
|
2551
|
+
break;
|
|
2552
|
+
case "revoke":
|
|
2553
|
+
data = await this.executeWithRetry(() => this.revoke(params));
|
|
2554
|
+
break;
|
|
2555
|
+
case "userinfo":
|
|
2556
|
+
data = await this.executeWithRetry(() => this.userinfo(params));
|
|
2557
|
+
break;
|
|
2558
|
+
default:
|
|
2559
|
+
throw new Error(`Unknown action: ${action}`);
|
|
2560
|
+
}
|
|
2561
|
+
return {
|
|
2562
|
+
success: true,
|
|
2563
|
+
data,
|
|
2564
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
2565
|
+
};
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
return this.handleError(action, error);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
// ---------------------------------------------------------------------------
|
|
2571
|
+
// Helpers
|
|
2572
|
+
// ---------------------------------------------------------------------------
|
|
2573
|
+
/** Generate a random hex token of the given byte length. */
|
|
2574
|
+
generateToken(bytes = 32) {
|
|
2575
|
+
const chars = "abcdef0123456789";
|
|
2576
|
+
let result = "";
|
|
2577
|
+
for (let i = 0; i < bytes * 2; i++) {
|
|
2578
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
2579
|
+
}
|
|
2580
|
+
return result;
|
|
2581
|
+
}
|
|
2582
|
+
/** Generate a mock user profile from a provider and access token. */
|
|
2583
|
+
generateMockUser(provider, sub) {
|
|
2584
|
+
return {
|
|
2585
|
+
sub,
|
|
2586
|
+
email: `user-${sub.slice(0, 8)}@${provider}.example.com`,
|
|
2587
|
+
name: `Mock User (${provider})`,
|
|
2588
|
+
picture: `https://${provider}.example.com/avatar/${sub.slice(0, 8)}.png`
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
// ---------------------------------------------------------------------------
|
|
2592
|
+
// Actions
|
|
2593
|
+
// ---------------------------------------------------------------------------
|
|
2594
|
+
async authorize(params) {
|
|
2595
|
+
const provider = params.provider;
|
|
2596
|
+
const scopes = params.scopes;
|
|
2597
|
+
const redirectUri = params.redirectUri;
|
|
2598
|
+
this.logger.debug("OAuth AUTHORIZE", { provider, scopes: scopes.join(", "), redirectUri });
|
|
2599
|
+
const state = this.generateToken(16);
|
|
2600
|
+
this.states.set(state, provider);
|
|
2601
|
+
const baseUrl = PROVIDER_AUTH_URLS[provider];
|
|
2602
|
+
const queryParams = new URLSearchParams({
|
|
2603
|
+
response_type: "code",
|
|
2604
|
+
scope: scopes.join(" "),
|
|
2605
|
+
redirect_uri: redirectUri,
|
|
2606
|
+
state,
|
|
2607
|
+
client_id: this.config.env.CLIENT_ID ?? "mock-client-id"
|
|
2608
|
+
});
|
|
2609
|
+
const authUrl = `${baseUrl}?${queryParams.toString()}`;
|
|
2610
|
+
return { authUrl, state };
|
|
2611
|
+
}
|
|
2612
|
+
async token(params) {
|
|
2613
|
+
const code = params.code;
|
|
2614
|
+
const state = params.state;
|
|
2615
|
+
this.logger.debug("OAuth TOKEN", { code, state });
|
|
2616
|
+
const provider = this.states.get(state);
|
|
2617
|
+
if (!provider) {
|
|
2618
|
+
throw new Error(
|
|
2619
|
+
`Invalid or expired state token: ${state}`
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
this.states.delete(state);
|
|
2623
|
+
const accessToken = this.generateToken(32);
|
|
2624
|
+
const refreshToken = this.generateToken(32);
|
|
2625
|
+
const expiresIn = 3600;
|
|
2626
|
+
const tokenSet = {
|
|
2627
|
+
accessToken,
|
|
2628
|
+
refreshToken,
|
|
2629
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
2630
|
+
};
|
|
2631
|
+
this.tokens.set(accessToken, tokenSet);
|
|
2632
|
+
this.refreshIndex.set(refreshToken, accessToken);
|
|
2633
|
+
const sub = this.generateToken(8);
|
|
2634
|
+
this.sessions.set(accessToken, this.generateMockUser(provider, sub));
|
|
2635
|
+
return { accessToken, refreshToken, expiresIn, tokenType: "bearer" };
|
|
2636
|
+
}
|
|
2637
|
+
async refresh(params) {
|
|
2638
|
+
const refreshToken = params.refreshToken;
|
|
2639
|
+
this.logger.debug("OAuth REFRESH", { refreshToken: refreshToken.slice(0, 8) + "..." });
|
|
2640
|
+
const oldAccessToken = this.refreshIndex.get(refreshToken);
|
|
2641
|
+
if (!oldAccessToken) {
|
|
2642
|
+
throw new Error("Invalid refresh token");
|
|
2643
|
+
}
|
|
2644
|
+
const userInfo = this.sessions.get(oldAccessToken);
|
|
2645
|
+
this.tokens.delete(oldAccessToken);
|
|
2646
|
+
this.sessions.delete(oldAccessToken);
|
|
2647
|
+
const newAccessToken = this.generateToken(32);
|
|
2648
|
+
const expiresIn = 3600;
|
|
2649
|
+
const tokenSet = {
|
|
2650
|
+
accessToken: newAccessToken,
|
|
2651
|
+
refreshToken,
|
|
2652
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
2653
|
+
};
|
|
2654
|
+
this.tokens.set(newAccessToken, tokenSet);
|
|
2655
|
+
this.refreshIndex.set(refreshToken, newAccessToken);
|
|
2656
|
+
if (userInfo) {
|
|
2657
|
+
this.sessions.set(newAccessToken, userInfo);
|
|
2658
|
+
}
|
|
2659
|
+
return { accessToken: newAccessToken, expiresIn };
|
|
2660
|
+
}
|
|
2661
|
+
async revoke(params) {
|
|
2662
|
+
const token = params.token;
|
|
2663
|
+
this.logger.debug("OAuth REVOKE", { token: token.slice(0, 8) + "..." });
|
|
2664
|
+
const tokenSet = this.tokens.get(token);
|
|
2665
|
+
if (tokenSet) {
|
|
2666
|
+
this.refreshIndex.delete(tokenSet.refreshToken);
|
|
2667
|
+
this.tokens.delete(token);
|
|
2668
|
+
this.sessions.delete(token);
|
|
2669
|
+
return { revoked: true };
|
|
2670
|
+
}
|
|
2671
|
+
const accessToken = this.refreshIndex.get(token);
|
|
2672
|
+
if (accessToken) {
|
|
2673
|
+
this.tokens.delete(accessToken);
|
|
2674
|
+
this.sessions.delete(accessToken);
|
|
2675
|
+
this.refreshIndex.delete(token);
|
|
2676
|
+
return { revoked: true };
|
|
2677
|
+
}
|
|
2678
|
+
return { revoked: false };
|
|
2679
|
+
}
|
|
2680
|
+
async userinfo(params) {
|
|
2681
|
+
const accessToken = params.accessToken;
|
|
2682
|
+
this.logger.debug("OAuth USERINFO", { accessToken: accessToken.slice(0, 8) + "..." });
|
|
2683
|
+
const tokenSet = this.tokens.get(accessToken);
|
|
2684
|
+
if (!tokenSet) {
|
|
2685
|
+
throw new Error("Invalid access token");
|
|
2686
|
+
}
|
|
2687
|
+
if (Date.now() > tokenSet.expiresAt) {
|
|
2688
|
+
this.tokens.delete(accessToken);
|
|
2689
|
+
this.sessions.delete(accessToken);
|
|
2690
|
+
throw new Error("Access token expired");
|
|
2691
|
+
}
|
|
2692
|
+
const userInfo = this.sessions.get(accessToken);
|
|
2693
|
+
if (!userInfo) {
|
|
2694
|
+
throw new Error("No session found for access token");
|
|
2695
|
+
}
|
|
2696
|
+
return userInfo;
|
|
2697
|
+
}
|
|
2698
|
+
};
|
|
2699
|
+
registerIntegration("oauth", OAuthIntegration);
|
|
2700
|
+
|
|
2701
|
+
// src/integrations/storage/index.ts
|
|
2702
|
+
var StorageIntegration = class extends BaseIntegration {
|
|
2703
|
+
constructor(config) {
|
|
2704
|
+
super(config);
|
|
2705
|
+
this.objects = /* @__PURE__ */ new Map();
|
|
2706
|
+
const storageUrl = config.env.STORAGE_URL;
|
|
2707
|
+
if (storageUrl) {
|
|
2708
|
+
this.logger.warn(
|
|
2709
|
+
"STORAGE_URL is configured but real storage client is not yet implemented. Falling back to in-memory store.",
|
|
2710
|
+
{ storageUrl }
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
this.logger.info("Storage integration initialized (in-memory backend)");
|
|
2714
|
+
}
|
|
2715
|
+
async execute(action, params) {
|
|
2716
|
+
const validation = this.validateParams(action, params);
|
|
2717
|
+
if (!validation.valid) {
|
|
2718
|
+
return {
|
|
2719
|
+
success: false,
|
|
2720
|
+
error: {
|
|
2721
|
+
name: "IntegrationError",
|
|
2722
|
+
message: "Validation failed",
|
|
2723
|
+
code: "VALIDATION_ERROR",
|
|
2724
|
+
details: validation.errors
|
|
2725
|
+
},
|
|
2726
|
+
metadata: this.createMetadata(action, 0)
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
const startTime = Date.now();
|
|
2730
|
+
try {
|
|
2731
|
+
let data;
|
|
2732
|
+
switch (action) {
|
|
2733
|
+
case "upload":
|
|
2734
|
+
data = await this.executeWithRetry(() => this.upload(params));
|
|
2735
|
+
break;
|
|
2736
|
+
case "download":
|
|
2737
|
+
data = await this.executeWithRetry(() => this.download(params));
|
|
2738
|
+
break;
|
|
2739
|
+
case "list":
|
|
2740
|
+
data = await this.executeWithRetry(() => this.list(params));
|
|
2741
|
+
break;
|
|
2742
|
+
case "delete":
|
|
2743
|
+
data = await this.executeWithRetry(() => this.deleteObject(params));
|
|
2744
|
+
break;
|
|
2745
|
+
case "getSignedUrl":
|
|
2746
|
+
data = await this.executeWithRetry(() => this.getSignedUrl(params));
|
|
2747
|
+
break;
|
|
2748
|
+
default:
|
|
2749
|
+
throw new Error(`Unknown action: ${action}`);
|
|
2750
|
+
}
|
|
2751
|
+
return {
|
|
2752
|
+
success: true,
|
|
2753
|
+
data,
|
|
2754
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
2755
|
+
};
|
|
2756
|
+
} catch (error) {
|
|
2757
|
+
return this.handleError(action, error);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
// ---------------------------------------------------------------------------
|
|
2761
|
+
// Helpers
|
|
2762
|
+
// ---------------------------------------------------------------------------
|
|
2763
|
+
/** Build a composite key from bucket and object key. */
|
|
2764
|
+
compositeKey(bucket, key) {
|
|
2765
|
+
return `${bucket}/${key}`;
|
|
2766
|
+
}
|
|
2767
|
+
/** Generate a deterministic etag from content. */
|
|
2768
|
+
generateEtag(content) {
|
|
2769
|
+
const raw = typeof content === "string" ? content : JSON.stringify(content);
|
|
2770
|
+
let hash = 0;
|
|
2771
|
+
for (let i = 0; i < raw.length; i++) {
|
|
2772
|
+
const ch = raw.charCodeAt(i);
|
|
2773
|
+
hash = (hash << 5) - hash + ch | 0;
|
|
2774
|
+
}
|
|
2775
|
+
return `"${Math.abs(hash).toString(16).padStart(8, "0")}"`;
|
|
2776
|
+
}
|
|
2777
|
+
/** Compute the byte size of content. */
|
|
2778
|
+
computeSize(content) {
|
|
2779
|
+
if (typeof content === "string") {
|
|
2780
|
+
return new TextEncoder().encode(content).byteLength;
|
|
2781
|
+
}
|
|
2782
|
+
return JSON.stringify(content).length;
|
|
2783
|
+
}
|
|
2784
|
+
// ---------------------------------------------------------------------------
|
|
2785
|
+
// Actions
|
|
2786
|
+
// ---------------------------------------------------------------------------
|
|
2787
|
+
async upload(params) {
|
|
2788
|
+
const bucket = params.bucket;
|
|
2789
|
+
const key = params.key;
|
|
2790
|
+
const content = params.content;
|
|
2791
|
+
const contentType = params.contentType ?? "application/octet-stream";
|
|
2792
|
+
const metadata = params.metadata ?? {};
|
|
2793
|
+
this.logger.debug("Storage UPLOAD", { bucket, key, contentType });
|
|
2794
|
+
const size = this.computeSize(content);
|
|
2795
|
+
const etag = this.generateEtag(content);
|
|
2796
|
+
const obj = {
|
|
2797
|
+
content,
|
|
2798
|
+
contentType,
|
|
2799
|
+
size,
|
|
2800
|
+
metadata,
|
|
2801
|
+
lastModified: Date.now(),
|
|
2802
|
+
etag
|
|
2803
|
+
};
|
|
2804
|
+
this.objects.set(this.compositeKey(bucket, key), obj);
|
|
2805
|
+
return { key, bucket, size, etag };
|
|
2806
|
+
}
|
|
2807
|
+
async download(params) {
|
|
2808
|
+
const bucket = params.bucket;
|
|
2809
|
+
const key = params.key;
|
|
2810
|
+
this.logger.debug("Storage DOWNLOAD", { bucket, key });
|
|
2811
|
+
const obj = this.objects.get(this.compositeKey(bucket, key));
|
|
2812
|
+
if (!obj) {
|
|
2813
|
+
throw new Error(`Object not found: ${bucket}/${key}`);
|
|
2814
|
+
}
|
|
2815
|
+
return {
|
|
2816
|
+
content: obj.content,
|
|
2817
|
+
contentType: obj.contentType,
|
|
2818
|
+
size: obj.size,
|
|
2819
|
+
metadata: obj.metadata
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
async list(params) {
|
|
2823
|
+
const bucket = params.bucket;
|
|
2824
|
+
const prefix = params.prefix ?? "";
|
|
2825
|
+
const maxKeys = params.maxKeys ?? 1e3;
|
|
2826
|
+
this.logger.debug("Storage LIST", { bucket, prefix, maxKeys });
|
|
2827
|
+
const bucketPrefix = `${bucket}/`;
|
|
2828
|
+
const fullPrefix = `${bucket}/${prefix}`;
|
|
2829
|
+
const results = [];
|
|
2830
|
+
for (const [compositeKey, obj] of this.objects) {
|
|
2831
|
+
if (!compositeKey.startsWith(fullPrefix)) continue;
|
|
2832
|
+
const objectKey = compositeKey.slice(bucketPrefix.length);
|
|
2833
|
+
results.push({
|
|
2834
|
+
key: objectKey,
|
|
2835
|
+
size: obj.size,
|
|
2836
|
+
lastModified: obj.lastModified
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
results.sort((a, b) => a.key.localeCompare(b.key));
|
|
2840
|
+
const truncated = results.length > maxKeys;
|
|
2841
|
+
return {
|
|
2842
|
+
keys: results.slice(0, maxKeys),
|
|
2843
|
+
truncated
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
async deleteObject(params) {
|
|
2847
|
+
const bucket = params.bucket;
|
|
2848
|
+
const key = params.key;
|
|
2849
|
+
this.logger.debug("Storage DELETE", { bucket, key });
|
|
2850
|
+
const existed = this.objects.has(this.compositeKey(bucket, key));
|
|
2851
|
+
this.objects.delete(this.compositeKey(bucket, key));
|
|
2852
|
+
return { deleted: existed };
|
|
2853
|
+
}
|
|
2854
|
+
async getSignedUrl(params) {
|
|
2855
|
+
const bucket = params.bucket;
|
|
2856
|
+
const key = params.key;
|
|
2857
|
+
const expiresIn = params.expiresIn ?? 3600;
|
|
2858
|
+
const operation = params.operation ?? "get";
|
|
2859
|
+
this.logger.debug("Storage GET_SIGNED_URL", { bucket, key, expiresIn, operation });
|
|
2860
|
+
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
2861
|
+
const token = Math.random().toString(36).slice(2, 18);
|
|
2862
|
+
const url = `https://storage.mock.local/${bucket}/${key}?X-Amz-Algorithm=MOCK-HMAC-SHA256&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host&X-Amz-Signature=${token}&operation=${operation}`;
|
|
2863
|
+
return { url, expiresAt };
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
registerIntegration("storage", StorageIntegration);
|
|
2867
|
+
|
|
2868
|
+
// src/integrations/docker/index.ts
|
|
2869
|
+
var DockerIntegration = class extends BaseIntegration {
|
|
2870
|
+
constructor(config) {
|
|
2871
|
+
super(config);
|
|
2872
|
+
this.containers = /* @__PURE__ */ new Map();
|
|
2873
|
+
this.images = /* @__PURE__ */ new Map();
|
|
2874
|
+
const dockerHost = config.env.DOCKER_HOST;
|
|
2875
|
+
if (dockerHost) {
|
|
2876
|
+
this.logger.warn(
|
|
2877
|
+
"DOCKER_HOST is configured but real Docker client is not yet implemented. Falling back to in-memory simulation.",
|
|
2878
|
+
{ dockerHost }
|
|
2879
|
+
);
|
|
2880
|
+
}
|
|
2881
|
+
this.logger.info("Docker integration initialized (in-memory simulation)");
|
|
2882
|
+
}
|
|
2883
|
+
async execute(action, params) {
|
|
2884
|
+
const validation = this.validateParams(action, params);
|
|
2885
|
+
if (!validation.valid) {
|
|
2886
|
+
return {
|
|
2887
|
+
success: false,
|
|
2888
|
+
error: {
|
|
2889
|
+
name: "IntegrationError",
|
|
2890
|
+
message: "Validation failed",
|
|
2891
|
+
code: "VALIDATION_ERROR",
|
|
2892
|
+
details: validation.errors
|
|
2893
|
+
},
|
|
2894
|
+
metadata: this.createMetadata(action, 0)
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
const startTime = Date.now();
|
|
2898
|
+
try {
|
|
2899
|
+
let data;
|
|
2900
|
+
switch (action) {
|
|
2901
|
+
case "build":
|
|
2902
|
+
data = await this.executeWithRetry(() => this.build(params));
|
|
2903
|
+
break;
|
|
2904
|
+
case "run":
|
|
2905
|
+
data = await this.executeWithRetry(() => this.run(params));
|
|
2906
|
+
break;
|
|
2907
|
+
case "stop":
|
|
2908
|
+
data = await this.executeWithRetry(() => this.stop(params));
|
|
2909
|
+
break;
|
|
2910
|
+
case "remove":
|
|
2911
|
+
data = await this.executeWithRetry(() => this.removeContainer(params));
|
|
2912
|
+
break;
|
|
2913
|
+
case "logs":
|
|
2914
|
+
data = await this.executeWithRetry(() => this.logs(params));
|
|
2915
|
+
break;
|
|
2916
|
+
case "status":
|
|
2917
|
+
data = await this.executeWithRetry(() => this.status(params));
|
|
2918
|
+
break;
|
|
2919
|
+
case "list":
|
|
2920
|
+
data = await this.executeWithRetry(() => this.list(params));
|
|
2921
|
+
break;
|
|
2922
|
+
default:
|
|
2923
|
+
throw new Error(`Unknown action: ${action}`);
|
|
2924
|
+
}
|
|
2925
|
+
return {
|
|
2926
|
+
success: true,
|
|
2927
|
+
data,
|
|
2928
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
2929
|
+
};
|
|
2930
|
+
} catch (error) {
|
|
2931
|
+
return this.handleError(action, error);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
// ---------------------------------------------------------------------------
|
|
2935
|
+
// Helpers
|
|
2936
|
+
// ---------------------------------------------------------------------------
|
|
2937
|
+
/** Generate a random hex container/image ID. */
|
|
2938
|
+
generateId() {
|
|
2939
|
+
const segments = [];
|
|
2940
|
+
for (let i = 0; i < 16; i++) {
|
|
2941
|
+
segments.push(Math.floor(Math.random() * 256).toString(16).padStart(2, "0"));
|
|
2942
|
+
}
|
|
2943
|
+
return segments.join("");
|
|
2944
|
+
}
|
|
2945
|
+
/** Find a container by ID (prefix match supported). */
|
|
2946
|
+
findContainer(containerId) {
|
|
2947
|
+
const exact = this.containers.get(containerId);
|
|
2948
|
+
if (exact) return exact;
|
|
2949
|
+
for (const [id, container] of this.containers) {
|
|
2950
|
+
if (id.startsWith(containerId)) {
|
|
2951
|
+
return container;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
return void 0;
|
|
2955
|
+
}
|
|
2956
|
+
/** Generate realistic log lines for a container. */
|
|
2957
|
+
generateLogLines(container, count) {
|
|
2958
|
+
const lines = [...container.logs];
|
|
2959
|
+
const baseTime = container.startedAt ?? container.createdAt;
|
|
2960
|
+
while (lines.length < count) {
|
|
2961
|
+
const ts = new Date(baseTime + lines.length * 1e3).toISOString();
|
|
2962
|
+
lines.push(`${ts} [info] Container ${container.name} \u2014 heartbeat #${lines.length + 1}`);
|
|
2963
|
+
}
|
|
2964
|
+
return lines;
|
|
2965
|
+
}
|
|
2966
|
+
// ---------------------------------------------------------------------------
|
|
2967
|
+
// Actions
|
|
2968
|
+
// ---------------------------------------------------------------------------
|
|
2969
|
+
async build(params) {
|
|
2970
|
+
const dockerfile = params.dockerfile ?? "Dockerfile";
|
|
2971
|
+
const tag = params.tag;
|
|
2972
|
+
const context = params.context ?? ".";
|
|
2973
|
+
const buildArgs = params.buildArgs ?? {};
|
|
2974
|
+
this.logger.debug("Docker BUILD", { dockerfile, tag, context, buildArgs });
|
|
2975
|
+
const imageId = `sha256:${this.generateId()}`;
|
|
2976
|
+
const size = 15e7 + Math.floor(Math.random() * 35e7);
|
|
2977
|
+
const buildTime = 2e3 + Math.floor(Math.random() * 8e3);
|
|
2978
|
+
const image = {
|
|
2979
|
+
id: imageId,
|
|
2980
|
+
tag,
|
|
2981
|
+
dockerfile,
|
|
2982
|
+
size,
|
|
2983
|
+
createdAt: Date.now()
|
|
2984
|
+
};
|
|
2985
|
+
this.images.set(tag, image);
|
|
2986
|
+
return { imageId, tag, size, buildTime };
|
|
2987
|
+
}
|
|
2988
|
+
async run(params) {
|
|
2989
|
+
const image = params.image;
|
|
2990
|
+
const name = params.name ?? `container-${this.generateId().slice(0, 12)}`;
|
|
2991
|
+
const rawPorts = params.ports ?? [];
|
|
2992
|
+
const env = params.env ?? {};
|
|
2993
|
+
const rawVolumes = params.volumes ?? [];
|
|
2994
|
+
const command = params.command ?? "";
|
|
2995
|
+
this.logger.debug("Docker RUN", { image, name, ports: rawPorts.map((p) => `${p.host}:${p.container}`).join(", "), volumes: rawVolumes.map((v) => `${v.host}:${v.container}`).join(", "), command });
|
|
2996
|
+
const containerId = this.generateId();
|
|
2997
|
+
const ports = rawPorts.map((p) => ({
|
|
2998
|
+
host: p.host,
|
|
2999
|
+
container: p.container,
|
|
3000
|
+
protocol: p.protocol ?? "tcp"
|
|
3001
|
+
}));
|
|
3002
|
+
const volumes = rawVolumes.map((v) => ({
|
|
3003
|
+
host: v.host,
|
|
3004
|
+
container: v.container
|
|
3005
|
+
}));
|
|
3006
|
+
const container = {
|
|
3007
|
+
id: containerId,
|
|
3008
|
+
name,
|
|
3009
|
+
image,
|
|
3010
|
+
status: "running",
|
|
3011
|
+
ports,
|
|
3012
|
+
env,
|
|
3013
|
+
volumes,
|
|
3014
|
+
command,
|
|
3015
|
+
labels: {},
|
|
3016
|
+
createdAt: Date.now(),
|
|
3017
|
+
startedAt: Date.now(),
|
|
3018
|
+
stoppedAt: null,
|
|
3019
|
+
logs: [
|
|
3020
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} [info] Starting container ${name} from image ${image}`,
|
|
3021
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} [info] Container ${name} is now running`
|
|
3022
|
+
]
|
|
3023
|
+
};
|
|
3024
|
+
this.containers.set(containerId, container);
|
|
3025
|
+
return {
|
|
3026
|
+
containerId,
|
|
3027
|
+
name,
|
|
3028
|
+
status: container.status,
|
|
3029
|
+
ports
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
async stop(params) {
|
|
3033
|
+
const containerId = params.containerId;
|
|
3034
|
+
this.logger.debug("Docker STOP", { containerId });
|
|
3035
|
+
const container = this.findContainer(containerId);
|
|
3036
|
+
if (!container) {
|
|
3037
|
+
throw new Error(`Container not found: ${containerId}`);
|
|
3038
|
+
}
|
|
3039
|
+
if (container.status !== "running" && container.status !== "paused") {
|
|
3040
|
+
throw new Error(`Container ${containerId} is not running (current status: ${container.status})`);
|
|
3041
|
+
}
|
|
3042
|
+
container.status = "exited";
|
|
3043
|
+
container.stoppedAt = Date.now();
|
|
3044
|
+
container.logs.push(
|
|
3045
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} [info] Container ${container.name} stopped`
|
|
3046
|
+
);
|
|
3047
|
+
return {
|
|
3048
|
+
containerId: container.id,
|
|
3049
|
+
status: container.status,
|
|
3050
|
+
stoppedAt: container.stoppedAt
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
async removeContainer(params) {
|
|
3054
|
+
const containerId = params.containerId;
|
|
3055
|
+
const force = params.force ?? false;
|
|
3056
|
+
this.logger.debug("Docker REMOVE", { containerId, force });
|
|
3057
|
+
const container = this.findContainer(containerId);
|
|
3058
|
+
if (!container) {
|
|
3059
|
+
throw new Error(`Container not found: ${containerId}`);
|
|
3060
|
+
}
|
|
3061
|
+
if (container.status === "running" && !force) {
|
|
3062
|
+
throw new Error(
|
|
3063
|
+
`Container ${containerId} is running. Stop it first or use force=true.`
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
this.containers.delete(container.id);
|
|
3067
|
+
return { containerId: container.id, removed: true };
|
|
3068
|
+
}
|
|
3069
|
+
async logs(params) {
|
|
3070
|
+
const containerId = params.containerId;
|
|
3071
|
+
const tail = params.tail ?? 100;
|
|
3072
|
+
const since = params.since ?? void 0;
|
|
3073
|
+
this.logger.debug("Docker LOGS", { containerId, tail, since });
|
|
3074
|
+
const container = this.findContainer(containerId);
|
|
3075
|
+
if (!container) {
|
|
3076
|
+
throw new Error(`Container not found: ${containerId}`);
|
|
3077
|
+
}
|
|
3078
|
+
let logLines = this.generateLogLines(container, tail);
|
|
3079
|
+
if (since) {
|
|
3080
|
+
const sinceMs = new Date(since).getTime();
|
|
3081
|
+
logLines = logLines.filter((line) => {
|
|
3082
|
+
const timestampMatch = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z/.exec(line);
|
|
3083
|
+
if (!timestampMatch) return true;
|
|
3084
|
+
return new Date(timestampMatch[0]).getTime() >= sinceMs;
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
const tailedLines = logLines.slice(-tail);
|
|
3088
|
+
return {
|
|
3089
|
+
containerId: container.id,
|
|
3090
|
+
logs: tailedLines,
|
|
3091
|
+
lineCount: tailedLines.length
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
async status(params) {
|
|
3095
|
+
const containerId = params.containerId;
|
|
3096
|
+
this.logger.debug("Docker STATUS", { containerId });
|
|
3097
|
+
const container = this.findContainer(containerId);
|
|
3098
|
+
if (!container) {
|
|
3099
|
+
throw new Error(`Container not found: ${containerId}`);
|
|
3100
|
+
}
|
|
3101
|
+
return {
|
|
3102
|
+
containerId: container.id,
|
|
3103
|
+
name: container.name,
|
|
3104
|
+
image: container.image,
|
|
3105
|
+
status: container.status,
|
|
3106
|
+
ports: container.ports,
|
|
3107
|
+
createdAt: container.createdAt,
|
|
3108
|
+
startedAt: container.startedAt,
|
|
3109
|
+
stoppedAt: container.stoppedAt
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
async list(params) {
|
|
3113
|
+
const showAll = params.all ?? false;
|
|
3114
|
+
const filterByLabel = params.filterByLabel ?? void 0;
|
|
3115
|
+
this.logger.debug("Docker LIST", { all: showAll, filterByLabel });
|
|
3116
|
+
let entries = Array.from(this.containers.values());
|
|
3117
|
+
if (!showAll) {
|
|
3118
|
+
entries = entries.filter((c) => c.status === "running");
|
|
3119
|
+
}
|
|
3120
|
+
if (filterByLabel) {
|
|
3121
|
+
const [labelKey, labelValue] = filterByLabel.split("=");
|
|
3122
|
+
entries = entries.filter((c) => {
|
|
3123
|
+
if (labelValue !== void 0) {
|
|
3124
|
+
return c.labels[labelKey] === labelValue;
|
|
3125
|
+
}
|
|
3126
|
+
return labelKey in c.labels;
|
|
123
3127
|
});
|
|
124
3128
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
3129
|
+
entries.sort((a, b) => b.createdAt - a.createdAt);
|
|
3130
|
+
const containers = entries.map((c) => ({
|
|
3131
|
+
id: c.id,
|
|
3132
|
+
name: c.name,
|
|
3133
|
+
image: c.image,
|
|
3134
|
+
status: c.status,
|
|
3135
|
+
ports: c.ports,
|
|
3136
|
+
createdAt: c.createdAt
|
|
3137
|
+
}));
|
|
3138
|
+
return { containers, total: containers.length };
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
registerIntegration("docker", DockerIntegration);
|
|
3142
|
+
|
|
3143
|
+
// src/integrations/database/sql-guard.ts
|
|
3144
|
+
var DENIED_KEYWORDS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE", "MERGE", "INTO", "FOR"]);
|
|
3145
|
+
function isWordChar(ch) {
|
|
3146
|
+
return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_" || ch === "$";
|
|
3147
|
+
}
|
|
3148
|
+
function skipSingleQuoted(sql, start, eString) {
|
|
3149
|
+
let i = start + 1;
|
|
3150
|
+
while (i < sql.length) {
|
|
3151
|
+
if (eString && sql[i] === "\\") {
|
|
3152
|
+
i += 2;
|
|
3153
|
+
continue;
|
|
3154
|
+
}
|
|
3155
|
+
if (sql[i] === "'") {
|
|
3156
|
+
if (sql[i + 1] === "'") {
|
|
3157
|
+
i += 2;
|
|
3158
|
+
continue;
|
|
146
3159
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
3160
|
+
return i + 1;
|
|
3161
|
+
}
|
|
3162
|
+
i++;
|
|
3163
|
+
}
|
|
3164
|
+
return sql.length;
|
|
3165
|
+
}
|
|
3166
|
+
function skipDoubleQuoted(sql, start) {
|
|
3167
|
+
let i = start + 1;
|
|
3168
|
+
while (i < sql.length) {
|
|
3169
|
+
if (sql[i] === '"') {
|
|
3170
|
+
if (sql[i + 1] === '"') {
|
|
3171
|
+
i += 2;
|
|
3172
|
+
continue;
|
|
152
3173
|
}
|
|
3174
|
+
return i + 1;
|
|
153
3175
|
}
|
|
3176
|
+
i++;
|
|
154
3177
|
}
|
|
155
|
-
return
|
|
156
|
-
valid: errors.length === 0,
|
|
157
|
-
errors
|
|
158
|
-
};
|
|
3178
|
+
return sql.length;
|
|
159
3179
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
3180
|
+
function matchDollarTag(sql, start) {
|
|
3181
|
+
let i = start + 1;
|
|
3182
|
+
if (sql[i] === "$") return "$$";
|
|
3183
|
+
const first = sql[i];
|
|
3184
|
+
if (!first || !(first >= "a" && first <= "z" || first >= "A" && first <= "Z" || first === "_")) {
|
|
3185
|
+
return void 0;
|
|
3186
|
+
}
|
|
3187
|
+
i++;
|
|
3188
|
+
while (i < sql.length && isWordChar(sql[i]) && sql[i] !== "$") i++;
|
|
3189
|
+
if (sql[i] !== "$") return void 0;
|
|
3190
|
+
return sql.slice(start, i + 1);
|
|
3191
|
+
}
|
|
3192
|
+
function sanitizeSql(sql) {
|
|
3193
|
+
const out = [];
|
|
3194
|
+
let i = 0;
|
|
3195
|
+
while (i < sql.length) {
|
|
3196
|
+
const ch = sql[i];
|
|
3197
|
+
if (ch === "-" && sql[i + 1] === "-") {
|
|
3198
|
+
while (i < sql.length && sql[i] !== "\n") i++;
|
|
3199
|
+
out.push(" ");
|
|
3200
|
+
continue;
|
|
3201
|
+
}
|
|
3202
|
+
if (ch === "/" && sql[i + 1] === "*") {
|
|
3203
|
+
let depth = 1;
|
|
3204
|
+
i += 2;
|
|
3205
|
+
while (i < sql.length && depth > 0) {
|
|
3206
|
+
if (sql[i] === "/" && sql[i + 1] === "*") {
|
|
3207
|
+
depth++;
|
|
3208
|
+
i += 2;
|
|
3209
|
+
} else if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
3210
|
+
depth--;
|
|
3211
|
+
i += 2;
|
|
3212
|
+
} else {
|
|
3213
|
+
i++;
|
|
179
3214
|
}
|
|
180
3215
|
}
|
|
181
|
-
|
|
182
|
-
|
|
3216
|
+
out.push(" ");
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
if (ch === "'") {
|
|
3220
|
+
let j = i - 1;
|
|
3221
|
+
while (j >= 0 && (sql[j] === " " || sql[j] === " " || sql[j] === "\n" || sql[j] === "\r")) j--;
|
|
3222
|
+
const prev = j >= 0 ? sql[j] : "";
|
|
3223
|
+
const eString = (prev === "e" || prev === "E") && (j === 0 || !isWordChar(sql[j - 1]));
|
|
3224
|
+
i = skipSingleQuoted(sql, i, eString);
|
|
3225
|
+
out.push("''");
|
|
3226
|
+
continue;
|
|
3227
|
+
}
|
|
3228
|
+
if (ch === '"') {
|
|
3229
|
+
i = skipDoubleQuoted(sql, i);
|
|
3230
|
+
out.push('""');
|
|
3231
|
+
continue;
|
|
3232
|
+
}
|
|
3233
|
+
if (ch === "$") {
|
|
3234
|
+
const tag = matchDollarTag(sql, i);
|
|
3235
|
+
if (tag) {
|
|
3236
|
+
const close = sql.indexOf(tag, i + tag.length);
|
|
3237
|
+
i = close === -1 ? sql.length : close + tag.length;
|
|
3238
|
+
out.push("$$");
|
|
3239
|
+
continue;
|
|
183
3240
|
}
|
|
184
|
-
const delay = Math.min(
|
|
185
|
-
backoffMs * Math.pow(2, attempt - 1),
|
|
186
|
-
maxBackoffMs
|
|
187
|
-
);
|
|
188
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
189
3241
|
}
|
|
3242
|
+
out.push(ch);
|
|
3243
|
+
i++;
|
|
190
3244
|
}
|
|
191
|
-
|
|
3245
|
+
return out.join("");
|
|
192
3246
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.logger = config.logger || new ConsoleLogger();
|
|
3247
|
+
function assertReadOnlySelect(sql) {
|
|
3248
|
+
const sanitized = sanitizeSql(sql);
|
|
3249
|
+
const statements = sanitized.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3250
|
+
if (statements.length === 0) {
|
|
3251
|
+
return { ok: false, reason: "Empty statement" };
|
|
199
3252
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
*/
|
|
203
|
-
validateParams(action, params) {
|
|
204
|
-
return validateParams(this.config.name, action, params);
|
|
3253
|
+
if (statements.length > 1) {
|
|
3254
|
+
return { ok: false, reason: "Only a single statement is allowed" };
|
|
205
3255
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
3256
|
+
const words = (statements[0].match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) ?? []).map(
|
|
3257
|
+
(w) => w.toUpperCase()
|
|
3258
|
+
);
|
|
3259
|
+
const first = words[0];
|
|
3260
|
+
if (first !== "SELECT" && first !== "WITH") {
|
|
3261
|
+
return { ok: false, reason: "Only SELECT statements are allowed" };
|
|
3262
|
+
}
|
|
3263
|
+
const denied = words.find((w) => DENIED_KEYWORDS.has(w));
|
|
3264
|
+
if (denied) {
|
|
3265
|
+
return { ok: false, reason: `Keyword not allowed in a read-only query: ${denied}` };
|
|
3266
|
+
}
|
|
3267
|
+
return { ok: true };
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
// src/integrations/database/index.ts
|
|
3271
|
+
var DEFAULT_STATEMENT_TIMEOUT_MS = 1e4;
|
|
3272
|
+
var PG_QUERY_CANCELED = "57014";
|
|
3273
|
+
var PostgresDriver = class {
|
|
3274
|
+
constructor(connectionString, statementTimeoutMs) {
|
|
3275
|
+
this.pool = new Pool({
|
|
3276
|
+
connectionString,
|
|
3277
|
+
statement_timeout: statementTimeoutMs,
|
|
3278
|
+
query_timeout: statementTimeoutMs
|
|
212
3279
|
});
|
|
213
|
-
const integrationError = error instanceof Error ? error : new Error(String(error));
|
|
214
|
-
return {
|
|
215
|
-
success: false,
|
|
216
|
-
error: integrationError,
|
|
217
|
-
metadata: this.createMetadata(action, 0, 0)
|
|
218
|
-
};
|
|
219
3280
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
*/
|
|
223
|
-
createMetadata(action, duration, retries = 0) {
|
|
3281
|
+
async query(sql, params) {
|
|
3282
|
+
const result = await this.pool.query(sql, [...params]);
|
|
224
3283
|
return {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
duration,
|
|
228
|
-
retries,
|
|
229
|
-
timestamp: Date.now()
|
|
3284
|
+
rows: result.rows,
|
|
3285
|
+
rowCount: result.rowCount ?? result.rows.length
|
|
230
3286
|
};
|
|
231
3287
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
3288
|
+
async end() {
|
|
3289
|
+
await this.pool.end();
|
|
3290
|
+
}
|
|
3291
|
+
};
|
|
3292
|
+
function mapDriverError(error) {
|
|
3293
|
+
if (error instanceof IntegrationError) return error;
|
|
3294
|
+
const code = error.code;
|
|
3295
|
+
if (code === PG_QUERY_CANCELED) {
|
|
3296
|
+
return new IntegrationError("Statement timeout exceeded", "TIMEOUT_ERROR");
|
|
3297
|
+
}
|
|
3298
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3299
|
+
return new IntegrationError(message, "SERVICE_ERROR");
|
|
3300
|
+
}
|
|
3301
|
+
var DatabaseIntegration = class extends BaseIntegration {
|
|
3302
|
+
constructor(config) {
|
|
3303
|
+
super(config);
|
|
3304
|
+
this.drivers = /* @__PURE__ */ new Map();
|
|
3305
|
+
const fromEnv = config.env.DATABASE_STATEMENT_TIMEOUT_MS;
|
|
3306
|
+
if (fromEnv) {
|
|
3307
|
+
const parsed = Number(fromEnv);
|
|
3308
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
3309
|
+
throw new Error(`Invalid DATABASE_STATEMENT_TIMEOUT_MS: ${fromEnv}`);
|
|
3310
|
+
}
|
|
3311
|
+
this.statementTimeoutMs = parsed;
|
|
3312
|
+
} else {
|
|
3313
|
+
this.statementTimeoutMs = config.timeout ?? DEFAULT_STATEMENT_TIMEOUT_MS;
|
|
238
3314
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
backoffMs: this.config.retry.backoffMs,
|
|
242
|
-
maxBackoffMs: this.config.retry.maxBackoffMs,
|
|
243
|
-
retryableErrors: [
|
|
244
|
-
"TIMEOUT_ERROR",
|
|
245
|
-
"NETWORK_ERROR",
|
|
246
|
-
"RATE_LIMIT_ERROR"
|
|
247
|
-
]
|
|
3315
|
+
this.logger.info("Database integration initialized", {
|
|
3316
|
+
statementTimeoutMs: this.statementTimeoutMs
|
|
248
3317
|
});
|
|
249
3318
|
}
|
|
3319
|
+
async execute(action, params) {
|
|
3320
|
+
const validation = this.validateParams(action, params);
|
|
3321
|
+
if (!validation.valid) {
|
|
3322
|
+
return {
|
|
3323
|
+
success: false,
|
|
3324
|
+
error: {
|
|
3325
|
+
name: "IntegrationError",
|
|
3326
|
+
message: "Validation failed",
|
|
3327
|
+
code: "VALIDATION_ERROR",
|
|
3328
|
+
details: validation.errors
|
|
3329
|
+
},
|
|
3330
|
+
metadata: this.createMetadata(action, 0)
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
const startTime = Date.now();
|
|
3334
|
+
try {
|
|
3335
|
+
let data;
|
|
3336
|
+
switch (action) {
|
|
3337
|
+
case "query":
|
|
3338
|
+
data = await this.executeWithRetry(() => this.runQuery(params));
|
|
3339
|
+
break;
|
|
3340
|
+
default:
|
|
3341
|
+
throw new Error(`Unknown action: ${action}`);
|
|
3342
|
+
}
|
|
3343
|
+
return {
|
|
3344
|
+
success: true,
|
|
3345
|
+
data,
|
|
3346
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
3347
|
+
};
|
|
3348
|
+
} catch (error) {
|
|
3349
|
+
return this.handleError(action, error);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
async runQuery(params) {
|
|
3353
|
+
const guard = assertReadOnlySelect(params.sql);
|
|
3354
|
+
if (!guard.ok) {
|
|
3355
|
+
throw new IntegrationError(
|
|
3356
|
+
`Read-only violation: ${guard.reason ?? "not a SELECT statement"}`,
|
|
3357
|
+
"VALIDATION_ERROR"
|
|
3358
|
+
);
|
|
3359
|
+
}
|
|
3360
|
+
const driver = this.driverFor(params.connectionRef);
|
|
3361
|
+
this.logger.debug("Database QUERY", { connectionRef: params.connectionRef });
|
|
3362
|
+
try {
|
|
3363
|
+
return await driver.query(params.sql, params.params ?? []);
|
|
3364
|
+
} catch (error) {
|
|
3365
|
+
throw mapDriverError(error);
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
/** Resolve (and cache) the driver for a connection reference. */
|
|
3369
|
+
driverFor(connectionRef) {
|
|
3370
|
+
const connectionString = process.env[connectionRef];
|
|
3371
|
+
if (!connectionString) {
|
|
3372
|
+
throw new IntegrationError(
|
|
3373
|
+
`Connection reference "${connectionRef}" is not set in the environment`,
|
|
3374
|
+
"AUTH_ERROR"
|
|
3375
|
+
);
|
|
3376
|
+
}
|
|
3377
|
+
const cached = this.drivers.get(connectionString);
|
|
3378
|
+
if (cached) return cached;
|
|
3379
|
+
const driver = new PostgresDriver(connectionString, this.statementTimeoutMs);
|
|
3380
|
+
this.drivers.set(connectionString, driver);
|
|
3381
|
+
return driver;
|
|
3382
|
+
}
|
|
250
3383
|
};
|
|
3384
|
+
registerIntegration("database", DatabaseIntegration);
|
|
251
3385
|
|
|
252
|
-
// src/
|
|
253
|
-
var
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
3386
|
+
// src/integrations/wikimedia/index.ts
|
|
3387
|
+
var WIKI_ENDPOINT = "https://en.wikipedia.org/w/api.php";
|
|
3388
|
+
var DEFAULT_UA = "Almadar/1.0 (https://almadar.dev)";
|
|
3389
|
+
var DEFAULT_TIMEOUT_MS2 = 6e3;
|
|
3390
|
+
var EXTRACT_CHAR_CAP = 2e3;
|
|
3391
|
+
var WikimediaIntegration = class extends BaseIntegration {
|
|
3392
|
+
constructor(config) {
|
|
3393
|
+
super(config);
|
|
3394
|
+
this.userAgent = config.env?.WIKIMEDIA_USER_AGENT ?? DEFAULT_UA;
|
|
3395
|
+
this.timeoutMs = Number(config.env?.WIKIMEDIA_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS2);
|
|
3396
|
+
this.logger.info("Wikimedia integration initialized");
|
|
259
3397
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
3398
|
+
async execute(action, params) {
|
|
3399
|
+
const validation = this.validateParams(action, params);
|
|
3400
|
+
if (!validation.valid) {
|
|
3401
|
+
return {
|
|
3402
|
+
success: false,
|
|
3403
|
+
error: {
|
|
3404
|
+
name: "IntegrationError",
|
|
3405
|
+
message: "Validation failed",
|
|
3406
|
+
code: "VALIDATION_ERROR",
|
|
3407
|
+
details: validation.errors
|
|
3408
|
+
},
|
|
3409
|
+
metadata: this.createMetadata(action, 0)
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
const startTime = Date.now();
|
|
3413
|
+
try {
|
|
3414
|
+
let data;
|
|
3415
|
+
switch (action) {
|
|
3416
|
+
case "getPage":
|
|
3417
|
+
data = await this.executeWithRetry(() => this.getPage(params));
|
|
3418
|
+
break;
|
|
3419
|
+
default:
|
|
3420
|
+
throw new Error(`Unknown action: ${action}`);
|
|
3421
|
+
}
|
|
3422
|
+
return { success: true, data, metadata: this.createMetadata(action, Date.now() - startTime) };
|
|
3423
|
+
} catch (error) {
|
|
3424
|
+
return this.handleError(action, error);
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
/** Look up a title on Wikipedia: description, portrait, lead extract. Empty fields on miss. */
|
|
3428
|
+
async getPage(params) {
|
|
3429
|
+
const title = String(params.title ?? "").trim();
|
|
3430
|
+
if (!title) return {};
|
|
3431
|
+
const url = `${WIKI_ENDPOINT}?action=query&format=json&redirects=1&prop=pageimages|description|extracts&explaintext=1&exintro=1&piprop=thumbnail&pithumbsize=240&titles=${encodeURIComponent(title)}`;
|
|
3432
|
+
const ctrl = new AbortController();
|
|
3433
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
3434
|
+
try {
|
|
3435
|
+
const res = await fetch(url, { headers: { "User-Agent": this.userAgent }, signal: ctrl.signal });
|
|
3436
|
+
if (!res.ok) return {};
|
|
3437
|
+
const data = await res.json();
|
|
3438
|
+
const pages = data.query?.pages;
|
|
3439
|
+
if (!pages) return {};
|
|
3440
|
+
const page = Object.values(pages)[0];
|
|
3441
|
+
if (!page || page.missing !== void 0) return {};
|
|
3442
|
+
const extract = typeof page.extract === "string" ? page.extract.slice(0, EXTRACT_CHAR_CAP).trim() : void 0;
|
|
3443
|
+
return {
|
|
3444
|
+
title: page.title,
|
|
3445
|
+
description: page.description ?? (typeof page.extract === "string" ? page.extract.split(".")[0] : void 0),
|
|
3446
|
+
portraitUrl: page.thumbnail?.source,
|
|
3447
|
+
extract
|
|
3448
|
+
};
|
|
3449
|
+
} finally {
|
|
3450
|
+
clearTimeout(timer);
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
};
|
|
3454
|
+
registerIntegration("wikimedia", WikimediaIntegration);
|
|
3455
|
+
|
|
3456
|
+
// src/integrations/iconify/index.ts
|
|
3457
|
+
var ICONIFY_API = "https://api.iconify.design";
|
|
3458
|
+
var DEFAULT_UA2 = "Almadar/1.0 (https://almadar.dev)";
|
|
3459
|
+
var DEFAULT_TIMEOUT_MS3 = 6e3;
|
|
3460
|
+
var IconifyIntegration = class extends BaseIntegration {
|
|
3461
|
+
constructor(config) {
|
|
3462
|
+
super(config);
|
|
3463
|
+
this.userAgent = config.env?.ICONIFY_USER_AGENT ?? DEFAULT_UA2;
|
|
3464
|
+
this.timeoutMs = Number(config.env?.ICONIFY_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS3);
|
|
3465
|
+
this.logger.info("Iconify integration initialized");
|
|
3466
|
+
}
|
|
3467
|
+
async execute(action, params) {
|
|
3468
|
+
const validation = this.validateParams(action, params);
|
|
3469
|
+
if (!validation.valid) {
|
|
3470
|
+
return {
|
|
3471
|
+
success: false,
|
|
3472
|
+
error: {
|
|
3473
|
+
name: "IntegrationError",
|
|
3474
|
+
message: "Validation failed",
|
|
3475
|
+
code: "VALIDATION_ERROR",
|
|
3476
|
+
details: validation.errors
|
|
3477
|
+
},
|
|
3478
|
+
metadata: this.createMetadata(action, 0)
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
const startTime = Date.now();
|
|
3482
|
+
try {
|
|
3483
|
+
let data;
|
|
3484
|
+
switch (action) {
|
|
3485
|
+
case "svgExists":
|
|
3486
|
+
data = await this.executeWithRetry(() => this.svgExists(params));
|
|
3487
|
+
break;
|
|
3488
|
+
case "search":
|
|
3489
|
+
data = await this.executeWithRetry(() => this.search(params));
|
|
3490
|
+
break;
|
|
3491
|
+
case "getIconBody":
|
|
3492
|
+
data = await this.executeWithRetry(() => this.getIconBody(params));
|
|
3493
|
+
break;
|
|
3494
|
+
default:
|
|
3495
|
+
throw new Error(`Unknown action: ${action}`);
|
|
3496
|
+
}
|
|
3497
|
+
return { success: true, data, metadata: this.createMetadata(action, Date.now() - startTime) };
|
|
3498
|
+
} catch (error) {
|
|
3499
|
+
return this.handleError(action, error);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
async svgExists(params) {
|
|
3503
|
+
const path = String(params.path ?? "");
|
|
3504
|
+
if (!path) return { exists: false };
|
|
3505
|
+
return { exists: await this.headOk(`${ICONIFY_API}/${path}`) };
|
|
3506
|
+
}
|
|
3507
|
+
async search(params) {
|
|
3508
|
+
const query = String(params.query ?? "");
|
|
3509
|
+
if (!query) return { icons: [] };
|
|
3510
|
+
const limit = Number(params.limit ?? 1);
|
|
3511
|
+
const data = await this.getJson(`${ICONIFY_API}/search?query=${encodeURIComponent(query)}&limit=${limit}`);
|
|
3512
|
+
return { icons: data?.icons ?? [] };
|
|
3513
|
+
}
|
|
3514
|
+
async getIconBody(params) {
|
|
3515
|
+
const iconId = String(params.iconId ?? "");
|
|
3516
|
+
const [prefix, name] = iconId.split(":");
|
|
3517
|
+
if (!prefix || !name) return { body: null };
|
|
3518
|
+
const data = await this.getJson(`${ICONIFY_API}/${prefix}.json?icons=${encodeURIComponent(name)}`);
|
|
3519
|
+
return { body: data?.icons?.[name]?.body ?? data?.aliases?.[name]?.body ?? null };
|
|
3520
|
+
}
|
|
3521
|
+
async headOk(url) {
|
|
3522
|
+
const ctrl = new AbortController();
|
|
3523
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
3524
|
+
try {
|
|
3525
|
+
const res = await fetch(url, { method: "GET", headers: { "User-Agent": this.userAgent }, signal: ctrl.signal });
|
|
3526
|
+
return res.ok;
|
|
3527
|
+
} catch {
|
|
3528
|
+
return false;
|
|
3529
|
+
} finally {
|
|
3530
|
+
clearTimeout(timer);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
async getJson(url) {
|
|
3534
|
+
const ctrl = new AbortController();
|
|
3535
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
3536
|
+
try {
|
|
3537
|
+
const res = await fetch(url, { headers: { "User-Agent": this.userAgent }, signal: ctrl.signal });
|
|
3538
|
+
if (!res.ok) return null;
|
|
3539
|
+
return await res.json();
|
|
3540
|
+
} catch {
|
|
3541
|
+
return null;
|
|
3542
|
+
} finally {
|
|
3543
|
+
clearTimeout(timer);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
};
|
|
3547
|
+
registerIntegration("iconify", IconifyIntegration);
|
|
3548
|
+
|
|
3549
|
+
// src/integrations/arxiv/index.ts
|
|
3550
|
+
var ARXIV_ENDPOINT = "https://export.arxiv.org/api/query";
|
|
3551
|
+
var DEFAULT_TIMEOUT_MS4 = 1e4;
|
|
3552
|
+
var DEFAULT_MAX_RESULTS = 8;
|
|
3553
|
+
function parseAtom(xml) {
|
|
3554
|
+
const entries = [];
|
|
3555
|
+
for (const block of xml.split("<entry>").slice(1)) {
|
|
3556
|
+
const grab = (tag) => {
|
|
3557
|
+
const m = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"));
|
|
3558
|
+
return m ? m[1].replace(/<[^>]+>/g, "").trim() : "";
|
|
268
3559
|
};
|
|
3560
|
+
const id = grab("id");
|
|
3561
|
+
const title = grab("title").replace(/\s+/g, " ");
|
|
3562
|
+
const summary = grab("summary").replace(/\s+/g, " ");
|
|
3563
|
+
const published = grab("published");
|
|
3564
|
+
const authors = Array.from(block.matchAll(/<author>\s*<name>([^<]+)<\/name>/g)).map((m) => m[1].trim());
|
|
3565
|
+
const linkMatch = block.match(/<link[^>]*rel="alternate"[^>]*href="([^"]+)"/i);
|
|
3566
|
+
const url = linkMatch ? linkMatch[1] : id;
|
|
3567
|
+
if (id || title) entries.push({ id, title, summary, authors, published, url });
|
|
3568
|
+
}
|
|
3569
|
+
return entries;
|
|
3570
|
+
}
|
|
3571
|
+
var ArxivIntegration = class extends BaseIntegration {
|
|
3572
|
+
constructor(config) {
|
|
3573
|
+
super(config);
|
|
3574
|
+
this.timeoutMs = Number(config.env?.ARXIV_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS4);
|
|
3575
|
+
this.logger.info("arXiv integration initialized");
|
|
3576
|
+
}
|
|
3577
|
+
async execute(action, params) {
|
|
3578
|
+
const validation = this.validateParams(action, params);
|
|
3579
|
+
if (!validation.valid) {
|
|
3580
|
+
return {
|
|
3581
|
+
success: false,
|
|
3582
|
+
error: {
|
|
3583
|
+
name: "IntegrationError",
|
|
3584
|
+
message: "Validation failed",
|
|
3585
|
+
code: "VALIDATION_ERROR",
|
|
3586
|
+
details: validation.errors
|
|
3587
|
+
},
|
|
3588
|
+
metadata: this.createMetadata(action, 0)
|
|
3589
|
+
};
|
|
3590
|
+
}
|
|
3591
|
+
const startTime = Date.now();
|
|
3592
|
+
try {
|
|
3593
|
+
let data;
|
|
3594
|
+
switch (action) {
|
|
3595
|
+
case "search":
|
|
3596
|
+
data = await this.executeWithRetry(() => this.search(params));
|
|
3597
|
+
break;
|
|
3598
|
+
default:
|
|
3599
|
+
throw new Error(`Unknown action: ${action}`);
|
|
3600
|
+
}
|
|
3601
|
+
return { success: true, data, metadata: this.createMetadata(action, Date.now() - startTime) };
|
|
3602
|
+
} catch (error) {
|
|
3603
|
+
return this.handleError(action, error);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
async search(params) {
|
|
3607
|
+
const query = String(params.query ?? "").trim();
|
|
3608
|
+
if (!query) return { results: [] };
|
|
3609
|
+
const maxResults = Number(params.maxResults ?? DEFAULT_MAX_RESULTS);
|
|
3610
|
+
const url = `${ARXIV_ENDPOINT}?search_query=${encodeURIComponent(`all:${query}`)}&start=0&max_results=${maxResults}&sortBy=relevance`;
|
|
3611
|
+
const ctrl = new AbortController();
|
|
3612
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
3613
|
+
try {
|
|
3614
|
+
const res = await fetch(url, { headers: { "Accept": "application/atom+xml" }, signal: ctrl.signal });
|
|
3615
|
+
if (!res.ok) return { results: [] };
|
|
3616
|
+
const xml = await res.text();
|
|
3617
|
+
return { results: parseAtom(xml) };
|
|
3618
|
+
} finally {
|
|
3619
|
+
clearTimeout(timer);
|
|
3620
|
+
}
|
|
269
3621
|
}
|
|
270
3622
|
};
|
|
3623
|
+
registerIntegration("arxiv", ArxivIntegration);
|
|
271
3624
|
|
|
272
3625
|
// src/mocks/MockIntegration.ts
|
|
273
3626
|
var MockIntegration = class extends BaseIntegration {
|
|
@@ -356,6 +3709,35 @@ function generateMockFromShape(shape) {
|
|
|
356
3709
|
var RuntimeIntegrationManager = class {
|
|
357
3710
|
constructor() {
|
|
358
3711
|
this.factory = new IntegrationFactory();
|
|
3712
|
+
this.installNotConfiguredFallback();
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3715
|
+
* Wrap `factory.execute` so an unknown/unconfigured service echoes its
|
|
3716
|
+
* params instead of throwing. This makes the manager safe to install as a
|
|
3717
|
+
* default `callService` handler (e.g. the playground sidecar) without every
|
|
3718
|
+
* service being configured. REAL execution errors (API failures, bad
|
|
3719
|
+
* params, schema violations) still propagate so the circuit's failure
|
|
3720
|
+
* events fire — only the two "not set up" errors (`Unknown integration`,
|
|
3721
|
+
* `Integration not configured`) are caught. Subsumes the broader wrapper
|
|
3722
|
+
* that previously lived inside `configureMockMode`.
|
|
3723
|
+
*/
|
|
3724
|
+
installNotConfiguredFallback() {
|
|
3725
|
+
const originalExecute = this.factory.execute.bind(this.factory);
|
|
3726
|
+
this.factory.execute = async (integration, action, params) => {
|
|
3727
|
+
try {
|
|
3728
|
+
return await originalExecute(integration, action, params);
|
|
3729
|
+
} catch (err) {
|
|
3730
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3731
|
+
if (/Unknown integration|not configured/i.test(msg)) {
|
|
3732
|
+
return {
|
|
3733
|
+
success: true,
|
|
3734
|
+
data: { ...params, _mock: true, _service: integration, _action: action },
|
|
3735
|
+
metadata: { integration, action, duration: 0, retries: 0, timestamp: Date.now() }
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
throw err;
|
|
3739
|
+
}
|
|
3740
|
+
};
|
|
359
3741
|
}
|
|
360
3742
|
/**
|
|
361
3743
|
* Configure from environment variables.
|
|
@@ -407,20 +3789,22 @@ var RuntimeIntegrationManager = class {
|
|
|
407
3789
|
}
|
|
408
3790
|
});
|
|
409
3791
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
3792
|
+
this.factory.configure("webhook", {
|
|
3793
|
+
env: {
|
|
3794
|
+
WEBHOOK_SIGNING_SECRET: env.WEBHOOK_SIGNING_SECRET || "",
|
|
3795
|
+
WEBHOOK_TIMEOUT_MS: env.WEBHOOK_TIMEOUT_MS || ""
|
|
3796
|
+
}
|
|
3797
|
+
});
|
|
3798
|
+
const llmEnv = {};
|
|
3799
|
+
for (const k of ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "KIMI_API_KEY", "OPEN_ROUTER_API_KEY"]) {
|
|
3800
|
+
if (env[k]) llmEnv[k] = env[k];
|
|
3801
|
+
}
|
|
3802
|
+
if (env.PROVIDER) llmEnv.PROVIDER = env.PROVIDER;
|
|
3803
|
+
for (const k of ["EMBEDDING_PROVIDER", "EMBEDDING_MODEL", "EMBEDDING_API_KEY"]) {
|
|
3804
|
+
if (env[k]) llmEnv[k] = env[k];
|
|
3805
|
+
}
|
|
3806
|
+
if (Object.keys(llmEnv).length > 0) {
|
|
3807
|
+
this.factory.configure("llm", { env: llmEnv });
|
|
424
3808
|
}
|
|
425
3809
|
if (env.MASAR_URL && env.MASAR_ML_TRAIT) {
|
|
426
3810
|
this.factory.configure("ml", {
|
|
@@ -468,18 +3852,6 @@ var RuntimeIntegrationManager = class {
|
|
|
468
3852
|
this.factory.configure(serviceName, { env: {} });
|
|
469
3853
|
this.factory.registerInstance(serviceName, mock);
|
|
470
3854
|
}
|
|
471
|
-
const originalExecute = this.factory.execute.bind(this.factory);
|
|
472
|
-
this.factory.execute = async (integration, action, params) => {
|
|
473
|
-
try {
|
|
474
|
-
return await originalExecute(integration, action, params);
|
|
475
|
-
} catch {
|
|
476
|
-
return {
|
|
477
|
-
success: true,
|
|
478
|
-
data: { ...params, _mock: true, _service: integration, _action: action },
|
|
479
|
-
metadata: { integration, action, duration: 0, retries: 0, timestamp: Date.now() }
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
3855
|
}
|
|
484
3856
|
/**
|
|
485
3857
|
* Load services registry from @almadar/core/patterns.
|