@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/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
- if (this.instances.has(name)) {
240
- return this.instances.get(name);
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(name, instance);
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
- sid: message.sid,
887
- status: message.status
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("twilio", TwilioIntegration);
892
- var EmailIntegration = class extends BaseIntegration {
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
- this.provider = config.env.PROVIDER || "sendgrid";
896
- this.fromEmail = config.env.FROM_EMAIL || "noreply@example.com";
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(`Email integration initialized (${this.provider})`);
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 "send":
932
- data = await this.executeWithRetry(() => this.send(params));
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, retries)
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 send(params) {
947
- const { to, subject, body, from } = params;
948
- this.logger.debug("Sending email", { to: String(to), subject: String(subject), provider: this.provider });
949
- if (this.provider === "sendgrid") {
950
- return await this.sendViaSendGrid(
951
- to,
952
- subject,
953
- body,
954
- from || this.fromEmail
955
- );
956
- } else if (this.provider === "resend") {
957
- return await this.sendViaResend(
958
- to,
959
- subject,
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
- throw new Error(`Unknown email provider: ${this.provider}`);
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 sendViaSendGrid(to, subject, body, from) {
967
- const msg = {
968
- to,
969
- from,
970
- subject,
971
- html: body
972
- };
973
- const response = await sgMail.send(msg);
974
- return {
975
- id: response[0].headers["x-message-id"],
976
- status: "sent"
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 sendViaResend(to, subject, body, from) {
980
- if (!this.resendClient) {
981
- throw new Error("Resend client not initialized");
982
- }
983
- const response = await this.resendClient.emails.send({
984
- from,
985
- to,
986
- subject,
987
- html: body
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("email", EmailIntegration);
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
- this.logger.info("OAuth integration initialized (mock backend)");
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
- // Helpers
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
- // Actions
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/integrations/storage/index.ts
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
- const storageUrl = config.env.STORAGE_URL;
2768
- if (storageUrl) {
2769
- this.logger.warn(
2770
- "STORAGE_URL is configured but real storage client is not yet implemented. Falling back to in-memory store.",
2771
- { storageUrl }
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
- /** Build a composite key from bucket and object key. */
4445
+ bucketOf(params) {
4446
+ return params.bucket || this.defaultBucket;
4447
+ }
2825
4448
  compositeKey(bucket, key) {
2826
4449
  return `${bucket}/${key}`;
2827
4450
  }
2828
- /** Generate a deterministic etag from content. */
2829
- generateEtag(content) {
2830
- const raw = typeof content === "string" ? content : JSON.stringify(content);
2831
- let hash = 0;
2832
- for (let i = 0; i < raw.length; i++) {
2833
- const ch = raw.charCodeAt(i);
2834
- hash = (hash << 5) - hash + ch | 0;
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
- return `"${Math.abs(hash).toString(16).padStart(8, "0")}"`;
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
- /** Compute the byte size of content. */
2839
- computeSize(content) {
2840
- if (typeof content === "string") {
2841
- return new TextEncoder().encode(content).byteLength;
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
- return JSON.stringify(content).length;
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.bucket;
2850
- const key = params.key;
2851
- const content = params.content;
2852
- const contentType = params.contentType ?? "application/octet-stream";
2853
- const metadata = params.metadata ?? {};
2854
- this.logger.debug("Storage UPLOAD", { bucket, key, contentType });
2855
- const size = this.computeSize(content);
2856
- const etag = this.generateEtag(content);
2857
- const obj = {
2858
- content,
2859
- contentType,
2860
- size,
2861
- metadata,
2862
- lastModified: Date.now(),
2863
- etag
2864
- };
2865
- this.objects.set(this.compositeKey(bucket, key), obj);
2866
- return { key, bucket, size, etag };
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.bucket;
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.bucket;
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: objectKey,
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
- const truncated = results.length > maxKeys;
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.bucket;
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.bucket;
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
- const expiresAt = Date.now() + expiresIn * 1e3;
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 = process.env[connectionRef];
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