@almadar/integrations 2.23.0 → 2.25.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/factory-BPVhvv5q.d.ts +59 -0
- package/dist/index.d.ts +276 -18
- package/dist/index.js +1857 -161
- package/dist/index.js.map +1 -1
- package/dist/mocks/index.d.ts +1 -1
- package/dist/mocks/index.js +39 -15
- package/dist/mocks/index.js.map +1 -1
- package/dist/runtime/index.d.ts +24 -5
- package/dist/runtime/index.js +1777 -139
- package/dist/runtime/index.js.map +1 -1
- package/dist/{contracts-CLTh6gjT.d.ts → store-CW1v7Apc.d.ts} +492 -5
- package/package.json +12 -7
- package/dist/factory-DTdVeyAi.d.ts +0 -41
package/dist/index.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { createLogger } from '@almadar/logger';
|
|
2
2
|
import { integratorsRegistry } from '@almadar/core/patterns';
|
|
3
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHmac, createHash } from 'crypto';
|
|
3
4
|
import Stripe from 'stripe';
|
|
4
5
|
import { google } from 'googleapis';
|
|
5
6
|
import twilio from 'twilio';
|
|
6
7
|
import sgMail from '@sendgrid/mail';
|
|
7
8
|
import { Resend } from 'resend';
|
|
9
|
+
import webpush from 'web-push';
|
|
10
|
+
import { Readable } from 'stream';
|
|
8
11
|
import { getAvailableProvider, LLMClient, EmbeddingClient } from '@almadar/llm';
|
|
9
12
|
import { z } from 'zod';
|
|
10
13
|
import { execSync, spawn } from 'child_process';
|
|
11
14
|
import { mkdtempSync, writeFileSync, rmSync, promises } from 'fs';
|
|
12
15
|
import { join } from 'path';
|
|
13
16
|
import { tmpdir } from 'os';
|
|
17
|
+
import * as oidc from 'openid-client';
|
|
18
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
|
19
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
14
20
|
import { Pool } from 'pg';
|
|
15
21
|
|
|
16
22
|
// src/types.ts
|
|
@@ -221,56 +227,80 @@ function getRegisteredIntegrations() {
|
|
|
221
227
|
}
|
|
222
228
|
|
|
223
229
|
// src/factory.ts
|
|
230
|
+
function instanceKey(name, principal) {
|
|
231
|
+
return principal ? `${name}\0${principal}` : name;
|
|
232
|
+
}
|
|
224
233
|
var IntegrationFactory = class {
|
|
225
234
|
constructor() {
|
|
226
235
|
this.instances = /* @__PURE__ */ new Map();
|
|
227
236
|
this.configs = /* @__PURE__ */ new Map();
|
|
228
237
|
}
|
|
229
238
|
/**
|
|
230
|
-
* Configure an integration (doesn't instantiate yet)
|
|
239
|
+
* Configure an integration (doesn't instantiate yet). A `principal` scopes
|
|
240
|
+
* the config to that principal; the app-wide config (no principal) is the
|
|
241
|
+
* fallback for every principal.
|
|
231
242
|
*/
|
|
232
|
-
configure(name, config) {
|
|
233
|
-
this.configs.set(name, { name, ...config });
|
|
243
|
+
configure(name, config, principal) {
|
|
244
|
+
this.configs.set(instanceKey(name, principal), { name, ...config });
|
|
234
245
|
}
|
|
235
246
|
/**
|
|
236
|
-
* Get or create an integration instance
|
|
247
|
+
* Get or create an integration instance. Principal-scoped lookups fall
|
|
248
|
+
* back to the app-wide config when no per-principal config exists.
|
|
237
249
|
*/
|
|
238
|
-
get(name) {
|
|
239
|
-
|
|
240
|
-
|
|
250
|
+
get(name, principal) {
|
|
251
|
+
const key = instanceKey(name, principal);
|
|
252
|
+
const cached = this.instances.get(key);
|
|
253
|
+
if (cached) {
|
|
254
|
+
return cached;
|
|
241
255
|
}
|
|
242
256
|
const Constructor = getIntegration(name);
|
|
243
257
|
if (!Constructor) {
|
|
244
258
|
throw new Error(`Unknown integration: ${name}. Make sure it's imported.`);
|
|
245
259
|
}
|
|
246
|
-
const config = this.configs.get(name);
|
|
260
|
+
const config = this.configs.get(key) ?? this.configs.get(name);
|
|
247
261
|
if (!config) {
|
|
248
262
|
throw new Error(
|
|
249
263
|
`Integration not configured: ${name}. Call configure() first.`
|
|
250
264
|
);
|
|
251
265
|
}
|
|
252
266
|
const instance = new Constructor(config);
|
|
253
|
-
this.instances.set(
|
|
267
|
+
this.instances.set(key, instance);
|
|
254
268
|
return instance;
|
|
255
269
|
}
|
|
256
270
|
/**
|
|
257
271
|
* Execute an action on an integration
|
|
258
272
|
*/
|
|
259
|
-
async execute(integration, action, params) {
|
|
260
|
-
const instance = this.get(integration);
|
|
273
|
+
async execute(integration, action, params, context) {
|
|
274
|
+
const instance = this.get(integration, context?.principal);
|
|
261
275
|
return await instance.execute(action, params);
|
|
262
276
|
}
|
|
263
277
|
/**
|
|
264
278
|
* Check if integration is configured
|
|
265
279
|
*/
|
|
266
|
-
isConfigured(name) {
|
|
267
|
-
return this.configs.has(name);
|
|
280
|
+
isConfigured(name, principal) {
|
|
281
|
+
return this.configs.has(instanceKey(name, principal)) || this.configs.has(name);
|
|
268
282
|
}
|
|
269
283
|
/**
|
|
270
284
|
* Register an integration instance directly (used by mock infrastructure)
|
|
271
285
|
*/
|
|
272
|
-
registerInstance(name, instance) {
|
|
273
|
-
this.instances.set(name, instance);
|
|
286
|
+
registerInstance(name, instance, principal) {
|
|
287
|
+
this.instances.set(instanceKey(name, principal), instance);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Drop the cached instance(s) for a name so the next `get` rebuilds from
|
|
291
|
+
* the current config — how a credential change goes live without restart.
|
|
292
|
+
* Configs are kept; without a name, every instance is dropped.
|
|
293
|
+
*/
|
|
294
|
+
invalidate(name) {
|
|
295
|
+
if (name === void 0) {
|
|
296
|
+
this.instances.clear();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
for (const key of this.instances.keys()) {
|
|
300
|
+
if (key === name || key.startsWith(`${name}\0`)) {
|
|
301
|
+
this.instances.delete(key);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
274
304
|
}
|
|
275
305
|
/**
|
|
276
306
|
* Clear all instances (useful for testing)
|
|
@@ -297,6 +327,157 @@ function resetIntegrationFactory() {
|
|
|
297
327
|
_factory?.reset();
|
|
298
328
|
_factory = null;
|
|
299
329
|
}
|
|
330
|
+
var CREDENTIAL_ENTITY_TYPE = "AlmadarIntegrationCredential";
|
|
331
|
+
var CREDENTIAL_MASTER_KEY_ENV = "ALMADAR_CREDENTIAL_MASTER_KEY";
|
|
332
|
+
var ENCRYPTION_ALGORITHM = "aes-256-gcm";
|
|
333
|
+
function isNonEmptyString(v) {
|
|
334
|
+
return typeof v === "string" && v.length > 0;
|
|
335
|
+
}
|
|
336
|
+
var CredentialStore = class {
|
|
337
|
+
constructor(adapter, masterKey) {
|
|
338
|
+
this.byEnvVar = /* @__PURE__ */ new Map();
|
|
339
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
340
|
+
this.warmed = false;
|
|
341
|
+
this.adapter = adapter;
|
|
342
|
+
const key = masterKey ?? process.env[CREDENTIAL_MASTER_KEY_ENV];
|
|
343
|
+
const valid = typeof key === "string" && /^[0-9a-fA-F]{64}$/.test(key);
|
|
344
|
+
if (!valid && process.env.NODE_ENV === "production") {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`${CREDENTIAL_MASTER_KEY_ENV} must be a 64-character hex string (32 bytes) in production \u2014 generate one with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
this.masterKeyHex = valid ? key : void 0;
|
|
350
|
+
}
|
|
351
|
+
/** True when a valid master key is present (writes and decryption enabled). */
|
|
352
|
+
get enabled() {
|
|
353
|
+
return this.masterKeyHex !== void 0;
|
|
354
|
+
}
|
|
355
|
+
/** Subscribe to credential changes; returns an unsubscribe function. */
|
|
356
|
+
subscribe(listener) {
|
|
357
|
+
this.listeners.add(listener);
|
|
358
|
+
return () => this.listeners.delete(listener);
|
|
359
|
+
}
|
|
360
|
+
notify() {
|
|
361
|
+
for (const listener of this.listeners) listener();
|
|
362
|
+
}
|
|
363
|
+
encrypt(plaintext) {
|
|
364
|
+
if (!this.masterKeyHex) {
|
|
365
|
+
throw new Error(`${CREDENTIAL_MASTER_KEY_ENV} is not configured \u2014 cannot store credentials`);
|
|
366
|
+
}
|
|
367
|
+
const iv = randomBytes(12);
|
|
368
|
+
const cipher = createCipheriv(ENCRYPTION_ALGORITHM, Buffer.from(this.masterKeyHex, "hex"), iv);
|
|
369
|
+
let ciphertext = cipher.update(plaintext, "utf8", "hex");
|
|
370
|
+
ciphertext += cipher.final("hex");
|
|
371
|
+
return { ciphertext, iv: iv.toString("hex"), authTag: cipher.getAuthTag().toString("hex") };
|
|
372
|
+
}
|
|
373
|
+
decrypt(ciphertext, ivHex, authTagHex) {
|
|
374
|
+
if (!this.masterKeyHex) {
|
|
375
|
+
throw new Error(`${CREDENTIAL_MASTER_KEY_ENV} is not configured \u2014 cannot decrypt credentials`);
|
|
376
|
+
}
|
|
377
|
+
const decipher = createDecipheriv(
|
|
378
|
+
ENCRYPTION_ALGORITHM,
|
|
379
|
+
Buffer.from(this.masterKeyHex, "hex"),
|
|
380
|
+
Buffer.from(ivHex, "hex")
|
|
381
|
+
);
|
|
382
|
+
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
383
|
+
let plaintext = decipher.update(ciphertext, "hex", "utf8");
|
|
384
|
+
plaintext += decipher.final("utf8");
|
|
385
|
+
return plaintext;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Load and decrypt every stored row into memory. Returns the number of
|
|
389
|
+
* resolvable credentials. Without a master key nothing decrypts (rows are
|
|
390
|
+
* left in place, resolution falls through to env).
|
|
391
|
+
*/
|
|
392
|
+
async warm() {
|
|
393
|
+
this.byEnvVar.clear();
|
|
394
|
+
this.warmed = true;
|
|
395
|
+
if (!this.enabled) return 0;
|
|
396
|
+
const rows = await this.adapter.list(CREDENTIAL_ENTITY_TYPE);
|
|
397
|
+
for (const row of rows) {
|
|
398
|
+
const { id, service, envVar, ciphertext, iv, authTag } = row;
|
|
399
|
+
if (!isNonEmptyString(id) || !isNonEmptyString(service) || !isNonEmptyString(envVar) || !isNonEmptyString(ciphertext) || !isNonEmptyString(iv) || !isNonEmptyString(authTag)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const value = this.decrypt(ciphertext, iv, authTag);
|
|
403
|
+
this.byEnvVar.set(envVar, {
|
|
404
|
+
id,
|
|
405
|
+
service,
|
|
406
|
+
envVar,
|
|
407
|
+
value,
|
|
408
|
+
last4: value.slice(-4),
|
|
409
|
+
updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : 0
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return this.byEnvVar.size;
|
|
413
|
+
}
|
|
414
|
+
/** Resolve one credential by env-var name (warmed values only). */
|
|
415
|
+
resolve(envVar) {
|
|
416
|
+
return this.byEnvVar.get(envVar)?.value;
|
|
417
|
+
}
|
|
418
|
+
/** All warmed values as an env-shaped map, for merging over the process env. */
|
|
419
|
+
snapshotEnv() {
|
|
420
|
+
const env = {};
|
|
421
|
+
for (const [envVar, entry] of this.byEnvVar) env[envVar] = entry.value;
|
|
422
|
+
return env;
|
|
423
|
+
}
|
|
424
|
+
/** Masked entries for display — never includes plaintext. */
|
|
425
|
+
entries() {
|
|
426
|
+
return Array.from(this.byEnvVar.values()).map(({ service, envVar, last4, updatedAt }) => ({ service, envVar, last4, updatedAt })).sort((a, b) => a.service.localeCompare(b.service) || a.envVar.localeCompare(b.envVar));
|
|
427
|
+
}
|
|
428
|
+
/** Upsert one credential (encrypts, persists, re-warms the entry, notifies). */
|
|
429
|
+
async set(service, envVar, value) {
|
|
430
|
+
if (!this.warmed) await this.warm();
|
|
431
|
+
const { ciphertext, iv, authTag } = this.encrypt(value);
|
|
432
|
+
const updatedAt = Date.now();
|
|
433
|
+
const existing = this.byEnvVar.get(envVar);
|
|
434
|
+
const data = { service, envVar, ciphertext, iv, authTag, updatedAt };
|
|
435
|
+
let id;
|
|
436
|
+
if (existing) {
|
|
437
|
+
id = existing.id;
|
|
438
|
+
await this.adapter.update(CREDENTIAL_ENTITY_TYPE, id, data);
|
|
439
|
+
} else {
|
|
440
|
+
({ id } = await this.adapter.create(CREDENTIAL_ENTITY_TYPE, data));
|
|
441
|
+
}
|
|
442
|
+
const entry = { id, service, envVar, value, last4: value.slice(-4), updatedAt };
|
|
443
|
+
this.byEnvVar.set(envVar, entry);
|
|
444
|
+
this.notify();
|
|
445
|
+
const { service: s, envVar: e, last4, updatedAt: u } = entry;
|
|
446
|
+
return { service: s, envVar: e, last4, updatedAt: u };
|
|
447
|
+
}
|
|
448
|
+
/** Delete one credential row; resolution falls back to env afterwards. */
|
|
449
|
+
async remove(envVar) {
|
|
450
|
+
if (!this.warmed) await this.warm();
|
|
451
|
+
const existing = this.byEnvVar.get(envVar);
|
|
452
|
+
if (!existing) return false;
|
|
453
|
+
await this.adapter.delete(CREDENTIAL_ENTITY_TYPE, existing.id);
|
|
454
|
+
this.byEnvVar.delete(envVar);
|
|
455
|
+
this.notify();
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// src/credentials/resolver.ts
|
|
461
|
+
var installedStore = null;
|
|
462
|
+
var activeFactory = null;
|
|
463
|
+
function installActiveFactory(factory) {
|
|
464
|
+
activeFactory = factory;
|
|
465
|
+
}
|
|
466
|
+
function getActiveFactory() {
|
|
467
|
+
return activeFactory;
|
|
468
|
+
}
|
|
469
|
+
function installCredentialStore(store) {
|
|
470
|
+
installedStore = store;
|
|
471
|
+
}
|
|
472
|
+
function getInstalledCredentialStore() {
|
|
473
|
+
return installedStore;
|
|
474
|
+
}
|
|
475
|
+
function uninstallCredentialStore() {
|
|
476
|
+
installedStore = null;
|
|
477
|
+
}
|
|
478
|
+
function resolveCredentialRef(ref, env = process.env) {
|
|
479
|
+
return installedStore?.resolve(ref) ?? env[ref];
|
|
480
|
+
}
|
|
300
481
|
var STRIPE_API_VERSION = "2025-02-24.acacia";
|
|
301
482
|
function priceToTier(priceId, prices) {
|
|
302
483
|
if (priceId === prices.solo) return "solo";
|
|
@@ -874,40 +1055,1020 @@ var TwilioIntegration = class extends BaseIntegration {
|
|
|
874
1055
|
status: message.status
|
|
875
1056
|
};
|
|
876
1057
|
}
|
|
877
|
-
async sendWhatsApp(params) {
|
|
878
|
-
const { to, body } = params;
|
|
879
|
-
this.logger.debug("Sending WhatsApp message", { to: String(to ?? "") });
|
|
880
|
-
const message = await this.client.messages.create({
|
|
881
|
-
from: `whatsapp:${this.phoneNumber}`,
|
|
882
|
-
to: `whatsapp:${to}`,
|
|
883
|
-
body
|
|
1058
|
+
async sendWhatsApp(params) {
|
|
1059
|
+
const { to, body } = params;
|
|
1060
|
+
this.logger.debug("Sending WhatsApp message", { to: String(to ?? "") });
|
|
1061
|
+
const message = await this.client.messages.create({
|
|
1062
|
+
from: `whatsapp:${this.phoneNumber}`,
|
|
1063
|
+
to: `whatsapp:${to}`,
|
|
1064
|
+
body
|
|
1065
|
+
});
|
|
1066
|
+
return {
|
|
1067
|
+
sid: message.sid,
|
|
1068
|
+
status: message.status
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
registerIntegration("twilio", TwilioIntegration);
|
|
1073
|
+
var EmailIntegration = class extends BaseIntegration {
|
|
1074
|
+
constructor(config) {
|
|
1075
|
+
super(config);
|
|
1076
|
+
this.provider = config.env.PROVIDER || "sendgrid";
|
|
1077
|
+
this.fromEmail = config.env.FROM_EMAIL || "noreply@example.com";
|
|
1078
|
+
if (this.provider === "sendgrid") {
|
|
1079
|
+
const apiKey = config.env.SENDGRID_API_KEY;
|
|
1080
|
+
if (!apiKey) {
|
|
1081
|
+
throw new Error("SENDGRID_API_KEY not configured");
|
|
1082
|
+
}
|
|
1083
|
+
sgMail.setApiKey(apiKey);
|
|
1084
|
+
} else if (this.provider === "resend") {
|
|
1085
|
+
const apiKey = config.env.RESEND_API_KEY;
|
|
1086
|
+
if (!apiKey) {
|
|
1087
|
+
throw new Error("RESEND_API_KEY not configured");
|
|
1088
|
+
}
|
|
1089
|
+
this.resendClient = new Resend(apiKey);
|
|
1090
|
+
}
|
|
1091
|
+
this.logger.info(`Email integration initialized (${this.provider})`);
|
|
1092
|
+
}
|
|
1093
|
+
async execute(action, params) {
|
|
1094
|
+
const validation = this.validateParams(action, params);
|
|
1095
|
+
if (!validation.valid) {
|
|
1096
|
+
return {
|
|
1097
|
+
success: false,
|
|
1098
|
+
error: {
|
|
1099
|
+
name: "IntegrationError",
|
|
1100
|
+
message: "Validation failed",
|
|
1101
|
+
code: "VALIDATION_ERROR",
|
|
1102
|
+
details: validation.errors
|
|
1103
|
+
},
|
|
1104
|
+
metadata: this.createMetadata(action, 0)
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
const startTime = Date.now();
|
|
1108
|
+
let retries = 0;
|
|
1109
|
+
try {
|
|
1110
|
+
let data;
|
|
1111
|
+
switch (action) {
|
|
1112
|
+
case "send":
|
|
1113
|
+
data = await this.executeWithRetry(() => this.send(params));
|
|
1114
|
+
break;
|
|
1115
|
+
default:
|
|
1116
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
success: true,
|
|
1120
|
+
data,
|
|
1121
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1122
|
+
};
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
return this.handleError(action, error);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
async send(params) {
|
|
1128
|
+
const { to, subject, body, from } = params;
|
|
1129
|
+
this.logger.debug("Sending email", { to: String(to), subject: String(subject), provider: this.provider });
|
|
1130
|
+
if (this.provider === "sendgrid") {
|
|
1131
|
+
return await this.sendViaSendGrid(
|
|
1132
|
+
to,
|
|
1133
|
+
subject,
|
|
1134
|
+
body,
|
|
1135
|
+
from || this.fromEmail
|
|
1136
|
+
);
|
|
1137
|
+
} else if (this.provider === "resend") {
|
|
1138
|
+
return await this.sendViaResend(
|
|
1139
|
+
to,
|
|
1140
|
+
subject,
|
|
1141
|
+
body,
|
|
1142
|
+
from || this.fromEmail
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
throw new Error(`Unknown email provider: ${this.provider}`);
|
|
1146
|
+
}
|
|
1147
|
+
async sendViaSendGrid(to, subject, body, from) {
|
|
1148
|
+
const msg = {
|
|
1149
|
+
to,
|
|
1150
|
+
from,
|
|
1151
|
+
subject,
|
|
1152
|
+
html: body
|
|
1153
|
+
};
|
|
1154
|
+
const response = await sgMail.send(msg);
|
|
1155
|
+
return {
|
|
1156
|
+
id: response[0].headers["x-message-id"],
|
|
1157
|
+
status: "sent"
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
async sendViaResend(to, subject, body, from) {
|
|
1161
|
+
if (!this.resendClient) {
|
|
1162
|
+
throw new Error("Resend client not initialized");
|
|
1163
|
+
}
|
|
1164
|
+
const response = await this.resendClient.emails.send({
|
|
1165
|
+
from,
|
|
1166
|
+
to,
|
|
1167
|
+
subject,
|
|
1168
|
+
html: body
|
|
1169
|
+
});
|
|
1170
|
+
return {
|
|
1171
|
+
id: response.data?.id,
|
|
1172
|
+
status: "sent"
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
registerIntegration("email", EmailIntegration);
|
|
1177
|
+
var WebhookIntegration = class extends BaseIntegration {
|
|
1178
|
+
constructor(config) {
|
|
1179
|
+
super(config);
|
|
1180
|
+
this.signingSecret = config.env.WEBHOOK_SIGNING_SECRET || "";
|
|
1181
|
+
this.timeoutMs = Number(config.env.WEBHOOK_TIMEOUT_MS) || 1e4;
|
|
1182
|
+
this.logger.info("Webhook integration initialized");
|
|
1183
|
+
}
|
|
1184
|
+
async execute(action, params) {
|
|
1185
|
+
const validation = this.validateParams(action, params);
|
|
1186
|
+
if (!validation.valid) {
|
|
1187
|
+
return {
|
|
1188
|
+
success: false,
|
|
1189
|
+
error: {
|
|
1190
|
+
name: "IntegrationError",
|
|
1191
|
+
message: "Validation failed",
|
|
1192
|
+
code: "VALIDATION_ERROR",
|
|
1193
|
+
details: validation.errors
|
|
1194
|
+
},
|
|
1195
|
+
metadata: this.createMetadata(action, 0)
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
const startTime = Date.now();
|
|
1199
|
+
let retries = 0;
|
|
1200
|
+
try {
|
|
1201
|
+
let data;
|
|
1202
|
+
switch (action) {
|
|
1203
|
+
case "send":
|
|
1204
|
+
data = await this.executeWithRetry(() => this.send(params));
|
|
1205
|
+
break;
|
|
1206
|
+
default:
|
|
1207
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
success: true,
|
|
1211
|
+
data,
|
|
1212
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1213
|
+
};
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
return this.handleError(action, error);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
async send(params) {
|
|
1219
|
+
const { url, event, payload, secret } = params;
|
|
1220
|
+
const body = JSON.stringify({
|
|
1221
|
+
event,
|
|
1222
|
+
payload: payload ?? {},
|
|
1223
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1224
|
+
});
|
|
1225
|
+
const headers = {
|
|
1226
|
+
"Content-Type": "application/json",
|
|
1227
|
+
"X-Almadar-Event": event
|
|
1228
|
+
};
|
|
1229
|
+
const signingSecret = secret || this.signingSecret;
|
|
1230
|
+
if (signingSecret) {
|
|
1231
|
+
headers["X-Almadar-Signature"] = "sha256=" + createHmac("sha256", signingSecret).update(body).digest("hex");
|
|
1232
|
+
}
|
|
1233
|
+
this.logger.debug("Sending webhook", { url: String(url), event: String(event) });
|
|
1234
|
+
const startTime = Date.now();
|
|
1235
|
+
const response = await fetch(url, {
|
|
1236
|
+
method: "POST",
|
|
1237
|
+
headers,
|
|
1238
|
+
body,
|
|
1239
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
1240
|
+
});
|
|
1241
|
+
if (response.status >= 500) {
|
|
1242
|
+
throw new Error(`Webhook endpoint returned ${response.status}`);
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
status: response.status,
|
|
1246
|
+
ok: response.ok,
|
|
1247
|
+
durationMs: Date.now() - startTime
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
registerIntegration("webhook", WebhookIntegration);
|
|
1252
|
+
function isParamRecord(value) {
|
|
1253
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
1254
|
+
}
|
|
1255
|
+
function toSubscription(value) {
|
|
1256
|
+
if (isParamRecord(value)) {
|
|
1257
|
+
const { endpoint, keys } = value;
|
|
1258
|
+
if (typeof endpoint === "string" && isParamRecord(keys)) {
|
|
1259
|
+
const { p256dh, auth } = keys;
|
|
1260
|
+
if (typeof p256dh === "string" && typeof auth === "string") {
|
|
1261
|
+
return { endpoint, keys: { p256dh, auth } };
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
throw new Error("push.send: subscription must be { endpoint, keys: { p256dh, auth } }");
|
|
1266
|
+
}
|
|
1267
|
+
var PushIntegration = class extends BaseIntegration {
|
|
1268
|
+
constructor(config) {
|
|
1269
|
+
super(config);
|
|
1270
|
+
this.vapidPublicKey = config.env.VAPID_PUBLIC_KEY || "";
|
|
1271
|
+
this.vapidPrivateKey = config.env.VAPID_PRIVATE_KEY || "";
|
|
1272
|
+
this.vapidSubject = config.env.VAPID_SUBJECT || "";
|
|
1273
|
+
this.logger.info("Push integration initialized");
|
|
1274
|
+
}
|
|
1275
|
+
async execute(action, params) {
|
|
1276
|
+
const validation = this.validateParams(action, params);
|
|
1277
|
+
if (!validation.valid) {
|
|
1278
|
+
return {
|
|
1279
|
+
success: false,
|
|
1280
|
+
error: {
|
|
1281
|
+
name: "IntegrationError",
|
|
1282
|
+
message: "Validation failed",
|
|
1283
|
+
code: "VALIDATION_ERROR",
|
|
1284
|
+
details: validation.errors
|
|
1285
|
+
},
|
|
1286
|
+
metadata: this.createMetadata(action, 0)
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const startTime = Date.now();
|
|
1290
|
+
let retries = 0;
|
|
1291
|
+
try {
|
|
1292
|
+
let data;
|
|
1293
|
+
switch (action) {
|
|
1294
|
+
case "send":
|
|
1295
|
+
data = await this.executeWithRetry(() => this.send(params));
|
|
1296
|
+
break;
|
|
1297
|
+
default:
|
|
1298
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
success: true,
|
|
1302
|
+
data,
|
|
1303
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1304
|
+
};
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
return this.handleError(action, error);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async send(params) {
|
|
1310
|
+
const { subscription, title, body, url, icon } = params;
|
|
1311
|
+
const sub = toSubscription(subscription);
|
|
1312
|
+
const payload = JSON.stringify({
|
|
1313
|
+
title,
|
|
1314
|
+
body,
|
|
1315
|
+
url: url || void 0,
|
|
1316
|
+
icon: icon || void 0
|
|
1317
|
+
});
|
|
1318
|
+
this.logger.debug("Sending push notification", { endpoint: sub.endpoint });
|
|
1319
|
+
try {
|
|
1320
|
+
const response = await webpush.sendNotification(sub, payload, {
|
|
1321
|
+
vapidDetails: {
|
|
1322
|
+
subject: this.vapidSubject,
|
|
1323
|
+
publicKey: this.vapidPublicKey,
|
|
1324
|
+
privateKey: this.vapidPrivateKey
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
return { statusCode: response.statusCode, ok: true, expired: false };
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
if (error instanceof webpush.WebPushError) {
|
|
1330
|
+
if (error.statusCode >= 500) {
|
|
1331
|
+
throw new Error(`Push endpoint returned ${error.statusCode}`);
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
statusCode: error.statusCode,
|
|
1335
|
+
ok: false,
|
|
1336
|
+
expired: error.statusCode === 404 || error.statusCode === 410
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
throw error;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
registerIntegration("push", PushIntegration);
|
|
1344
|
+
|
|
1345
|
+
// src/integrations/calendar/webhooks.ts
|
|
1346
|
+
function header(headers, name) {
|
|
1347
|
+
const direct = headers[name] ?? headers[name.toLowerCase()];
|
|
1348
|
+
if (direct !== void 0) return direct;
|
|
1349
|
+
const found = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
1350
|
+
return (found !== void 0 ? headers[found] : void 0) ?? "";
|
|
1351
|
+
}
|
|
1352
|
+
function parseCalendarPushNotification(headers, expectedToken) {
|
|
1353
|
+
const channelId = header(headers, "x-goog-channel-id");
|
|
1354
|
+
const resourceId = header(headers, "x-goog-resource-id");
|
|
1355
|
+
const resourceState = header(headers, "x-goog-resource-state");
|
|
1356
|
+
if (!channelId || !resourceId || !resourceState) {
|
|
1357
|
+
return { error: "missing-headers" };
|
|
1358
|
+
}
|
|
1359
|
+
if (expectedToken && header(headers, "x-goog-channel-token") !== expectedToken) {
|
|
1360
|
+
return { error: "bad-token" };
|
|
1361
|
+
}
|
|
1362
|
+
return {
|
|
1363
|
+
type: "calendar.changed",
|
|
1364
|
+
channelId,
|
|
1365
|
+
resourceId,
|
|
1366
|
+
resourceState,
|
|
1367
|
+
messageNumber: Number(header(headers, "x-goog-message-number")) || 0
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function googleCalendarHookProvider(expectedToken) {
|
|
1371
|
+
return (input) => {
|
|
1372
|
+
const parsed = parseCalendarPushNotification(input.headers, expectedToken);
|
|
1373
|
+
if ("error" in parsed) return { error: parsed.error };
|
|
1374
|
+
if (parsed.resourceState === "sync") return { ack: true };
|
|
1375
|
+
return {
|
|
1376
|
+
event: "CAL_REMOTE_CHANGED",
|
|
1377
|
+
payload: {
|
|
1378
|
+
channelId: parsed.channelId,
|
|
1379
|
+
resourceId: parsed.resourceId,
|
|
1380
|
+
resourceState: parsed.resourceState
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
var CalendarIntegration = class extends BaseIntegration {
|
|
1386
|
+
constructor(config) {
|
|
1387
|
+
super(config);
|
|
1388
|
+
const rawKey = config.env.GOOGLE_CALENDAR_SA_KEY;
|
|
1389
|
+
if (!rawKey) {
|
|
1390
|
+
throw new Error("GOOGLE_CALENDAR_SA_KEY not configured");
|
|
1391
|
+
}
|
|
1392
|
+
const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
|
|
1393
|
+
const key = JSON.parse(keyJson);
|
|
1394
|
+
const subject = config.env.GOOGLE_CALENDAR_SUBJECT || void 0;
|
|
1395
|
+
const auth = new google.auth.JWT({
|
|
1396
|
+
email: key.client_email,
|
|
1397
|
+
key: key.private_key,
|
|
1398
|
+
scopes: ["https://www.googleapis.com/auth/calendar"],
|
|
1399
|
+
subject
|
|
1400
|
+
});
|
|
1401
|
+
this.client = google.calendar({ version: "v3", auth });
|
|
1402
|
+
this.defaultCalendarId = config.env.GOOGLE_CALENDAR_ID || "primary";
|
|
1403
|
+
this.logger.info("Calendar integration initialized", {
|
|
1404
|
+
delegated: Boolean(subject)
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
async execute(action, params) {
|
|
1408
|
+
const validation = this.validateParams(action, params);
|
|
1409
|
+
if (!validation.valid) {
|
|
1410
|
+
return {
|
|
1411
|
+
success: false,
|
|
1412
|
+
error: {
|
|
1413
|
+
name: "IntegrationError",
|
|
1414
|
+
message: "Validation failed",
|
|
1415
|
+
code: "VALIDATION_ERROR",
|
|
1416
|
+
details: validation.errors
|
|
1417
|
+
},
|
|
1418
|
+
metadata: this.createMetadata(action, 0)
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
const startTime = Date.now();
|
|
1422
|
+
let retries = 0;
|
|
1423
|
+
try {
|
|
1424
|
+
let data;
|
|
1425
|
+
switch (action) {
|
|
1426
|
+
case "listEvents":
|
|
1427
|
+
data = await this.executeWithRetry(() => this.listEvents(params));
|
|
1428
|
+
break;
|
|
1429
|
+
case "createEvent":
|
|
1430
|
+
data = await this.executeWithRetry(() => this.createEvent(params));
|
|
1431
|
+
break;
|
|
1432
|
+
case "updateEvent":
|
|
1433
|
+
data = await this.executeWithRetry(() => this.updateEvent(params));
|
|
1434
|
+
break;
|
|
1435
|
+
case "deleteEvent":
|
|
1436
|
+
data = await this.executeWithRetry(() => this.deleteEvent(params));
|
|
1437
|
+
break;
|
|
1438
|
+
case "freeBusy":
|
|
1439
|
+
data = await this.executeWithRetry(() => this.freeBusy(params));
|
|
1440
|
+
break;
|
|
1441
|
+
case "watch":
|
|
1442
|
+
data = await this.executeWithRetry(() => this.watch(params));
|
|
1443
|
+
break;
|
|
1444
|
+
case "stopWatch":
|
|
1445
|
+
data = await this.executeWithRetry(() => this.stopWatch(params));
|
|
1446
|
+
break;
|
|
1447
|
+
default:
|
|
1448
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
success: true,
|
|
1452
|
+
data,
|
|
1453
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1454
|
+
};
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
return this.handleError(action, error);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
calendarId(params) {
|
|
1460
|
+
return params.calendarId || this.defaultCalendarId;
|
|
1461
|
+
}
|
|
1462
|
+
toEventTime(value) {
|
|
1463
|
+
return value.length === 10 ? { date: value } : { dateTime: value };
|
|
1464
|
+
}
|
|
1465
|
+
fromEventTime(time) {
|
|
1466
|
+
return time?.dateTime ?? time?.date ?? "";
|
|
1467
|
+
}
|
|
1468
|
+
async listEvents(params) {
|
|
1469
|
+
const { timeMin, timeMax, syncToken, maxResults } = params;
|
|
1470
|
+
const response = await this.client.events.list({
|
|
1471
|
+
calendarId: this.calendarId(params),
|
|
1472
|
+
// Incremental sync: a syncToken supersedes the window params (the API
|
|
1473
|
+
// rejects combining them).
|
|
1474
|
+
...syncToken ? { syncToken } : {
|
|
1475
|
+
timeMin: timeMin || void 0,
|
|
1476
|
+
timeMax: timeMax || void 0,
|
|
1477
|
+
singleEvents: true,
|
|
1478
|
+
orderBy: "startTime"
|
|
1479
|
+
},
|
|
1480
|
+
maxResults: maxResults || 250
|
|
1481
|
+
});
|
|
1482
|
+
const items = response.data.items ?? [];
|
|
1483
|
+
return {
|
|
1484
|
+
events: items.map((event) => ({
|
|
1485
|
+
id: event.id ?? "",
|
|
1486
|
+
summary: event.summary ?? "",
|
|
1487
|
+
description: event.description ?? "",
|
|
1488
|
+
location: event.location ?? "",
|
|
1489
|
+
start: this.fromEventTime(event.start ?? void 0),
|
|
1490
|
+
end: this.fromEventTime(event.end ?? void 0),
|
|
1491
|
+
status: event.status ?? "",
|
|
1492
|
+
updated: event.updated ?? ""
|
|
1493
|
+
})),
|
|
1494
|
+
nextSyncToken: response.data.nextSyncToken ?? null
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
resolveEnd(start, end, durationMinutes) {
|
|
1498
|
+
if (end) return end;
|
|
1499
|
+
if (start.length === 10) {
|
|
1500
|
+
const next = new Date(Date.parse(start) + 24 * 60 * 6e4);
|
|
1501
|
+
return next.toISOString().slice(0, 10);
|
|
1502
|
+
}
|
|
1503
|
+
if (durationMinutes && durationMinutes > 0) {
|
|
1504
|
+
return new Date(Date.parse(start) + durationMinutes * 6e4).toISOString();
|
|
1505
|
+
}
|
|
1506
|
+
throw new Error("createEvent requires `end` or a positive `durationMinutes`");
|
|
1507
|
+
}
|
|
1508
|
+
async createEvent(params) {
|
|
1509
|
+
const { summary, description, location, start, end, durationMinutes } = params;
|
|
1510
|
+
const response = await this.client.events.insert({
|
|
1511
|
+
calendarId: this.calendarId(params),
|
|
1512
|
+
requestBody: {
|
|
1513
|
+
summary,
|
|
1514
|
+
description: description || void 0,
|
|
1515
|
+
location: location || void 0,
|
|
1516
|
+
start: this.toEventTime(start),
|
|
1517
|
+
end: this.toEventTime(
|
|
1518
|
+
this.resolveEnd(start, end || void 0, durationMinutes || void 0)
|
|
1519
|
+
)
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
return {
|
|
1523
|
+
id: response.data.id ?? "",
|
|
1524
|
+
status: response.data.status ?? "",
|
|
1525
|
+
htmlLink: response.data.htmlLink ?? ""
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
async updateEvent(params) {
|
|
1529
|
+
const { eventId, summary, description, location, start, end } = params;
|
|
1530
|
+
const requestBody = {};
|
|
1531
|
+
if (typeof summary === "string") requestBody.summary = summary;
|
|
1532
|
+
if (typeof description === "string") requestBody.description = description;
|
|
1533
|
+
if (typeof location === "string") requestBody.location = location;
|
|
1534
|
+
if (typeof start === "string" && start) requestBody.start = this.toEventTime(start);
|
|
1535
|
+
if (typeof end === "string" && end) requestBody.end = this.toEventTime(end);
|
|
1536
|
+
const response = await this.client.events.patch({
|
|
1537
|
+
calendarId: this.calendarId(params),
|
|
1538
|
+
eventId,
|
|
1539
|
+
requestBody
|
|
1540
|
+
});
|
|
1541
|
+
return {
|
|
1542
|
+
id: response.data.id ?? "",
|
|
1543
|
+
status: response.data.status ?? ""
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
async deleteEvent(params) {
|
|
1547
|
+
const { eventId } = params;
|
|
1548
|
+
await this.client.events.delete({
|
|
1549
|
+
calendarId: this.calendarId(params),
|
|
1550
|
+
eventId
|
|
1551
|
+
});
|
|
1552
|
+
return { id: eventId, deleted: true };
|
|
1553
|
+
}
|
|
1554
|
+
async freeBusy(params) {
|
|
1555
|
+
const { timeMin, timeMax } = params;
|
|
1556
|
+
const id = this.calendarId(params);
|
|
1557
|
+
const response = await this.client.freebusy.query({
|
|
1558
|
+
requestBody: {
|
|
1559
|
+
timeMin,
|
|
1560
|
+
timeMax,
|
|
1561
|
+
items: [{ id }]
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
const busy = response.data.calendars?.[id]?.busy ?? [];
|
|
1565
|
+
return {
|
|
1566
|
+
busy: busy.map((slot) => ({ start: slot.start ?? "", end: slot.end ?? "" }))
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
async watch(params) {
|
|
1570
|
+
const { channelId, address, ttlSeconds } = params;
|
|
1571
|
+
const response = await this.client.events.watch({
|
|
1572
|
+
calendarId: this.calendarId(params),
|
|
1573
|
+
requestBody: {
|
|
1574
|
+
id: channelId,
|
|
1575
|
+
type: "web_hook",
|
|
1576
|
+
address,
|
|
1577
|
+
params: ttlSeconds ? { ttl: String(ttlSeconds) } : void 0
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
return {
|
|
1581
|
+
channelId: response.data.id ?? channelId,
|
|
1582
|
+
resourceId: response.data.resourceId ?? "",
|
|
1583
|
+
expiration: response.data.expiration ?? ""
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
async stopWatch(params) {
|
|
1587
|
+
const { channelId, resourceId } = params;
|
|
1588
|
+
await this.client.channels.stop({
|
|
1589
|
+
requestBody: {
|
|
1590
|
+
id: channelId,
|
|
1591
|
+
resourceId
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
return { stopped: true };
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
registerIntegration("calendar", CalendarIntegration);
|
|
1598
|
+
function decodeContent(content) {
|
|
1599
|
+
const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
|
|
1600
|
+
if (dataUrlMatch) {
|
|
1601
|
+
const [, mime, isB64, body] = dataUrlMatch;
|
|
1602
|
+
const bytes = isB64 ? Buffer.from(body, "base64") : Buffer.from(decodeURIComponent(body), "utf8");
|
|
1603
|
+
return { bytes, contentType: mime || null };
|
|
1604
|
+
}
|
|
1605
|
+
return { bytes: Buffer.from(content, "utf8"), contentType: null };
|
|
1606
|
+
}
|
|
1607
|
+
var DriveIntegration = class extends BaseIntegration {
|
|
1608
|
+
constructor(config) {
|
|
1609
|
+
super(config);
|
|
1610
|
+
const rawKey = config.env.GOOGLE_DRIVE_SA_KEY;
|
|
1611
|
+
if (!rawKey) {
|
|
1612
|
+
throw new Error("GOOGLE_DRIVE_SA_KEY not configured");
|
|
1613
|
+
}
|
|
1614
|
+
const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
|
|
1615
|
+
const key = JSON.parse(keyJson);
|
|
1616
|
+
const subject = config.env.GOOGLE_DRIVE_SUBJECT || void 0;
|
|
1617
|
+
const auth = new google.auth.JWT({
|
|
1618
|
+
email: key.client_email,
|
|
1619
|
+
key: key.private_key,
|
|
1620
|
+
scopes: ["https://www.googleapis.com/auth/drive"],
|
|
1621
|
+
subject
|
|
1622
|
+
});
|
|
1623
|
+
this.client = google.drive({ version: "v3", auth });
|
|
1624
|
+
this.logger.info("Drive integration initialized", { delegated: Boolean(subject) });
|
|
1625
|
+
}
|
|
1626
|
+
async execute(action, params) {
|
|
1627
|
+
const validation = this.validateParams(action, params);
|
|
1628
|
+
if (!validation.valid) {
|
|
1629
|
+
return {
|
|
1630
|
+
success: false,
|
|
1631
|
+
error: {
|
|
1632
|
+
name: "IntegrationError",
|
|
1633
|
+
message: "Validation failed",
|
|
1634
|
+
code: "VALIDATION_ERROR",
|
|
1635
|
+
details: validation.errors
|
|
1636
|
+
},
|
|
1637
|
+
metadata: this.createMetadata(action, 0)
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
const startTime = Date.now();
|
|
1641
|
+
try {
|
|
1642
|
+
let data;
|
|
1643
|
+
switch (action) {
|
|
1644
|
+
case "listFiles":
|
|
1645
|
+
data = await this.executeWithRetry(() => this.listFiles(params));
|
|
1646
|
+
break;
|
|
1647
|
+
case "getFile":
|
|
1648
|
+
data = await this.executeWithRetry(() => this.getFile(params));
|
|
1649
|
+
break;
|
|
1650
|
+
case "uploadFile":
|
|
1651
|
+
data = await this.executeWithRetry(() => this.uploadFile(params));
|
|
1652
|
+
break;
|
|
1653
|
+
case "createFolder":
|
|
1654
|
+
data = await this.executeWithRetry(() => this.createFolder(params));
|
|
1655
|
+
break;
|
|
1656
|
+
case "shareFile":
|
|
1657
|
+
data = await this.executeWithRetry(() => this.shareFile(params));
|
|
1658
|
+
break;
|
|
1659
|
+
default:
|
|
1660
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1661
|
+
}
|
|
1662
|
+
return {
|
|
1663
|
+
success: true,
|
|
1664
|
+
data,
|
|
1665
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1666
|
+
};
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
return this.handleError(action, error);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async listFiles(params) {
|
|
1672
|
+
const { folderId, query, maxResults } = params;
|
|
1673
|
+
const clauses = ["trashed = false"];
|
|
1674
|
+
if (folderId) clauses.push(`'${String(folderId).replace(/'/g, "\\'")}' in parents`);
|
|
1675
|
+
if (query) clauses.push(String(query));
|
|
1676
|
+
const response = await this.client.files.list({
|
|
1677
|
+
q: clauses.join(" and "),
|
|
1678
|
+
pageSize: maxResults || 100,
|
|
1679
|
+
fields: "files(id, name, mimeType, size, modifiedTime, webViewLink)"
|
|
1680
|
+
});
|
|
1681
|
+
return {
|
|
1682
|
+
files: (response.data.files ?? []).map((file) => ({
|
|
1683
|
+
id: file.id ?? "",
|
|
1684
|
+
name: file.name ?? "",
|
|
1685
|
+
mimeType: file.mimeType ?? "",
|
|
1686
|
+
size: Number(file.size ?? 0),
|
|
1687
|
+
modifiedTime: file.modifiedTime ?? "",
|
|
1688
|
+
webViewLink: file.webViewLink ?? ""
|
|
1689
|
+
}))
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
async getFile(params) {
|
|
1693
|
+
const fileId = params.fileId;
|
|
1694
|
+
const meta = await this.client.files.get({
|
|
1695
|
+
fileId,
|
|
1696
|
+
fields: "id, name, mimeType, size"
|
|
1697
|
+
});
|
|
1698
|
+
const content = await this.client.files.get(
|
|
1699
|
+
{ fileId, alt: "media" },
|
|
1700
|
+
{ responseType: "arraybuffer" }
|
|
1701
|
+
);
|
|
1702
|
+
const bytes = Buffer.from(content.data);
|
|
1703
|
+
return {
|
|
1704
|
+
id: meta.data.id ?? fileId,
|
|
1705
|
+
name: meta.data.name ?? "",
|
|
1706
|
+
mimeType: meta.data.mimeType ?? "application/octet-stream",
|
|
1707
|
+
content: bytes.toString("base64"),
|
|
1708
|
+
size: bytes.length
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
async uploadFile(params) {
|
|
1712
|
+
const { name, content, mimeType, folderId } = params;
|
|
1713
|
+
const { bytes, contentType } = decodeContent(content);
|
|
1714
|
+
const response = await this.client.files.create({
|
|
1715
|
+
requestBody: {
|
|
1716
|
+
name,
|
|
1717
|
+
parents: folderId ? [folderId] : void 0
|
|
1718
|
+
},
|
|
1719
|
+
media: {
|
|
1720
|
+
mimeType: mimeType || contentType || "application/octet-stream",
|
|
1721
|
+
body: Readable.from(bytes)
|
|
1722
|
+
},
|
|
1723
|
+
fields: "id, name, webViewLink"
|
|
1724
|
+
});
|
|
1725
|
+
return {
|
|
1726
|
+
id: response.data.id ?? "",
|
|
1727
|
+
name: response.data.name ?? name,
|
|
1728
|
+
webViewLink: response.data.webViewLink ?? ""
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
async createFolder(params) {
|
|
1732
|
+
const { name, parentId } = params;
|
|
1733
|
+
const response = await this.client.files.create({
|
|
1734
|
+
requestBody: {
|
|
1735
|
+
name,
|
|
1736
|
+
mimeType: "application/vnd.google-apps.folder",
|
|
1737
|
+
parents: parentId ? [parentId] : void 0
|
|
1738
|
+
},
|
|
1739
|
+
fields: "id, name"
|
|
1740
|
+
});
|
|
1741
|
+
return { id: response.data.id ?? "", name: response.data.name ?? name };
|
|
1742
|
+
}
|
|
1743
|
+
async shareFile(params) {
|
|
1744
|
+
const { fileId, email, role } = params;
|
|
1745
|
+
const response = await this.client.permissions.create({
|
|
1746
|
+
fileId,
|
|
1747
|
+
requestBody: {
|
|
1748
|
+
type: "user",
|
|
1749
|
+
role: role || "reader",
|
|
1750
|
+
emailAddress: email
|
|
1751
|
+
},
|
|
1752
|
+
fields: "id"
|
|
1753
|
+
});
|
|
1754
|
+
return { shared: true, permissionId: response.data.id ?? "" };
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
registerIntegration("drive", DriveIntegration);
|
|
1758
|
+
|
|
1759
|
+
// src/integrations/metaAds/index.ts
|
|
1760
|
+
var GRAPH_BASE = "https://graph.facebook.com/v21.0";
|
|
1761
|
+
var MetaAdsIntegration = class extends BaseIntegration {
|
|
1762
|
+
constructor(config) {
|
|
1763
|
+
super(config);
|
|
1764
|
+
this.accessToken = config.env.META_ACCESS_TOKEN || "";
|
|
1765
|
+
if (!this.accessToken) {
|
|
1766
|
+
throw new Error("META_ACCESS_TOKEN not configured");
|
|
1767
|
+
}
|
|
1768
|
+
this.defaultAccountId = config.env.META_AD_ACCOUNT_ID || "";
|
|
1769
|
+
this.logger.info("Meta Ads integration initialized");
|
|
1770
|
+
}
|
|
1771
|
+
async execute(action, params) {
|
|
1772
|
+
const validation = this.validateParams(action, params);
|
|
1773
|
+
if (!validation.valid) {
|
|
1774
|
+
return {
|
|
1775
|
+
success: false,
|
|
1776
|
+
error: {
|
|
1777
|
+
name: "IntegrationError",
|
|
1778
|
+
message: "Validation failed",
|
|
1779
|
+
code: "VALIDATION_ERROR",
|
|
1780
|
+
details: validation.errors
|
|
1781
|
+
},
|
|
1782
|
+
metadata: this.createMetadata(action, 0)
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
const startTime = Date.now();
|
|
1786
|
+
try {
|
|
1787
|
+
let data;
|
|
1788
|
+
switch (action) {
|
|
1789
|
+
case "getSpend":
|
|
1790
|
+
data = await this.executeWithRetry(() => this.getSpend(params));
|
|
1791
|
+
break;
|
|
1792
|
+
case "listCampaigns":
|
|
1793
|
+
data = await this.executeWithRetry(() => this.listCampaigns(params));
|
|
1794
|
+
break;
|
|
1795
|
+
default:
|
|
1796
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1797
|
+
}
|
|
1798
|
+
return {
|
|
1799
|
+
success: true,
|
|
1800
|
+
data,
|
|
1801
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1802
|
+
};
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
return this.handleError(action, error);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
accountId(params) {
|
|
1808
|
+
const id = params.accountId || this.defaultAccountId;
|
|
1809
|
+
if (!id) {
|
|
1810
|
+
throw new Error("No ad account: pass `accountId` or set META_AD_ACCOUNT_ID");
|
|
1811
|
+
}
|
|
1812
|
+
return id.startsWith("act_") ? id : `act_${id}`;
|
|
1813
|
+
}
|
|
1814
|
+
async graphGet(path, query) {
|
|
1815
|
+
const url = new URL(`${GRAPH_BASE}/${path}`);
|
|
1816
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1817
|
+
url.searchParams.set(key, value);
|
|
1818
|
+
}
|
|
1819
|
+
url.searchParams.set("access_token", this.accessToken);
|
|
1820
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
1821
|
+
if (response.status >= 500) {
|
|
1822
|
+
throw new Error(`Meta Graph API returned ${response.status}`);
|
|
1823
|
+
}
|
|
1824
|
+
const body = await response.json();
|
|
1825
|
+
if (!response.ok) {
|
|
1826
|
+
throw new Error(`Meta Graph API error: ${body.error?.message ?? response.status}`);
|
|
1827
|
+
}
|
|
1828
|
+
return body.data ?? body;
|
|
1829
|
+
}
|
|
1830
|
+
async getSpend(params) {
|
|
1831
|
+
const { since, until } = params;
|
|
1832
|
+
const rows = await this.graphGet(`${this.accountId(params)}/insights`, {
|
|
1833
|
+
fields: "spend,impressions,clicks,account_currency",
|
|
1834
|
+
time_range: JSON.stringify({ since, until }),
|
|
1835
|
+
level: "account"
|
|
1836
|
+
});
|
|
1837
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
1838
|
+
return {
|
|
1839
|
+
spend: Number(row?.spend ?? 0),
|
|
1840
|
+
currency: row?.account_currency ?? "",
|
|
1841
|
+
impressions: Number(row?.impressions ?? 0),
|
|
1842
|
+
clicks: Number(row?.clicks ?? 0)
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
async listCampaigns(params) {
|
|
1846
|
+
const { status } = params;
|
|
1847
|
+
const rows = await this.graphGet(`${this.accountId(params)}/campaigns`, {
|
|
1848
|
+
fields: "id,name,status,daily_budget",
|
|
1849
|
+
...status ? { effective_status: JSON.stringify([status]) } : {}
|
|
1850
|
+
});
|
|
1851
|
+
return {
|
|
1852
|
+
campaigns: (Array.isArray(rows) ? rows : []).map((row) => ({
|
|
1853
|
+
id: row.id ?? "",
|
|
1854
|
+
name: row.name ?? "",
|
|
1855
|
+
status: row.status ?? "",
|
|
1856
|
+
// Meta reports budgets in minor units (cents).
|
|
1857
|
+
dailyBudget: Number(row.daily_budget ?? 0) / 100
|
|
1858
|
+
}))
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
registerIntegration("metaAds", MetaAdsIntegration);
|
|
1863
|
+
|
|
1864
|
+
// src/integrations/accounting/index.ts
|
|
1865
|
+
var AccountingIntegration = class extends BaseIntegration {
|
|
1866
|
+
constructor(config) {
|
|
1867
|
+
super(config);
|
|
1868
|
+
this.logger.info("Accounting integration initialized (generic CSV export)");
|
|
1869
|
+
}
|
|
1870
|
+
async execute(action, params) {
|
|
1871
|
+
const validation = this.validateParams(action, params);
|
|
1872
|
+
if (!validation.valid) {
|
|
1873
|
+
return {
|
|
1874
|
+
success: false,
|
|
1875
|
+
error: {
|
|
1876
|
+
name: "IntegrationError",
|
|
1877
|
+
message: "Validation failed",
|
|
1878
|
+
code: "VALIDATION_ERROR",
|
|
1879
|
+
details: validation.errors
|
|
1880
|
+
},
|
|
1881
|
+
metadata: this.createMetadata(action, 0)
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
const startTime = Date.now();
|
|
1885
|
+
try {
|
|
1886
|
+
let data;
|
|
1887
|
+
switch (action) {
|
|
1888
|
+
case "exportInvoices":
|
|
1889
|
+
data = this.exportRows(params.invoices, INVOICE_COLUMNS, "invoices");
|
|
1890
|
+
break;
|
|
1891
|
+
case "exportJournal":
|
|
1892
|
+
data = this.exportRows(params.entries, JOURNAL_COLUMNS, "journal");
|
|
1893
|
+
break;
|
|
1894
|
+
default:
|
|
1895
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1896
|
+
}
|
|
1897
|
+
return {
|
|
1898
|
+
success: true,
|
|
1899
|
+
data,
|
|
1900
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1901
|
+
};
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
return this.handleError(action, error);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
exportRows(rowsValue, columns, kind) {
|
|
1907
|
+
if (!Array.isArray(rowsValue)) {
|
|
1908
|
+
throw new Error(`${kind} export requires an array of rows`);
|
|
1909
|
+
}
|
|
1910
|
+
const lines = [columns.join(",")];
|
|
1911
|
+
for (const row of rowsValue) {
|
|
1912
|
+
if (row === null || typeof row !== "object" || Array.isArray(row) || row instanceof Date) {
|
|
1913
|
+
throw new Error(`${kind} export: every row must be an object`);
|
|
1914
|
+
}
|
|
1915
|
+
const record = row;
|
|
1916
|
+
lines.push(columns.map((column) => csvCell(record[column])).join(","));
|
|
1917
|
+
}
|
|
1918
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1919
|
+
return {
|
|
1920
|
+
content: lines.join("\r\n") + "\r\n",
|
|
1921
|
+
filename: `${kind}-export-${stamp}.csv`,
|
|
1922
|
+
count: rowsValue.length
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
var INVOICE_COLUMNS = ["id", "number", "customer", "issuedAt", "dueAt", "currency", "net", "tax", "gross", "status"];
|
|
1927
|
+
var JOURNAL_COLUMNS = ["date", "account", "description", "debit", "credit", "reference"];
|
|
1928
|
+
function csvCell(value) {
|
|
1929
|
+
if (value === void 0 || value === null) return "";
|
|
1930
|
+
const raw = value instanceof Date ? value.toISOString() : String(value);
|
|
1931
|
+
return /[",\r\n]/.test(raw) ? `"${raw.replace(/"/g, '""')}"` : raw;
|
|
1932
|
+
}
|
|
1933
|
+
registerIntegration("accounting", AccountingIntegration);
|
|
1934
|
+
|
|
1935
|
+
// src/integrations/banking/index.ts
|
|
1936
|
+
var GC_BASE = "https://bankaccountdata.gocardless.com/api/v2";
|
|
1937
|
+
var BankingIntegration = class extends BaseIntegration {
|
|
1938
|
+
constructor(config) {
|
|
1939
|
+
super(config);
|
|
1940
|
+
this.accessToken = null;
|
|
1941
|
+
this.accessTokenExpiresAt = 0;
|
|
1942
|
+
this.secretId = config.env.GOCARDLESS_SECRET_ID || "";
|
|
1943
|
+
this.secretKey = config.env.GOCARDLESS_SECRET_KEY || "";
|
|
1944
|
+
if (!this.secretId || !this.secretKey) {
|
|
1945
|
+
throw new Error("GOCARDLESS_SECRET_ID / GOCARDLESS_SECRET_KEY not configured");
|
|
1946
|
+
}
|
|
1947
|
+
this.logger.info("Banking integration initialized (GoCardless Bank Account Data)");
|
|
1948
|
+
}
|
|
1949
|
+
async execute(action, params) {
|
|
1950
|
+
const validation = this.validateParams(action, params);
|
|
1951
|
+
if (!validation.valid) {
|
|
1952
|
+
return {
|
|
1953
|
+
success: false,
|
|
1954
|
+
error: {
|
|
1955
|
+
name: "IntegrationError",
|
|
1956
|
+
message: "Validation failed",
|
|
1957
|
+
code: "VALIDATION_ERROR",
|
|
1958
|
+
details: validation.errors
|
|
1959
|
+
},
|
|
1960
|
+
metadata: this.createMetadata(action, 0)
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
const startTime = Date.now();
|
|
1964
|
+
try {
|
|
1965
|
+
let data;
|
|
1966
|
+
switch (action) {
|
|
1967
|
+
case "createRequisition":
|
|
1968
|
+
data = await this.executeWithRetry(() => this.createRequisition(params));
|
|
1969
|
+
break;
|
|
1970
|
+
case "listAccounts":
|
|
1971
|
+
data = await this.executeWithRetry(() => this.listAccounts(params));
|
|
1972
|
+
break;
|
|
1973
|
+
case "listTransactions":
|
|
1974
|
+
data = await this.executeWithRetry(() => this.listTransactions(params));
|
|
1975
|
+
break;
|
|
1976
|
+
default:
|
|
1977
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1978
|
+
}
|
|
1979
|
+
return {
|
|
1980
|
+
success: true,
|
|
1981
|
+
data,
|
|
1982
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1983
|
+
};
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
return this.handleError(action, error);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
async token() {
|
|
1989
|
+
if (this.accessToken && Date.now() < this.accessTokenExpiresAt - 6e4) {
|
|
1990
|
+
return this.accessToken;
|
|
1991
|
+
}
|
|
1992
|
+
const response = await fetch(`${GC_BASE}/token/new/`, {
|
|
1993
|
+
method: "POST",
|
|
1994
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1995
|
+
body: JSON.stringify({ secret_id: this.secretId, secret_key: this.secretKey }),
|
|
1996
|
+
signal: AbortSignal.timeout(15e3)
|
|
1997
|
+
});
|
|
1998
|
+
if (!response.ok) {
|
|
1999
|
+
throw new Error(`GoCardless token request failed: ${response.status}`);
|
|
2000
|
+
}
|
|
2001
|
+
const body = await response.json();
|
|
2002
|
+
this.accessToken = body.access;
|
|
2003
|
+
this.accessTokenExpiresAt = Date.now() + body.access_expires * 1e3;
|
|
2004
|
+
return this.accessToken;
|
|
2005
|
+
}
|
|
2006
|
+
async gcRequest(path, init2) {
|
|
2007
|
+
const response = await fetch(`${GC_BASE}${path}`, {
|
|
2008
|
+
method: init2?.method ?? "GET",
|
|
2009
|
+
headers: {
|
|
2010
|
+
Accept: "application/json",
|
|
2011
|
+
"Content-Type": "application/json",
|
|
2012
|
+
Authorization: `Bearer ${await this.token()}`
|
|
2013
|
+
},
|
|
2014
|
+
body: init2?.body === void 0 ? void 0 : JSON.stringify(init2.body),
|
|
2015
|
+
signal: AbortSignal.timeout(2e4)
|
|
2016
|
+
});
|
|
2017
|
+
if (response.status >= 500) {
|
|
2018
|
+
throw new Error(`GoCardless returned ${response.status}`);
|
|
2019
|
+
}
|
|
2020
|
+
const body = await response.json();
|
|
2021
|
+
if (!response.ok) {
|
|
2022
|
+
throw new Error(`GoCardless error ${response.status}: ${JSON.stringify(body).slice(0, 300)}`);
|
|
2023
|
+
}
|
|
2024
|
+
return body;
|
|
2025
|
+
}
|
|
2026
|
+
async createRequisition(params) {
|
|
2027
|
+
const { institutionId, redirectUrl, reference } = params;
|
|
2028
|
+
const body = await this.gcRequest("/requisitions/", {
|
|
2029
|
+
method: "POST",
|
|
2030
|
+
body: {
|
|
2031
|
+
institution_id: institutionId,
|
|
2032
|
+
redirect: redirectUrl,
|
|
2033
|
+
reference: reference || void 0
|
|
2034
|
+
}
|
|
884
2035
|
});
|
|
2036
|
+
return { requisitionId: body.id ?? "", link: body.link ?? "" };
|
|
2037
|
+
}
|
|
2038
|
+
async listAccounts(params) {
|
|
2039
|
+
const { requisitionId } = params;
|
|
2040
|
+
const body = await this.gcRequest(`/requisitions/${requisitionId}/`);
|
|
2041
|
+
return { accounts: body.accounts ?? [] };
|
|
2042
|
+
}
|
|
2043
|
+
async listTransactions(params) {
|
|
2044
|
+
const { accountId, dateFrom, dateTo } = params;
|
|
2045
|
+
const query = new URLSearchParams();
|
|
2046
|
+
if (dateFrom) query.set("date_from", dateFrom);
|
|
2047
|
+
if (dateTo) query.set("date_to", dateTo);
|
|
2048
|
+
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
|
2049
|
+
const body = await this.gcRequest(`/accounts/${accountId}/transactions/${suffix}`);
|
|
885
2050
|
return {
|
|
886
|
-
|
|
887
|
-
|
|
2051
|
+
transactions: (body.transactions?.booked ?? []).map((tx) => ({
|
|
2052
|
+
id: tx.transactionId ?? tx.internalTransactionId ?? "",
|
|
2053
|
+
amount: Number(tx.transactionAmount?.amount ?? 0),
|
|
2054
|
+
currency: tx.transactionAmount?.currency ?? "",
|
|
2055
|
+
date: tx.bookingDate ?? "",
|
|
2056
|
+
description: tx.remittanceInformationUnstructured ?? "",
|
|
2057
|
+
counterparty: tx.creditorName ?? tx.debtorName ?? ""
|
|
2058
|
+
}))
|
|
888
2059
|
};
|
|
889
2060
|
}
|
|
890
2061
|
};
|
|
891
|
-
registerIntegration("
|
|
892
|
-
|
|
2062
|
+
registerIntegration("banking", BankingIntegration);
|
|
2063
|
+
|
|
2064
|
+
// src/integrations/esign/index.ts
|
|
2065
|
+
var EsignIntegration = class extends BaseIntegration {
|
|
893
2066
|
constructor(config) {
|
|
894
2067
|
super(config);
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (this.provider === "sendgrid") {
|
|
898
|
-
const apiKey = config.env.SENDGRID_API_KEY;
|
|
899
|
-
if (!apiKey) {
|
|
900
|
-
throw new Error("SENDGRID_API_KEY not configured");
|
|
901
|
-
}
|
|
902
|
-
sgMail.setApiKey(apiKey);
|
|
903
|
-
} else if (this.provider === "resend") {
|
|
904
|
-
const apiKey = config.env.RESEND_API_KEY;
|
|
905
|
-
if (!apiKey) {
|
|
906
|
-
throw new Error("RESEND_API_KEY not configured");
|
|
907
|
-
}
|
|
908
|
-
this.resendClient = new Resend(apiKey);
|
|
2068
|
+
if (!config.env.DOCUSIGN_BASE_URL || !config.env.DOCUSIGN_ACCESS_TOKEN) {
|
|
2069
|
+
throw new Error("DOCUSIGN_BASE_URL / DOCUSIGN_ACCESS_TOKEN not configured");
|
|
909
2070
|
}
|
|
910
|
-
this.logger.info(
|
|
2071
|
+
this.logger.info("E-sign integration initialized (DocuSign)");
|
|
911
2072
|
}
|
|
912
2073
|
async execute(action, params) {
|
|
913
2074
|
const validation = this.validateParams(action, params);
|
|
@@ -924,12 +2085,17 @@ var EmailIntegration = class extends BaseIntegration {
|
|
|
924
2085
|
};
|
|
925
2086
|
}
|
|
926
2087
|
const startTime = Date.now();
|
|
927
|
-
let retries = 0;
|
|
928
2088
|
try {
|
|
929
2089
|
let data;
|
|
930
2090
|
switch (action) {
|
|
931
|
-
case "
|
|
932
|
-
data = await this.executeWithRetry(() => this.
|
|
2091
|
+
case "sendEnvelope":
|
|
2092
|
+
data = await this.executeWithRetry(() => this.sendEnvelope(params));
|
|
2093
|
+
break;
|
|
2094
|
+
case "getEnvelopeStatus":
|
|
2095
|
+
data = await this.executeWithRetry(() => this.getEnvelopeStatus(params));
|
|
2096
|
+
break;
|
|
2097
|
+
case "downloadDocument":
|
|
2098
|
+
data = await this.executeWithRetry(() => this.downloadDocument(params));
|
|
933
2099
|
break;
|
|
934
2100
|
default:
|
|
935
2101
|
throw new Error(`Unknown action: ${action}`);
|
|
@@ -937,62 +2103,81 @@ var EmailIntegration = class extends BaseIntegration {
|
|
|
937
2103
|
return {
|
|
938
2104
|
success: true,
|
|
939
2105
|
data,
|
|
940
|
-
metadata: this.createMetadata(action, Date.now() - startTime
|
|
2106
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
941
2107
|
};
|
|
942
2108
|
} catch (error) {
|
|
943
2109
|
return this.handleError(action, error);
|
|
944
2110
|
}
|
|
945
2111
|
}
|
|
946
|
-
async
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
)
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
body,
|
|
961
|
-
from || this.fromEmail
|
|
962
|
-
);
|
|
2112
|
+
async dsRequest(path, init2) {
|
|
2113
|
+
const base = this.config.env.DOCUSIGN_BASE_URL.replace(/\/$/, "");
|
|
2114
|
+
const response = await fetch(`${base}${path}`, {
|
|
2115
|
+
method: init2?.method ?? "GET",
|
|
2116
|
+
headers: {
|
|
2117
|
+
Accept: init2?.raw ? "application/pdf" : "application/json",
|
|
2118
|
+
"Content-Type": "application/json",
|
|
2119
|
+
Authorization: `Bearer ${this.config.env.DOCUSIGN_ACCESS_TOKEN}`
|
|
2120
|
+
},
|
|
2121
|
+
body: init2?.body === void 0 ? void 0 : JSON.stringify(init2.body),
|
|
2122
|
+
signal: AbortSignal.timeout(3e4)
|
|
2123
|
+
});
|
|
2124
|
+
if (response.status >= 500) {
|
|
2125
|
+
throw new Error(`DocuSign returned ${response.status}`);
|
|
963
2126
|
}
|
|
964
|
-
|
|
2127
|
+
if (!response.ok) {
|
|
2128
|
+
const detail = await response.text();
|
|
2129
|
+
throw new Error(`DocuSign error ${response.status}: ${detail.slice(0, 300)}`);
|
|
2130
|
+
}
|
|
2131
|
+
if (init2?.raw) {
|
|
2132
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2133
|
+
}
|
|
2134
|
+
return response.json();
|
|
965
2135
|
}
|
|
966
|
-
async
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
2136
|
+
async sendEnvelope(params) {
|
|
2137
|
+
const { recipientEmail, recipientName, documentName, documentContent, emailSubject } = params;
|
|
2138
|
+
const rawContent = documentContent;
|
|
2139
|
+
const base64 = rawContent.startsWith("data:") ? rawContent.slice(rawContent.indexOf(",") + 1) : rawContent;
|
|
2140
|
+
const body = await this.dsRequest("/envelopes", {
|
|
2141
|
+
method: "POST",
|
|
2142
|
+
body: {
|
|
2143
|
+
emailSubject: emailSubject || `Please sign: ${documentName}`,
|
|
2144
|
+
status: "sent",
|
|
2145
|
+
documents: [
|
|
2146
|
+
{
|
|
2147
|
+
documentBase64: base64,
|
|
2148
|
+
name: documentName,
|
|
2149
|
+
fileExtension: String(documentName).split(".").pop() || "pdf",
|
|
2150
|
+
documentId: "1"
|
|
2151
|
+
}
|
|
2152
|
+
],
|
|
2153
|
+
recipients: {
|
|
2154
|
+
signers: [
|
|
2155
|
+
{
|
|
2156
|
+
email: recipientEmail,
|
|
2157
|
+
name: recipientName,
|
|
2158
|
+
recipientId: "1",
|
|
2159
|
+
routingOrder: "1"
|
|
2160
|
+
}
|
|
2161
|
+
]
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
return { envelopeId: body.envelopeId ?? "", status: body.status ?? "sent" };
|
|
978
2166
|
}
|
|
979
|
-
async
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
2167
|
+
async getEnvelopeStatus(params) {
|
|
2168
|
+
const { envelopeId } = params;
|
|
2169
|
+
const body = await this.dsRequest(`/envelopes/${envelopeId}`);
|
|
2170
|
+
return { status: body.status ?? "", completedAt: body.completedDateTime ?? "" };
|
|
2171
|
+
}
|
|
2172
|
+
async downloadDocument(params) {
|
|
2173
|
+
const { envelopeId } = params;
|
|
2174
|
+
const bytes = await this.dsRequest(`/envelopes/${envelopeId}/documents/combined`, {
|
|
2175
|
+
raw: true
|
|
988
2176
|
});
|
|
989
|
-
return {
|
|
990
|
-
id: response.data?.id,
|
|
991
|
-
status: "sent"
|
|
992
|
-
};
|
|
2177
|
+
return { content: bytes.toString("base64"), documentName: `envelope-${envelopeId}.pdf` };
|
|
993
2178
|
}
|
|
994
2179
|
};
|
|
995
|
-
registerIntegration("
|
|
2180
|
+
registerIntegration("esign", EsignIntegration);
|
|
996
2181
|
var LLMIntegration = class extends BaseIntegration {
|
|
997
2182
|
constructor(config) {
|
|
998
2183
|
super(config);
|
|
@@ -2563,25 +3748,35 @@ var OtelIntegration = class extends BaseIntegration {
|
|
|
2563
3748
|
}
|
|
2564
3749
|
};
|
|
2565
3750
|
registerIntegration("otel", OtelIntegration);
|
|
2566
|
-
|
|
2567
|
-
// src/integrations/oauth/index.ts
|
|
2568
3751
|
var PROVIDER_AUTH_URLS = {
|
|
2569
3752
|
google: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
2570
3753
|
github: "https://github.com/login/oauth/authorize",
|
|
2571
3754
|
auth0: "https://auth.example.com/authorize"
|
|
2572
3755
|
};
|
|
3756
|
+
var PROVIDER_ISSUERS = {
|
|
3757
|
+
google: "https://accounts.google.com"
|
|
3758
|
+
};
|
|
2573
3759
|
var OAuthIntegration = class extends BaseIntegration {
|
|
2574
3760
|
constructor(config) {
|
|
2575
3761
|
super(config);
|
|
2576
|
-
/** Maps state token -> provider for pending authorization flows */
|
|
3762
|
+
/** Maps state token -> provider for pending MOCK authorization flows */
|
|
2577
3763
|
this.states = /* @__PURE__ */ new Map();
|
|
2578
|
-
/** Maps access token -> token set */
|
|
3764
|
+
/** Maps access token -> token set (mock backend) */
|
|
2579
3765
|
this.tokens = /* @__PURE__ */ new Map();
|
|
2580
|
-
/** Maps refresh token -> access token for refresh lookups */
|
|
3766
|
+
/** Maps refresh token -> access token for refresh lookups (mock backend) */
|
|
2581
3767
|
this.refreshIndex = /* @__PURE__ */ new Map();
|
|
2582
3768
|
/** Maps access token -> mock user session */
|
|
2583
3769
|
this.sessions = /* @__PURE__ */ new Map();
|
|
2584
|
-
|
|
3770
|
+
/** Maps state -> pending OIDC authorization (real backend) */
|
|
3771
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
3772
|
+
/** Maps access token -> ID-token subject, for userinfo subject checks */
|
|
3773
|
+
this.subjects = /* @__PURE__ */ new Map();
|
|
3774
|
+
/** Discovered issuer configurations, keyed by issuer URL */
|
|
3775
|
+
this.discovered = /* @__PURE__ */ new Map();
|
|
3776
|
+
this.real = config.env.OAUTH_MODE !== "mock" && Boolean(config.env.OAUTH_CLIENT_ID) && Boolean(config.env.OAUTH_CLIENT_SECRET);
|
|
3777
|
+
this.logger.info(
|
|
3778
|
+
this.real ? "OAuth integration initialized (OIDC backend via openid-client)" : "OAuth integration initialized (mock backend)"
|
|
3779
|
+
);
|
|
2585
3780
|
}
|
|
2586
3781
|
async execute(action, params) {
|
|
2587
3782
|
const validation = this.validateParams(action, params);
|
|
@@ -2602,19 +3797,19 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2602
3797
|
let data;
|
|
2603
3798
|
switch (action) {
|
|
2604
3799
|
case "authorize":
|
|
2605
|
-
data = await this.executeWithRetry(() => this.authorize(params));
|
|
3800
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcAuthorize(params) : this.authorize(params));
|
|
2606
3801
|
break;
|
|
2607
3802
|
case "token":
|
|
2608
|
-
data = await this.executeWithRetry(() => this.token(params));
|
|
3803
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcToken(params) : this.token(params));
|
|
2609
3804
|
break;
|
|
2610
3805
|
case "refresh":
|
|
2611
|
-
data = await this.executeWithRetry(() => this.refresh(params));
|
|
3806
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcRefresh(params) : this.refresh(params));
|
|
2612
3807
|
break;
|
|
2613
3808
|
case "revoke":
|
|
2614
|
-
data = await this.executeWithRetry(() => this.revoke(params));
|
|
3809
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcRevoke(params) : this.revoke(params));
|
|
2615
3810
|
break;
|
|
2616
3811
|
case "userinfo":
|
|
2617
|
-
data = await this.executeWithRetry(() => this.userinfo(params));
|
|
3812
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcUserinfo(params) : this.userinfo(params));
|
|
2618
3813
|
break;
|
|
2619
3814
|
default:
|
|
2620
3815
|
throw new Error(`Unknown action: ${action}`);
|
|
@@ -2629,7 +3824,119 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2629
3824
|
}
|
|
2630
3825
|
}
|
|
2631
3826
|
// ---------------------------------------------------------------------------
|
|
2632
|
-
//
|
|
3827
|
+
// OIDC backend (openid-client)
|
|
3828
|
+
// ---------------------------------------------------------------------------
|
|
3829
|
+
issuerFor(provider) {
|
|
3830
|
+
const configured = this.config.env.OIDC_ISSUER_URL;
|
|
3831
|
+
if (configured) return configured;
|
|
3832
|
+
const issuer = PROVIDER_ISSUERS[provider];
|
|
3833
|
+
if (!issuer) {
|
|
3834
|
+
throw new Error(
|
|
3835
|
+
`Provider "${provider}" has no OIDC issuer \u2014 set OIDC_ISSUER_URL to an OIDC-compliant issuer, or use OAUTH_MODE=mock`
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3838
|
+
return issuer;
|
|
3839
|
+
}
|
|
3840
|
+
async configurationFor(provider) {
|
|
3841
|
+
const issuer = this.issuerFor(provider);
|
|
3842
|
+
const cached = this.discovered.get(issuer);
|
|
3843
|
+
if (cached) return cached;
|
|
3844
|
+
const configuration = await oidc.discovery(
|
|
3845
|
+
new URL(issuer),
|
|
3846
|
+
this.config.env.OAUTH_CLIENT_ID,
|
|
3847
|
+
this.config.env.OAUTH_CLIENT_SECRET
|
|
3848
|
+
);
|
|
3849
|
+
this.discovered.set(issuer, configuration);
|
|
3850
|
+
return configuration;
|
|
3851
|
+
}
|
|
3852
|
+
async oidcAuthorize(params) {
|
|
3853
|
+
const provider = params.provider;
|
|
3854
|
+
const scopes = params.scopes;
|
|
3855
|
+
const redirectUri = params.redirectUri || this.config.env.OAUTH_REDIRECT_URI;
|
|
3856
|
+
const configuration = await this.configurationFor(provider);
|
|
3857
|
+
const state = oidc.randomState();
|
|
3858
|
+
const pkceVerifier = oidc.randomPKCECodeVerifier();
|
|
3859
|
+
const codeChallenge = await oidc.calculatePKCECodeChallenge(pkceVerifier);
|
|
3860
|
+
const parameters = {
|
|
3861
|
+
redirect_uri: redirectUri,
|
|
3862
|
+
scope: scopes.join(" "),
|
|
3863
|
+
state,
|
|
3864
|
+
code_challenge: codeChallenge,
|
|
3865
|
+
code_challenge_method: "S256"
|
|
3866
|
+
};
|
|
3867
|
+
if (provider === "google") {
|
|
3868
|
+
parameters.access_type = "offline";
|
|
3869
|
+
parameters.prompt = "consent";
|
|
3870
|
+
}
|
|
3871
|
+
const authUrl = oidc.buildAuthorizationUrl(configuration, parameters);
|
|
3872
|
+
this.pending.set(state, { provider, redirectUri, pkceVerifier });
|
|
3873
|
+
return { authUrl: authUrl.toString(), state };
|
|
3874
|
+
}
|
|
3875
|
+
async oidcToken(params) {
|
|
3876
|
+
const code = params.code;
|
|
3877
|
+
const state = params.state;
|
|
3878
|
+
const pendingAuth = this.pending.get(state);
|
|
3879
|
+
if (!pendingAuth) {
|
|
3880
|
+
throw new Error(`Invalid or expired state token: ${state}`);
|
|
3881
|
+
}
|
|
3882
|
+
this.pending.delete(state);
|
|
3883
|
+
const configuration = await this.configurationFor(pendingAuth.provider);
|
|
3884
|
+
const callbackUrl = new URL(pendingAuth.redirectUri);
|
|
3885
|
+
callbackUrl.searchParams.set("code", code);
|
|
3886
|
+
callbackUrl.searchParams.set("state", state);
|
|
3887
|
+
const tokens = await oidc.authorizationCodeGrant(configuration, callbackUrl, {
|
|
3888
|
+
expectedState: state,
|
|
3889
|
+
pkceCodeVerifier: pendingAuth.pkceVerifier
|
|
3890
|
+
});
|
|
3891
|
+
const claims = tokens.claims();
|
|
3892
|
+
if (claims?.sub) {
|
|
3893
|
+
this.subjects.set(tokens.access_token, claims.sub);
|
|
3894
|
+
}
|
|
3895
|
+
return {
|
|
3896
|
+
accessToken: tokens.access_token,
|
|
3897
|
+
refreshToken: tokens.refresh_token ?? "",
|
|
3898
|
+
expiresIn: tokens.expires_in ?? 3600,
|
|
3899
|
+
tokenType: "bearer"
|
|
3900
|
+
};
|
|
3901
|
+
}
|
|
3902
|
+
async oidcRefresh(params) {
|
|
3903
|
+
const refreshToken = params.refreshToken;
|
|
3904
|
+
const configuration = await this.configurationFor("google");
|
|
3905
|
+
const tokens = await oidc.refreshTokenGrant(configuration, refreshToken);
|
|
3906
|
+
const claims = tokens.claims();
|
|
3907
|
+
if (claims?.sub) {
|
|
3908
|
+
this.subjects.set(tokens.access_token, claims.sub);
|
|
3909
|
+
}
|
|
3910
|
+
return {
|
|
3911
|
+
accessToken: tokens.access_token,
|
|
3912
|
+
expiresIn: tokens.expires_in ?? 3600
|
|
3913
|
+
};
|
|
3914
|
+
}
|
|
3915
|
+
async oidcRevoke(params) {
|
|
3916
|
+
const token = params.token;
|
|
3917
|
+
const configuration = await this.configurationFor("google");
|
|
3918
|
+
await oidc.tokenRevocation(configuration, token);
|
|
3919
|
+
this.subjects.delete(token);
|
|
3920
|
+
return { revoked: true };
|
|
3921
|
+
}
|
|
3922
|
+
async oidcUserinfo(params) {
|
|
3923
|
+
const accessToken = params.accessToken;
|
|
3924
|
+
const configuration = await this.configurationFor("google");
|
|
3925
|
+
const subject = this.subjects.get(accessToken);
|
|
3926
|
+
const info = await oidc.fetchUserInfo(
|
|
3927
|
+
configuration,
|
|
3928
|
+
accessToken,
|
|
3929
|
+
subject ?? oidc.skipSubjectCheck
|
|
3930
|
+
);
|
|
3931
|
+
return {
|
|
3932
|
+
sub: info.sub,
|
|
3933
|
+
email: typeof info.email === "string" ? info.email : "",
|
|
3934
|
+
name: typeof info.name === "string" ? info.name : "",
|
|
3935
|
+
picture: typeof info.picture === "string" ? info.picture : ""
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
// ---------------------------------------------------------------------------
|
|
3939
|
+
// Mock backend helpers
|
|
2633
3940
|
// ---------------------------------------------------------------------------
|
|
2634
3941
|
/** Generate a random hex token of the given byte length. */
|
|
2635
3942
|
generateToken(bytes = 32) {
|
|
@@ -2650,7 +3957,7 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2650
3957
|
};
|
|
2651
3958
|
}
|
|
2652
3959
|
// ---------------------------------------------------------------------------
|
|
2653
|
-
//
|
|
3960
|
+
// Mock backend actions
|
|
2654
3961
|
// ---------------------------------------------------------------------------
|
|
2655
3962
|
async authorize(params) {
|
|
2656
3963
|
const provider = params.provider;
|
|
@@ -2759,19 +4066,333 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2759
4066
|
};
|
|
2760
4067
|
registerIntegration("oauth", OAuthIntegration);
|
|
2761
4068
|
|
|
2762
|
-
// src/
|
|
4069
|
+
// src/contracts.ts
|
|
4070
|
+
var serviceCredentials = {
|
|
4071
|
+
stripe: [
|
|
4072
|
+
{ envVar: "STRIPE_SECRET_KEY", required: true, description: "Stripe secret API key" },
|
|
4073
|
+
{ envVar: "STRIPE_WEBHOOK_SECRET", required: false, description: "Webhook endpoint signing secret" }
|
|
4074
|
+
],
|
|
4075
|
+
youtube: [
|
|
4076
|
+
{ envVar: "YOUTUBE_API_KEY", required: true, description: "YouTube Data API key" }
|
|
4077
|
+
],
|
|
4078
|
+
twilio: [
|
|
4079
|
+
{ envVar: "TWILIO_ACCOUNT_SID", required: true, description: "Twilio account SID" },
|
|
4080
|
+
{ envVar: "TWILIO_AUTH_TOKEN", required: true, description: "Twilio auth token" },
|
|
4081
|
+
{ envVar: "TWILIO_PHONE_NUMBER", required: false, description: "Default sender phone number" }
|
|
4082
|
+
],
|
|
4083
|
+
email: [
|
|
4084
|
+
{ envVar: "SENDGRID_API_KEY", required: false, description: "SendGrid API key (use this OR RESEND_API_KEY)" },
|
|
4085
|
+
{ envVar: "RESEND_API_KEY", required: false, description: "Resend API key (use this OR SENDGRID_API_KEY)" },
|
|
4086
|
+
{ envVar: "FROM_EMAIL", required: false, description: "Default sender email address" }
|
|
4087
|
+
],
|
|
4088
|
+
webhook: [
|
|
4089
|
+
{ envVar: "WEBHOOK_SIGNING_SECRET", required: false, description: "Default HMAC-SHA256 signing secret (per-call secret overrides)" },
|
|
4090
|
+
{ envVar: "WEBHOOK_TIMEOUT_MS", required: false, description: "Per-request timeout (ms, default 10000)" }
|
|
4091
|
+
],
|
|
4092
|
+
push: [
|
|
4093
|
+
{ envVar: "VAPID_PUBLIC_KEY", required: true, description: "VAPID public key (web-push generate-vapid-keys)" },
|
|
4094
|
+
{ envVar: "VAPID_PRIVATE_KEY", required: true, description: "VAPID private key" },
|
|
4095
|
+
{ envVar: "VAPID_SUBJECT", required: true, description: "VAPID subject (mailto: or https: contact URI)" }
|
|
4096
|
+
],
|
|
4097
|
+
calendar: [
|
|
4098
|
+
{ envVar: "GOOGLE_CALENDAR_SA_KEY", required: true, description: "Google service-account key JSON (raw or base64) with calendar scope; store in Secret Manager, bind as env" },
|
|
4099
|
+
{ envVar: "GOOGLE_CALENDAR_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" },
|
|
4100
|
+
{ envVar: "GOOGLE_CALENDAR_ID", required: false, description: "Default calendar id (default: primary)" }
|
|
4101
|
+
],
|
|
4102
|
+
drive: [
|
|
4103
|
+
{ envVar: "GOOGLE_DRIVE_SA_KEY", required: true, description: "Google service-account key JSON (raw or base64) with drive scope; store in Secret Manager, bind as env" },
|
|
4104
|
+
{ envVar: "GOOGLE_DRIVE_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" }
|
|
4105
|
+
],
|
|
4106
|
+
metaAds: [
|
|
4107
|
+
{ envVar: "META_ACCESS_TOKEN", required: true, description: "Meta Graph API access token (Marketing API, ads_read)" },
|
|
4108
|
+
{ envVar: "META_AD_ACCOUNT_ID", required: false, description: "Default ad account id (act_\u2026); per-call accountId overrides" }
|
|
4109
|
+
],
|
|
4110
|
+
accounting: [],
|
|
4111
|
+
banking: [
|
|
4112
|
+
{ envVar: "GOCARDLESS_SECRET_ID", required: true, description: "GoCardless Bank Account Data secret id" },
|
|
4113
|
+
{ envVar: "GOCARDLESS_SECRET_KEY", required: true, description: "GoCardless Bank Account Data secret key" }
|
|
4114
|
+
],
|
|
4115
|
+
esign: [
|
|
4116
|
+
{ envVar: "DOCUSIGN_BASE_URL", required: true, description: "DocuSign REST base (e.g. https://demo.docusign.net/restapi/v2.1/accounts/<accountId>)" },
|
|
4117
|
+
{ envVar: "DOCUSIGN_ACCESS_TOKEN", required: true, description: "DocuSign OAuth access token (JWT grant rotation is the deployment concern)" }
|
|
4118
|
+
],
|
|
4119
|
+
llm: [
|
|
4120
|
+
{ envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key (use this OR OPENAI_API_KEY)" },
|
|
4121
|
+
{ envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key (use this OR ANTHROPIC_API_KEY)" }
|
|
4122
|
+
],
|
|
4123
|
+
"llm-integration": [
|
|
4124
|
+
{ envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key" },
|
|
4125
|
+
{ envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key" }
|
|
4126
|
+
],
|
|
4127
|
+
ml: [
|
|
4128
|
+
{ envVar: "MASAR_URL", required: true, description: "Base URL of the deployed model-serving orbital" },
|
|
4129
|
+
{ envVar: "MASAR_ML_TRAIT", required: true, description: "Kebab-case trait name the serving orbital exposes its /events route under" }
|
|
4130
|
+
],
|
|
4131
|
+
deepagent: [
|
|
4132
|
+
{ envVar: "DEEPAGENT_API_URL", required: true, description: "DeepAgent server URL" },
|
|
4133
|
+
{ envVar: "DEEPAGENT_API_KEY", required: false, description: "DeepAgent API key" }
|
|
4134
|
+
],
|
|
4135
|
+
github: [
|
|
4136
|
+
{ envVar: "GITHUB_TOKEN", required: true, description: "GitHub personal access token" },
|
|
4137
|
+
{ envVar: "GITHUB_OWNER", required: false, description: "Default repository owner" },
|
|
4138
|
+
{ envVar: "GITHUB_REPO", required: false, description: "Default repository name" }
|
|
4139
|
+
],
|
|
4140
|
+
docker: [
|
|
4141
|
+
{ envVar: "DOCKER_HOST", required: false, description: "Docker daemon host URL" }
|
|
4142
|
+
],
|
|
4143
|
+
storage: [
|
|
4144
|
+
{ envVar: "STORAGE_ACCESS_KEY_ID", required: true, description: "S3-compatible access key id" },
|
|
4145
|
+
{ envVar: "STORAGE_SECRET_ACCESS_KEY", required: true, description: "S3-compatible secret access key" },
|
|
4146
|
+
{ envVar: "STORAGE_BUCKET", required: true, description: "Default bucket name" },
|
|
4147
|
+
{ envVar: "STORAGE_REGION", required: false, description: "Region (default us-east-1)" },
|
|
4148
|
+
{ envVar: "STORAGE_ENDPOINT", required: false, description: "Custom S3-compatible endpoint (R2/MinIO/\u2026); empty = AWS S3" },
|
|
4149
|
+
{ envVar: "STORAGE_PUBLIC_URL_BASE", required: false, description: "Base URL for public-acl object links; empty = endpoint-derived" }
|
|
4150
|
+
],
|
|
4151
|
+
queue: [
|
|
4152
|
+
{ envVar: "QUEUE_URL", required: true, description: "Message queue connection URL" }
|
|
4153
|
+
],
|
|
4154
|
+
redis: [
|
|
4155
|
+
{ envVar: "REDIS_URL", required: true, description: "Redis connection URL" }
|
|
4156
|
+
],
|
|
4157
|
+
oauth: [
|
|
4158
|
+
{ envVar: "OAUTH_CLIENT_ID", required: true, description: "OIDC client ID" },
|
|
4159
|
+
{ envVar: "OAUTH_CLIENT_SECRET", required: true, description: "OIDC client secret" },
|
|
4160
|
+
{ envVar: "OAUTH_REDIRECT_URI", required: false, description: "OIDC redirect URI (default per-call param)" },
|
|
4161
|
+
{ envVar: "OIDC_ISSUER_URL", required: false, description: "OIDC issuer URL (default https://accounts.google.com)" }
|
|
4162
|
+
],
|
|
4163
|
+
credentials: [
|
|
4164
|
+
{ envVar: "ALMADAR_CREDENTIAL_MASTER_KEY", required: false, description: "AES-256 master key (64-char hex) enabling the hosted credential store \u2014 hold it alone in the platform secret store" }
|
|
4165
|
+
],
|
|
4166
|
+
otel: [
|
|
4167
|
+
{ envVar: "OTEL_EXPORTER_OTLP_ENDPOINT", required: true, description: "OpenTelemetry collector endpoint" },
|
|
4168
|
+
{ envVar: "OTEL_SERVICE_NAME", required: false, description: "Service name for traces" }
|
|
4169
|
+
],
|
|
4170
|
+
cli: [],
|
|
4171
|
+
// No fixed env vars — connection strings are resolved per-query from the
|
|
4172
|
+
// caller-supplied connectionRef, so credentials cannot be declared statically.
|
|
4173
|
+
database: [],
|
|
4174
|
+
wikimedia: [
|
|
4175
|
+
{ envVar: "WIKIMEDIA_USER_AGENT", required: false, description: "Descriptive User-Agent for the Wikipedia API" },
|
|
4176
|
+
{ envVar: "WIKIMEDIA_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
|
|
4177
|
+
],
|
|
4178
|
+
iconify: [
|
|
4179
|
+
{ envVar: "ICONIFY_USER_AGENT", required: false, description: "User-Agent for the Iconify API" },
|
|
4180
|
+
{ envVar: "ICONIFY_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
|
|
4181
|
+
],
|
|
4182
|
+
arxiv: [
|
|
4183
|
+
{ envVar: "ARXIV_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
|
|
4184
|
+
]
|
|
4185
|
+
};
|
|
4186
|
+
var serviceProbes = {
|
|
4187
|
+
calendar: { action: "listEvents", params: { maxResults: 1 } },
|
|
4188
|
+
drive: { action: "listFiles", params: { maxResults: 1 } },
|
|
4189
|
+
metaAds: { action: "listCampaigns", params: {} }
|
|
4190
|
+
};
|
|
4191
|
+
|
|
4192
|
+
// src/integrations/credentials/index.ts
|
|
4193
|
+
var ENV_VAR_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
4194
|
+
function isProbeService(service) {
|
|
4195
|
+
return service in serviceProbes;
|
|
4196
|
+
}
|
|
4197
|
+
var CredentialsIntegration = class extends BaseIntegration {
|
|
4198
|
+
constructor(config) {
|
|
4199
|
+
super(config);
|
|
4200
|
+
this.logger.info("Credentials integration initialized (tenant credential store surface)");
|
|
4201
|
+
}
|
|
4202
|
+
async execute(action, params) {
|
|
4203
|
+
const validation = this.validateParams(action, params);
|
|
4204
|
+
if (!validation.valid) {
|
|
4205
|
+
return {
|
|
4206
|
+
success: false,
|
|
4207
|
+
error: {
|
|
4208
|
+
name: "IntegrationError",
|
|
4209
|
+
message: "Validation failed",
|
|
4210
|
+
code: "VALIDATION_ERROR",
|
|
4211
|
+
details: validation.errors
|
|
4212
|
+
},
|
|
4213
|
+
metadata: this.createMetadata(action, 0)
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
const startTime = Date.now();
|
|
4217
|
+
try {
|
|
4218
|
+
let data;
|
|
4219
|
+
switch (action) {
|
|
4220
|
+
case "list":
|
|
4221
|
+
data = this.list(typeof params.service === "string" ? params.service : void 0);
|
|
4222
|
+
break;
|
|
4223
|
+
case "set":
|
|
4224
|
+
data = await this.set(params.service, params.envVar, params.value);
|
|
4225
|
+
break;
|
|
4226
|
+
case "remove":
|
|
4227
|
+
data = await this.remove(params.service, params.envVar);
|
|
4228
|
+
break;
|
|
4229
|
+
case "test":
|
|
4230
|
+
data = await this.test(params.service);
|
|
4231
|
+
break;
|
|
4232
|
+
default:
|
|
4233
|
+
throw new Error(`Unknown action: ${action}`);
|
|
4234
|
+
}
|
|
4235
|
+
return {
|
|
4236
|
+
success: true,
|
|
4237
|
+
data,
|
|
4238
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
4239
|
+
};
|
|
4240
|
+
} catch (error) {
|
|
4241
|
+
return this.handleError(action, error);
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
declaredFor(service) {
|
|
4245
|
+
return serviceCredentials[service] ?? [];
|
|
4246
|
+
}
|
|
4247
|
+
assertSettable(service, envVar) {
|
|
4248
|
+
if (service === "database") {
|
|
4249
|
+
if (!ENV_VAR_NAME.test(envVar)) {
|
|
4250
|
+
throw new Error(`"${envVar}" is not a well-formed connection reference (expected an env-var name)`);
|
|
4251
|
+
}
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
const declared = this.declaredFor(service);
|
|
4255
|
+
if (declared.length === 0) {
|
|
4256
|
+
throw new Error(`Service "${service}" declares no credentials`);
|
|
4257
|
+
}
|
|
4258
|
+
if (!declared.some((c) => c.envVar === envVar)) {
|
|
4259
|
+
const valid = declared.map((c) => c.envVar).join(", ");
|
|
4260
|
+
throw new Error(`"${envVar}" is not a declared credential of "${service}" (declared: ${valid})`);
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
list(serviceFilter) {
|
|
4264
|
+
const store = getInstalledCredentialStore();
|
|
4265
|
+
const stored = new Map((store?.entries() ?? []).map((e) => [`${e.service}\0${e.envVar}`, e]));
|
|
4266
|
+
const entries = [];
|
|
4267
|
+
for (const [service, declared] of Object.entries(serviceCredentials)) {
|
|
4268
|
+
if (serviceFilter && service !== serviceFilter) continue;
|
|
4269
|
+
for (const { envVar, required, description } of declared) {
|
|
4270
|
+
const fromStore = stored.get(`${service}\0${envVar}`);
|
|
4271
|
+
stored.delete(`${service}\0${envVar}`);
|
|
4272
|
+
const fromEnv = process.env[envVar];
|
|
4273
|
+
const source = fromStore ? "store" : fromEnv ? "env" : "none";
|
|
4274
|
+
entries.push({
|
|
4275
|
+
service,
|
|
4276
|
+
envVar,
|
|
4277
|
+
required,
|
|
4278
|
+
description,
|
|
4279
|
+
configured: source !== "none",
|
|
4280
|
+
source,
|
|
4281
|
+
last4: fromStore ? fromStore.last4 : fromEnv ? fromEnv.slice(-4) : ""
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
for (const e of stored.values()) {
|
|
4286
|
+
if (serviceFilter && e.service !== serviceFilter) continue;
|
|
4287
|
+
entries.push({
|
|
4288
|
+
service: e.service,
|
|
4289
|
+
envVar: e.envVar,
|
|
4290
|
+
required: false,
|
|
4291
|
+
description: "Stored connection reference",
|
|
4292
|
+
configured: true,
|
|
4293
|
+
source: "store",
|
|
4294
|
+
last4: e.last4
|
|
4295
|
+
});
|
|
4296
|
+
}
|
|
4297
|
+
return { enabled: store?.enabled ?? false, entries };
|
|
4298
|
+
}
|
|
4299
|
+
async set(service, envVar, value) {
|
|
4300
|
+
const store = getInstalledCredentialStore();
|
|
4301
|
+
if (!store) {
|
|
4302
|
+
throw new Error("No credential store is installed on this host \u2014 set credentials via the environment");
|
|
4303
|
+
}
|
|
4304
|
+
this.assertSettable(service, envVar);
|
|
4305
|
+
const entry = await store.set(service, envVar, value);
|
|
4306
|
+
return { saved: true, service, envVar, last4: entry.last4 };
|
|
4307
|
+
}
|
|
4308
|
+
async remove(service, envVar) {
|
|
4309
|
+
const store = getInstalledCredentialStore();
|
|
4310
|
+
if (!store) {
|
|
4311
|
+
throw new Error("No credential store is installed on this host");
|
|
4312
|
+
}
|
|
4313
|
+
this.assertSettable(service, envVar);
|
|
4314
|
+
return { removed: await store.remove(envVar) };
|
|
4315
|
+
}
|
|
4316
|
+
async test(service) {
|
|
4317
|
+
const declared = this.declaredFor(service);
|
|
4318
|
+
const missing = declared.filter((c) => c.required && !resolveCredentialRef(c.envVar)).map((c) => c.envVar);
|
|
4319
|
+
const configured = missing.length === 0;
|
|
4320
|
+
if (!configured) {
|
|
4321
|
+
return { service, configured, missing, probed: false, ok: false, message: `Missing required credentials: ${missing.join(", ")}` };
|
|
4322
|
+
}
|
|
4323
|
+
const factory = getActiveFactory();
|
|
4324
|
+
const probe = isProbeService(service) ? serviceProbes[service] : void 0;
|
|
4325
|
+
if (!factory || !probe) {
|
|
4326
|
+
return { service, configured, missing, probed: false, ok: true, message: "Credentials present (no live probe declared for this service)" };
|
|
4327
|
+
}
|
|
4328
|
+
if (!factory.isConfigured(service)) {
|
|
4329
|
+
return { service, configured, missing, probed: false, ok: false, message: "Credentials present but the service is not configured on this host \u2014 restart or re-save a credential" };
|
|
4330
|
+
}
|
|
4331
|
+
const result = await factory.execute(service, probe.action, probe.params);
|
|
4332
|
+
if (!result.success) {
|
|
4333
|
+
return { service, configured, missing, probed: true, ok: false, message: result.error?.message ?? `Probe ${probe.action} failed` };
|
|
4334
|
+
}
|
|
4335
|
+
const echoed = result.data !== null && typeof result.data === "object" && "_mock" in result.data;
|
|
4336
|
+
if (echoed) {
|
|
4337
|
+
return { service, configured, missing, probed: false, ok: false, message: "Probe was mock-echoed \u2014 the service is not actually configured" };
|
|
4338
|
+
}
|
|
4339
|
+
return { service, configured, missing, probed: true, ok: true, message: `Probe ${probe.action} succeeded` };
|
|
4340
|
+
}
|
|
4341
|
+
};
|
|
4342
|
+
registerIntegration("credentials", CredentialsIntegration);
|
|
4343
|
+
function isParamRecord2(value) {
|
|
4344
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
4345
|
+
}
|
|
4346
|
+
function toFilePayload(value) {
|
|
4347
|
+
if (!isParamRecord2(value)) return null;
|
|
4348
|
+
const { name, size, type, content } = value;
|
|
4349
|
+
if (typeof name !== "string") return null;
|
|
4350
|
+
return {
|
|
4351
|
+
name,
|
|
4352
|
+
size: typeof size === "number" ? size : 0,
|
|
4353
|
+
type: typeof type === "string" ? type : "application/octet-stream",
|
|
4354
|
+
content: typeof content === "string" ? content : void 0
|
|
4355
|
+
};
|
|
4356
|
+
}
|
|
4357
|
+
function decodeContent2(content) {
|
|
4358
|
+
const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
|
|
4359
|
+
if (dataUrlMatch) {
|
|
4360
|
+
const [, mime, isB64, body] = dataUrlMatch;
|
|
4361
|
+
const bytes = isB64 ? Buffer.from(body, "base64") : Buffer.from(decodeURIComponent(body), "utf8");
|
|
4362
|
+
return { bytes, contentType: mime || null };
|
|
4363
|
+
}
|
|
4364
|
+
return { bytes: Buffer.from(content, "utf8"), contentType: null };
|
|
4365
|
+
}
|
|
2763
4366
|
var StorageIntegration = class extends BaseIntegration {
|
|
2764
4367
|
constructor(config) {
|
|
2765
4368
|
super(config);
|
|
2766
4369
|
this.objects = /* @__PURE__ */ new Map();
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
4370
|
+
this.s3 = null;
|
|
4371
|
+
this.defaultBucket = config.env.STORAGE_BUCKET || "";
|
|
4372
|
+
this.publicUrlBase = config.env.STORAGE_PUBLIC_URL_BASE || "";
|
|
4373
|
+
const accessKeyId = config.env.STORAGE_ACCESS_KEY_ID || "";
|
|
4374
|
+
const secretAccessKey = config.env.STORAGE_SECRET_ACCESS_KEY || "";
|
|
4375
|
+
if (accessKeyId && secretAccessKey) {
|
|
4376
|
+
const endpoint = config.env.STORAGE_ENDPOINT || void 0;
|
|
4377
|
+
this.s3 = new S3Client({
|
|
4378
|
+
region: config.env.STORAGE_REGION || "us-east-1",
|
|
4379
|
+
endpoint,
|
|
4380
|
+
// Path-style is what MinIO/R2-style endpoints expect.
|
|
4381
|
+
forcePathStyle: Boolean(endpoint),
|
|
4382
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
4383
|
+
});
|
|
4384
|
+
this.logger.info("Storage integration initialized (S3 backend)", {
|
|
4385
|
+
endpoint: endpoint ?? "aws",
|
|
4386
|
+
bucket: this.defaultBucket
|
|
4387
|
+
});
|
|
4388
|
+
} else {
|
|
4389
|
+
if (process.env.NODE_ENV === "production") {
|
|
4390
|
+
throw new Error(
|
|
4391
|
+
"Storage credentials missing in production (STORAGE_ACCESS_KEY_ID / STORAGE_SECRET_ACCESS_KEY) \u2014 refusing the in-memory fallback. See SECRETS.md."
|
|
4392
|
+
);
|
|
4393
|
+
}
|
|
4394
|
+
this.logger.warn("Storage integration initialized (in-memory backend \u2014 dev only, nothing persists)");
|
|
2773
4395
|
}
|
|
2774
|
-
this.logger.info("Storage integration initialized (in-memory backend)");
|
|
2775
4396
|
}
|
|
2776
4397
|
async execute(action, params) {
|
|
2777
4398
|
const validation = this.validateParams(action, params);
|
|
@@ -2821,107 +4442,182 @@ var StorageIntegration = class extends BaseIntegration {
|
|
|
2821
4442
|
// ---------------------------------------------------------------------------
|
|
2822
4443
|
// Helpers
|
|
2823
4444
|
// ---------------------------------------------------------------------------
|
|
2824
|
-
|
|
4445
|
+
bucketOf(params) {
|
|
4446
|
+
return params.bucket || this.defaultBucket;
|
|
4447
|
+
}
|
|
2825
4448
|
compositeKey(bucket, key) {
|
|
2826
4449
|
return `${bucket}/${key}`;
|
|
2827
4450
|
}
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
4451
|
+
generateEtag(bytes) {
|
|
4452
|
+
return `"${createHash("md5").update(bytes).digest("hex")}"`;
|
|
4453
|
+
}
|
|
4454
|
+
/** Resolve the upload inputs from either admitted shape. */
|
|
4455
|
+
resolveUpload(params) {
|
|
4456
|
+
const file = toFilePayload(params.file);
|
|
4457
|
+
if (file) {
|
|
4458
|
+
const maxSize = typeof params.maxSize === "number" ? params.maxSize : 0;
|
|
4459
|
+
if (maxSize > 0 && file.size > maxSize) {
|
|
4460
|
+
throw new Error(`Upload rejected: ${file.name} is ${file.size} bytes (max ${maxSize})`);
|
|
4461
|
+
}
|
|
4462
|
+
if (!file.content) {
|
|
4463
|
+
throw new Error(
|
|
4464
|
+
`Upload rejected: file payload for '${file.name}' carries no content \u2014 the uploader must include the base64 data URL`
|
|
4465
|
+
);
|
|
4466
|
+
}
|
|
4467
|
+
const { bytes: bytes2, contentType: contentType2 } = decodeContent2(file.content);
|
|
4468
|
+
const safeName = file.name.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
4469
|
+
return {
|
|
4470
|
+
key: `${Date.now()}-${safeName}`,
|
|
4471
|
+
bytes: bytes2,
|
|
4472
|
+
contentType: contentType2 ?? file.type,
|
|
4473
|
+
acl: params.acl === "public" ? "public-read" : void 0
|
|
4474
|
+
};
|
|
4475
|
+
}
|
|
4476
|
+
const key = params.key;
|
|
4477
|
+
const content = params.content;
|
|
4478
|
+
if (!key || content === void 0 || content === null) {
|
|
4479
|
+
throw new Error("upload requires either `file` (with content) or the `key` + `content` pair");
|
|
2835
4480
|
}
|
|
2836
|
-
|
|
4481
|
+
const raw = typeof content === "string" ? content : JSON.stringify(content);
|
|
4482
|
+
const { bytes, contentType } = decodeContent2(raw);
|
|
4483
|
+
return {
|
|
4484
|
+
key,
|
|
4485
|
+
bytes,
|
|
4486
|
+
contentType: params.contentType || contentType || "application/octet-stream",
|
|
4487
|
+
acl: params.acl === "public" ? "public-read" : void 0
|
|
4488
|
+
};
|
|
2837
4489
|
}
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
4490
|
+
publicUrl(bucket, key) {
|
|
4491
|
+
if (this.publicUrlBase) {
|
|
4492
|
+
return `${this.publicUrlBase.replace(/\/$/, "")}/${key}`;
|
|
4493
|
+
}
|
|
4494
|
+
const endpoint = this.config.env.STORAGE_ENDPOINT;
|
|
4495
|
+
if (endpoint) {
|
|
4496
|
+
return `${endpoint.replace(/\/$/, "")}/${bucket}/${key}`;
|
|
2842
4497
|
}
|
|
2843
|
-
|
|
4498
|
+
const region = this.config.env.STORAGE_REGION || "us-east-1";
|
|
4499
|
+
return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
2844
4500
|
}
|
|
2845
4501
|
// ---------------------------------------------------------------------------
|
|
2846
4502
|
// Actions
|
|
2847
4503
|
// ---------------------------------------------------------------------------
|
|
2848
4504
|
async upload(params) {
|
|
2849
|
-
const bucket = params
|
|
2850
|
-
const key = params
|
|
2851
|
-
const
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
4505
|
+
const bucket = this.bucketOf(params);
|
|
4506
|
+
const { key, bytes, contentType, acl } = this.resolveUpload(params);
|
|
4507
|
+
const etag = this.generateEtag(bytes);
|
|
4508
|
+
this.logger.debug("Storage UPLOAD", { bucket, key, contentType, size: bytes.length });
|
|
4509
|
+
if (this.s3) {
|
|
4510
|
+
await this.s3.send(
|
|
4511
|
+
new PutObjectCommand({
|
|
4512
|
+
Bucket: bucket,
|
|
4513
|
+
Key: key,
|
|
4514
|
+
Body: bytes,
|
|
4515
|
+
ContentType: contentType,
|
|
4516
|
+
ACL: acl
|
|
4517
|
+
})
|
|
4518
|
+
);
|
|
4519
|
+
} else {
|
|
4520
|
+
this.objects.set(this.compositeKey(bucket, key), {
|
|
4521
|
+
content: bytes.toString("base64"),
|
|
4522
|
+
contentType,
|
|
4523
|
+
size: bytes.length,
|
|
4524
|
+
metadata: params.metadata ?? {},
|
|
4525
|
+
lastModified: Date.now(),
|
|
4526
|
+
etag
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
const url = acl === "public-read" ? this.publicUrl(bucket, key) : (await this.signUrl(bucket, key, "get", 3600)).url;
|
|
4530
|
+
return { key, bucket, size: bytes.length, etag, id: key, url };
|
|
2867
4531
|
}
|
|
2868
4532
|
async download(params) {
|
|
2869
|
-
const bucket = params
|
|
4533
|
+
const bucket = this.bucketOf(params);
|
|
2870
4534
|
const key = params.key;
|
|
2871
4535
|
this.logger.debug("Storage DOWNLOAD", { bucket, key });
|
|
4536
|
+
if (this.s3) {
|
|
4537
|
+
const response = await this.s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
4538
|
+
const bytes = Buffer.from(await response.Body.transformToByteArray());
|
|
4539
|
+
return {
|
|
4540
|
+
content: bytes.toString("base64"),
|
|
4541
|
+
contentType: response.ContentType ?? "application/octet-stream",
|
|
4542
|
+
size: bytes.length,
|
|
4543
|
+
metadata: {}
|
|
4544
|
+
};
|
|
4545
|
+
}
|
|
2872
4546
|
const obj = this.objects.get(this.compositeKey(bucket, key));
|
|
2873
4547
|
if (!obj) {
|
|
2874
4548
|
throw new Error(`Object not found: ${bucket}/${key}`);
|
|
2875
4549
|
}
|
|
2876
4550
|
return {
|
|
2877
|
-
content: obj.content,
|
|
4551
|
+
content: String(obj.content),
|
|
2878
4552
|
contentType: obj.contentType,
|
|
2879
4553
|
size: obj.size,
|
|
2880
4554
|
metadata: obj.metadata
|
|
2881
4555
|
};
|
|
2882
4556
|
}
|
|
2883
4557
|
async list(params) {
|
|
2884
|
-
const bucket = params
|
|
4558
|
+
const bucket = this.bucketOf(params);
|
|
2885
4559
|
const prefix = params.prefix ?? "";
|
|
2886
4560
|
const maxKeys = params.maxKeys ?? 1e3;
|
|
2887
4561
|
this.logger.debug("Storage LIST", { bucket, prefix, maxKeys });
|
|
4562
|
+
if (this.s3) {
|
|
4563
|
+
const response = await this.s3.send(
|
|
4564
|
+
new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix || void 0, MaxKeys: maxKeys })
|
|
4565
|
+
);
|
|
4566
|
+
return {
|
|
4567
|
+
keys: (response.Contents ?? []).map((entry) => ({
|
|
4568
|
+
key: entry.Key ?? "",
|
|
4569
|
+
size: entry.Size ?? 0,
|
|
4570
|
+
lastModified: entry.LastModified?.getTime() ?? 0
|
|
4571
|
+
})),
|
|
4572
|
+
// Continuation tokens are unrepresentable in the result type (ledger
|
|
4573
|
+
// I-11) — surface the clamp honestly.
|
|
4574
|
+
truncated: Boolean(response.IsTruncated)
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
2888
4577
|
const bucketPrefix = `${bucket}/`;
|
|
2889
4578
|
const fullPrefix = `${bucket}/${prefix}`;
|
|
2890
4579
|
const results = [];
|
|
2891
4580
|
for (const [compositeKey, obj] of this.objects) {
|
|
2892
4581
|
if (!compositeKey.startsWith(fullPrefix)) continue;
|
|
2893
|
-
const objectKey = compositeKey.slice(bucketPrefix.length);
|
|
2894
4582
|
results.push({
|
|
2895
|
-
key:
|
|
4583
|
+
key: compositeKey.slice(bucketPrefix.length),
|
|
2896
4584
|
size: obj.size,
|
|
2897
4585
|
lastModified: obj.lastModified
|
|
2898
4586
|
});
|
|
2899
4587
|
}
|
|
2900
4588
|
results.sort((a, b) => a.key.localeCompare(b.key));
|
|
2901
|
-
|
|
2902
|
-
return {
|
|
2903
|
-
keys: results.slice(0, maxKeys),
|
|
2904
|
-
truncated
|
|
2905
|
-
};
|
|
4589
|
+
return { keys: results.slice(0, maxKeys), truncated: results.length > maxKeys };
|
|
2906
4590
|
}
|
|
2907
4591
|
async deleteObject(params) {
|
|
2908
|
-
const bucket = params
|
|
4592
|
+
const bucket = this.bucketOf(params);
|
|
2909
4593
|
const key = params.key;
|
|
2910
4594
|
this.logger.debug("Storage DELETE", { bucket, key });
|
|
4595
|
+
if (this.s3) {
|
|
4596
|
+
await this.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
4597
|
+
return { deleted: true };
|
|
4598
|
+
}
|
|
2911
4599
|
const existed = this.objects.has(this.compositeKey(bucket, key));
|
|
2912
4600
|
this.objects.delete(this.compositeKey(bucket, key));
|
|
2913
4601
|
return { deleted: existed };
|
|
2914
4602
|
}
|
|
4603
|
+
async signUrl(bucket, key, operation, expiresIn) {
|
|
4604
|
+
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
4605
|
+
if (this.s3) {
|
|
4606
|
+
const command = operation === "put" ? new PutObjectCommand({ Bucket: bucket, Key: key }) : new GetObjectCommand({ Bucket: bucket, Key: key });
|
|
4607
|
+
const url2 = await getSignedUrl(this.s3, command, { expiresIn });
|
|
4608
|
+
return { url: url2, expiresAt };
|
|
4609
|
+
}
|
|
4610
|
+
const token = createHash("sha256").update(`${bucket}/${key}/${expiresAt}`).digest("hex").slice(0, 16);
|
|
4611
|
+
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}`;
|
|
4612
|
+
return { url, expiresAt };
|
|
4613
|
+
}
|
|
2915
4614
|
async getSignedUrl(params) {
|
|
2916
|
-
const bucket = params
|
|
4615
|
+
const bucket = this.bucketOf(params);
|
|
2917
4616
|
const key = params.key;
|
|
2918
4617
|
const expiresIn = params.expiresIn ?? 3600;
|
|
2919
4618
|
const operation = params.operation ?? "get";
|
|
2920
4619
|
this.logger.debug("Storage GET_SIGNED_URL", { bucket, key, expiresIn, operation });
|
|
2921
|
-
|
|
2922
|
-
const token = Math.random().toString(36).slice(2, 18);
|
|
2923
|
-
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}`;
|
|
2924
|
-
return { url, expiresAt };
|
|
4620
|
+
return this.signUrl(bucket, key, operation, expiresIn);
|
|
2925
4621
|
}
|
|
2926
4622
|
};
|
|
2927
4623
|
registerIntegration("storage", StorageIntegration);
|
|
@@ -3428,10 +5124,10 @@ var DatabaseIntegration = class extends BaseIntegration {
|
|
|
3428
5124
|
}
|
|
3429
5125
|
/** Resolve (and cache) the driver for a connection reference. */
|
|
3430
5126
|
driverFor(connectionRef) {
|
|
3431
|
-
const connectionString =
|
|
5127
|
+
const connectionString = resolveCredentialRef(connectionRef);
|
|
3432
5128
|
if (!connectionString) {
|
|
3433
5129
|
throw new IntegrationError(
|
|
3434
|
-
`Connection reference "${connectionRef}" is not set in the environment`,
|
|
5130
|
+
`Connection reference "${connectionRef}" is not set in the credential store or environment`,
|
|
3435
5131
|
"AUTH_ERROR"
|
|
3436
5132
|
);
|
|
3437
5133
|
}
|
|
@@ -3683,6 +5379,6 @@ var ArxivIntegration = class extends BaseIntegration {
|
|
|
3683
5379
|
};
|
|
3684
5380
|
registerIntegration("arxiv", ArxivIntegration);
|
|
3685
5381
|
|
|
3686
|
-
export { ArxivIntegration, BaseIntegration, CLIIntegration, ConsoleLogger, DatabaseIntegration, DeepAgentIntegration, DockerIntegration, EmailIntegration, GitHubIntegration, IconifyIntegration, IntegrationError, IntegrationFactory, LLMIntegration, MLIntegration, OAuthIntegration, OtelIntegration, QueueIntegration, RedisIntegration, StorageIntegration, StripeIntegration, TwilioIntegration, WikimediaIntegration, YouTubeIntegration, assertReadOnlySelect, getIntegration, getIntegrationFactory, getRegisteredIntegrations, isKnownIntegration, registerIntegration, resetIntegrationFactory, validateParams, verifyAndParseStripeEvent, withRetry };
|
|
5382
|
+
export { AccountingIntegration, ArxivIntegration, BankingIntegration, BaseIntegration, CLIIntegration, CREDENTIAL_ENTITY_TYPE, CREDENTIAL_MASTER_KEY_ENV, CalendarIntegration, ConsoleLogger, CredentialStore, CredentialsIntegration, DatabaseIntegration, DeepAgentIntegration, DockerIntegration, DriveIntegration, EmailIntegration, EsignIntegration, GitHubIntegration, IconifyIntegration, IntegrationError, IntegrationFactory, LLMIntegration, MLIntegration, MetaAdsIntegration, OAuthIntegration, OtelIntegration, PushIntegration, QueueIntegration, RedisIntegration, StorageIntegration, StripeIntegration, TwilioIntegration, WebhookIntegration, WikimediaIntegration, YouTubeIntegration, assertReadOnlySelect, getActiveFactory, getInstalledCredentialStore, getIntegration, getIntegrationFactory, getRegisteredIntegrations, googleCalendarHookProvider, installActiveFactory, installCredentialStore, isKnownIntegration, parseCalendarPushNotification, registerIntegration, resetIntegrationFactory, resolveCredentialRef, uninstallCredentialStore, validateParams, verifyAndParseStripeEvent, withRetry };
|
|
3687
5383
|
//# sourceMappingURL=index.js.map
|
|
3688
5384
|
//# sourceMappingURL=index.js.map
|