@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.
@@ -1,16 +1,22 @@
1
1
  import { createLogger } from '@almadar/logger';
2
2
  import { integratorsRegistry } from '@almadar/core/patterns';
3
+ import { createHmac, createHash } from 'crypto';
3
4
  import Stripe2 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
  import { createRequire } from 'module';
16
22
 
@@ -216,56 +222,80 @@ function getIntegration(name) {
216
222
  }
217
223
 
218
224
  // src/factory.ts
225
+ function instanceKey(name, principal) {
226
+ return principal ? `${name}\0${principal}` : name;
227
+ }
219
228
  var IntegrationFactory = class {
220
229
  constructor() {
221
230
  this.instances = /* @__PURE__ */ new Map();
222
231
  this.configs = /* @__PURE__ */ new Map();
223
232
  }
224
233
  /**
225
- * Configure an integration (doesn't instantiate yet)
234
+ * Configure an integration (doesn't instantiate yet). A `principal` scopes
235
+ * the config to that principal; the app-wide config (no principal) is the
236
+ * fallback for every principal.
226
237
  */
227
- configure(name, config) {
228
- this.configs.set(name, { name, ...config });
238
+ configure(name, config, principal) {
239
+ this.configs.set(instanceKey(name, principal), { name, ...config });
229
240
  }
230
241
  /**
231
- * Get or create an integration instance
242
+ * Get or create an integration instance. Principal-scoped lookups fall
243
+ * back to the app-wide config when no per-principal config exists.
232
244
  */
233
- get(name) {
234
- if (this.instances.has(name)) {
235
- return this.instances.get(name);
245
+ get(name, principal) {
246
+ const key = instanceKey(name, principal);
247
+ const cached = this.instances.get(key);
248
+ if (cached) {
249
+ return cached;
236
250
  }
237
251
  const Constructor = getIntegration(name);
238
252
  if (!Constructor) {
239
253
  throw new Error(`Unknown integration: ${name}. Make sure it's imported.`);
240
254
  }
241
- const config = this.configs.get(name);
255
+ const config = this.configs.get(key) ?? this.configs.get(name);
242
256
  if (!config) {
243
257
  throw new Error(
244
258
  `Integration not configured: ${name}. Call configure() first.`
245
259
  );
246
260
  }
247
261
  const instance = new Constructor(config);
248
- this.instances.set(name, instance);
262
+ this.instances.set(key, instance);
249
263
  return instance;
250
264
  }
251
265
  /**
252
266
  * Execute an action on an integration
253
267
  */
254
- async execute(integration, action, params) {
255
- const instance = this.get(integration);
268
+ async execute(integration, action, params, context) {
269
+ const instance = this.get(integration, context?.principal);
256
270
  return await instance.execute(action, params);
257
271
  }
258
272
  /**
259
273
  * Check if integration is configured
260
274
  */
261
- isConfigured(name) {
262
- return this.configs.has(name);
275
+ isConfigured(name, principal) {
276
+ return this.configs.has(instanceKey(name, principal)) || this.configs.has(name);
263
277
  }
264
278
  /**
265
279
  * Register an integration instance directly (used by mock infrastructure)
266
280
  */
267
- registerInstance(name, instance) {
268
- this.instances.set(name, instance);
281
+ registerInstance(name, instance, principal) {
282
+ this.instances.set(instanceKey(name, principal), instance);
283
+ }
284
+ /**
285
+ * Drop the cached instance(s) for a name so the next `get` rebuilds from
286
+ * the current config — how a credential change goes live without restart.
287
+ * Configs are kept; without a name, every instance is dropped.
288
+ */
289
+ invalidate(name) {
290
+ if (name === void 0) {
291
+ this.instances.clear();
292
+ return;
293
+ }
294
+ for (const key of this.instances.keys()) {
295
+ if (key === name || key.startsWith(`${name}\0`)) {
296
+ this.instances.delete(key);
297
+ }
298
+ }
269
299
  }
270
300
  /**
271
301
  * Clear all instances (useful for testing)
@@ -282,6 +312,25 @@ var IntegrationFactory = class {
282
312
  }
283
313
  };
284
314
 
315
+ // src/credentials/resolver.ts
316
+ var installedStore = null;
317
+ var activeFactory = null;
318
+ function installActiveFactory(factory) {
319
+ activeFactory = factory;
320
+ }
321
+ function getActiveFactory() {
322
+ return activeFactory;
323
+ }
324
+ function installCredentialStore(store) {
325
+ installedStore = store;
326
+ }
327
+ function getInstalledCredentialStore() {
328
+ return installedStore;
329
+ }
330
+ function resolveCredentialRef(ref, env = process.env) {
331
+ return installedStore?.resolve(ref) ?? env[ref];
332
+ }
333
+
285
334
  // src/integrations/stripe/index.ts
286
335
  var STRIPE_API_VERSION = "2025-02-24.acacia";
287
336
  function isoFromUnix(seconds) {
@@ -800,62 +849,1025 @@ var EmailIntegration = class extends BaseIntegration {
800
849
  return {
801
850
  success: true,
802
851
  data,
803
- metadata: this.createMetadata(action, Date.now() - startTime, retries)
852
+ metadata: this.createMetadata(action, Date.now() - startTime, retries)
853
+ };
854
+ } catch (error) {
855
+ return this.handleError(action, error);
856
+ }
857
+ }
858
+ async send(params) {
859
+ const { to, subject, body, from } = params;
860
+ this.logger.debug("Sending email", { to: String(to), subject: String(subject), provider: this.provider });
861
+ if (this.provider === "sendgrid") {
862
+ return await this.sendViaSendGrid(
863
+ to,
864
+ subject,
865
+ body,
866
+ from || this.fromEmail
867
+ );
868
+ } else if (this.provider === "resend") {
869
+ return await this.sendViaResend(
870
+ to,
871
+ subject,
872
+ body,
873
+ from || this.fromEmail
874
+ );
875
+ }
876
+ throw new Error(`Unknown email provider: ${this.provider}`);
877
+ }
878
+ async sendViaSendGrid(to, subject, body, from) {
879
+ const msg = {
880
+ to,
881
+ from,
882
+ subject,
883
+ html: body
884
+ };
885
+ const response = await sgMail.send(msg);
886
+ return {
887
+ id: response[0].headers["x-message-id"],
888
+ status: "sent"
889
+ };
890
+ }
891
+ async sendViaResend(to, subject, body, from) {
892
+ if (!this.resendClient) {
893
+ throw new Error("Resend client not initialized");
894
+ }
895
+ const response = await this.resendClient.emails.send({
896
+ from,
897
+ to,
898
+ subject,
899
+ html: body
900
+ });
901
+ return {
902
+ id: response.data?.id,
903
+ status: "sent"
904
+ };
905
+ }
906
+ };
907
+ registerIntegration("email", EmailIntegration);
908
+ var WebhookIntegration = class extends BaseIntegration {
909
+ constructor(config) {
910
+ super(config);
911
+ this.signingSecret = config.env.WEBHOOK_SIGNING_SECRET || "";
912
+ this.timeoutMs = Number(config.env.WEBHOOK_TIMEOUT_MS) || 1e4;
913
+ this.logger.info("Webhook integration initialized");
914
+ }
915
+ async execute(action, params) {
916
+ const validation = this.validateParams(action, params);
917
+ if (!validation.valid) {
918
+ return {
919
+ success: false,
920
+ error: {
921
+ name: "IntegrationError",
922
+ message: "Validation failed",
923
+ code: "VALIDATION_ERROR",
924
+ details: validation.errors
925
+ },
926
+ metadata: this.createMetadata(action, 0)
927
+ };
928
+ }
929
+ const startTime = Date.now();
930
+ let retries = 0;
931
+ try {
932
+ let data;
933
+ switch (action) {
934
+ case "send":
935
+ data = await this.executeWithRetry(() => this.send(params));
936
+ break;
937
+ default:
938
+ throw new Error(`Unknown action: ${action}`);
939
+ }
940
+ return {
941
+ success: true,
942
+ data,
943
+ metadata: this.createMetadata(action, Date.now() - startTime, retries)
944
+ };
945
+ } catch (error) {
946
+ return this.handleError(action, error);
947
+ }
948
+ }
949
+ async send(params) {
950
+ const { url, event, payload, secret } = params;
951
+ const body = JSON.stringify({
952
+ event,
953
+ payload: payload ?? {},
954
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
955
+ });
956
+ const headers = {
957
+ "Content-Type": "application/json",
958
+ "X-Almadar-Event": event
959
+ };
960
+ const signingSecret = secret || this.signingSecret;
961
+ if (signingSecret) {
962
+ headers["X-Almadar-Signature"] = "sha256=" + createHmac("sha256", signingSecret).update(body).digest("hex");
963
+ }
964
+ this.logger.debug("Sending webhook", { url: String(url), event: String(event) });
965
+ const startTime = Date.now();
966
+ const response = await fetch(url, {
967
+ method: "POST",
968
+ headers,
969
+ body,
970
+ signal: AbortSignal.timeout(this.timeoutMs)
971
+ });
972
+ if (response.status >= 500) {
973
+ throw new Error(`Webhook endpoint returned ${response.status}`);
974
+ }
975
+ return {
976
+ status: response.status,
977
+ ok: response.ok,
978
+ durationMs: Date.now() - startTime
979
+ };
980
+ }
981
+ };
982
+ registerIntegration("webhook", WebhookIntegration);
983
+ function isParamRecord(value) {
984
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
985
+ }
986
+ function toSubscription(value) {
987
+ if (isParamRecord(value)) {
988
+ const { endpoint, keys } = value;
989
+ if (typeof endpoint === "string" && isParamRecord(keys)) {
990
+ const { p256dh, auth } = keys;
991
+ if (typeof p256dh === "string" && typeof auth === "string") {
992
+ return { endpoint, keys: { p256dh, auth } };
993
+ }
994
+ }
995
+ }
996
+ throw new Error("push.send: subscription must be { endpoint, keys: { p256dh, auth } }");
997
+ }
998
+ var PushIntegration = class extends BaseIntegration {
999
+ constructor(config) {
1000
+ super(config);
1001
+ this.vapidPublicKey = config.env.VAPID_PUBLIC_KEY || "";
1002
+ this.vapidPrivateKey = config.env.VAPID_PRIVATE_KEY || "";
1003
+ this.vapidSubject = config.env.VAPID_SUBJECT || "";
1004
+ this.logger.info("Push integration initialized");
1005
+ }
1006
+ async execute(action, params) {
1007
+ const validation = this.validateParams(action, params);
1008
+ if (!validation.valid) {
1009
+ return {
1010
+ success: false,
1011
+ error: {
1012
+ name: "IntegrationError",
1013
+ message: "Validation failed",
1014
+ code: "VALIDATION_ERROR",
1015
+ details: validation.errors
1016
+ },
1017
+ metadata: this.createMetadata(action, 0)
1018
+ };
1019
+ }
1020
+ const startTime = Date.now();
1021
+ let retries = 0;
1022
+ try {
1023
+ let data;
1024
+ switch (action) {
1025
+ case "send":
1026
+ data = await this.executeWithRetry(() => this.send(params));
1027
+ break;
1028
+ default:
1029
+ throw new Error(`Unknown action: ${action}`);
1030
+ }
1031
+ return {
1032
+ success: true,
1033
+ data,
1034
+ metadata: this.createMetadata(action, Date.now() - startTime, retries)
1035
+ };
1036
+ } catch (error) {
1037
+ return this.handleError(action, error);
1038
+ }
1039
+ }
1040
+ async send(params) {
1041
+ const { subscription, title, body, url, icon } = params;
1042
+ const sub = toSubscription(subscription);
1043
+ const payload = JSON.stringify({
1044
+ title,
1045
+ body,
1046
+ url: url || void 0,
1047
+ icon: icon || void 0
1048
+ });
1049
+ this.logger.debug("Sending push notification", { endpoint: sub.endpoint });
1050
+ try {
1051
+ const response = await webpush.sendNotification(sub, payload, {
1052
+ vapidDetails: {
1053
+ subject: this.vapidSubject,
1054
+ publicKey: this.vapidPublicKey,
1055
+ privateKey: this.vapidPrivateKey
1056
+ }
1057
+ });
1058
+ return { statusCode: response.statusCode, ok: true, expired: false };
1059
+ } catch (error) {
1060
+ if (error instanceof webpush.WebPushError) {
1061
+ if (error.statusCode >= 500) {
1062
+ throw new Error(`Push endpoint returned ${error.statusCode}`);
1063
+ }
1064
+ return {
1065
+ statusCode: error.statusCode,
1066
+ ok: false,
1067
+ expired: error.statusCode === 404 || error.statusCode === 410
1068
+ };
1069
+ }
1070
+ throw error;
1071
+ }
1072
+ }
1073
+ };
1074
+ registerIntegration("push", PushIntegration);
1075
+ var CalendarIntegration = class extends BaseIntegration {
1076
+ constructor(config) {
1077
+ super(config);
1078
+ const rawKey = config.env.GOOGLE_CALENDAR_SA_KEY;
1079
+ if (!rawKey) {
1080
+ throw new Error("GOOGLE_CALENDAR_SA_KEY not configured");
1081
+ }
1082
+ const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
1083
+ const key = JSON.parse(keyJson);
1084
+ const subject = config.env.GOOGLE_CALENDAR_SUBJECT || void 0;
1085
+ const auth = new google.auth.JWT({
1086
+ email: key.client_email,
1087
+ key: key.private_key,
1088
+ scopes: ["https://www.googleapis.com/auth/calendar"],
1089
+ subject
1090
+ });
1091
+ this.client = google.calendar({ version: "v3", auth });
1092
+ this.defaultCalendarId = config.env.GOOGLE_CALENDAR_ID || "primary";
1093
+ this.logger.info("Calendar integration initialized", {
1094
+ delegated: Boolean(subject)
1095
+ });
1096
+ }
1097
+ async execute(action, params) {
1098
+ const validation = this.validateParams(action, params);
1099
+ if (!validation.valid) {
1100
+ return {
1101
+ success: false,
1102
+ error: {
1103
+ name: "IntegrationError",
1104
+ message: "Validation failed",
1105
+ code: "VALIDATION_ERROR",
1106
+ details: validation.errors
1107
+ },
1108
+ metadata: this.createMetadata(action, 0)
1109
+ };
1110
+ }
1111
+ const startTime = Date.now();
1112
+ let retries = 0;
1113
+ try {
1114
+ let data;
1115
+ switch (action) {
1116
+ case "listEvents":
1117
+ data = await this.executeWithRetry(() => this.listEvents(params));
1118
+ break;
1119
+ case "createEvent":
1120
+ data = await this.executeWithRetry(() => this.createEvent(params));
1121
+ break;
1122
+ case "updateEvent":
1123
+ data = await this.executeWithRetry(() => this.updateEvent(params));
1124
+ break;
1125
+ case "deleteEvent":
1126
+ data = await this.executeWithRetry(() => this.deleteEvent(params));
1127
+ break;
1128
+ case "freeBusy":
1129
+ data = await this.executeWithRetry(() => this.freeBusy(params));
1130
+ break;
1131
+ case "watch":
1132
+ data = await this.executeWithRetry(() => this.watch(params));
1133
+ break;
1134
+ case "stopWatch":
1135
+ data = await this.executeWithRetry(() => this.stopWatch(params));
1136
+ break;
1137
+ default:
1138
+ throw new Error(`Unknown action: ${action}`);
1139
+ }
1140
+ return {
1141
+ success: true,
1142
+ data,
1143
+ metadata: this.createMetadata(action, Date.now() - startTime, retries)
1144
+ };
1145
+ } catch (error) {
1146
+ return this.handleError(action, error);
1147
+ }
1148
+ }
1149
+ calendarId(params) {
1150
+ return params.calendarId || this.defaultCalendarId;
1151
+ }
1152
+ toEventTime(value) {
1153
+ return value.length === 10 ? { date: value } : { dateTime: value };
1154
+ }
1155
+ fromEventTime(time) {
1156
+ return time?.dateTime ?? time?.date ?? "";
1157
+ }
1158
+ async listEvents(params) {
1159
+ const { timeMin, timeMax, syncToken, maxResults } = params;
1160
+ const response = await this.client.events.list({
1161
+ calendarId: this.calendarId(params),
1162
+ // Incremental sync: a syncToken supersedes the window params (the API
1163
+ // rejects combining them).
1164
+ ...syncToken ? { syncToken } : {
1165
+ timeMin: timeMin || void 0,
1166
+ timeMax: timeMax || void 0,
1167
+ singleEvents: true,
1168
+ orderBy: "startTime"
1169
+ },
1170
+ maxResults: maxResults || 250
1171
+ });
1172
+ const items = response.data.items ?? [];
1173
+ return {
1174
+ events: items.map((event) => ({
1175
+ id: event.id ?? "",
1176
+ summary: event.summary ?? "",
1177
+ description: event.description ?? "",
1178
+ location: event.location ?? "",
1179
+ start: this.fromEventTime(event.start ?? void 0),
1180
+ end: this.fromEventTime(event.end ?? void 0),
1181
+ status: event.status ?? "",
1182
+ updated: event.updated ?? ""
1183
+ })),
1184
+ nextSyncToken: response.data.nextSyncToken ?? null
1185
+ };
1186
+ }
1187
+ resolveEnd(start, end, durationMinutes) {
1188
+ if (end) return end;
1189
+ if (start.length === 10) {
1190
+ const next = new Date(Date.parse(start) + 24 * 60 * 6e4);
1191
+ return next.toISOString().slice(0, 10);
1192
+ }
1193
+ if (durationMinutes && durationMinutes > 0) {
1194
+ return new Date(Date.parse(start) + durationMinutes * 6e4).toISOString();
1195
+ }
1196
+ throw new Error("createEvent requires `end` or a positive `durationMinutes`");
1197
+ }
1198
+ async createEvent(params) {
1199
+ const { summary, description, location, start, end, durationMinutes } = params;
1200
+ const response = await this.client.events.insert({
1201
+ calendarId: this.calendarId(params),
1202
+ requestBody: {
1203
+ summary,
1204
+ description: description || void 0,
1205
+ location: location || void 0,
1206
+ start: this.toEventTime(start),
1207
+ end: this.toEventTime(
1208
+ this.resolveEnd(start, end || void 0, durationMinutes || void 0)
1209
+ )
1210
+ }
1211
+ });
1212
+ return {
1213
+ id: response.data.id ?? "",
1214
+ status: response.data.status ?? "",
1215
+ htmlLink: response.data.htmlLink ?? ""
1216
+ };
1217
+ }
1218
+ async updateEvent(params) {
1219
+ const { eventId, summary, description, location, start, end } = params;
1220
+ const requestBody = {};
1221
+ if (typeof summary === "string") requestBody.summary = summary;
1222
+ if (typeof description === "string") requestBody.description = description;
1223
+ if (typeof location === "string") requestBody.location = location;
1224
+ if (typeof start === "string" && start) requestBody.start = this.toEventTime(start);
1225
+ if (typeof end === "string" && end) requestBody.end = this.toEventTime(end);
1226
+ const response = await this.client.events.patch({
1227
+ calendarId: this.calendarId(params),
1228
+ eventId,
1229
+ requestBody
1230
+ });
1231
+ return {
1232
+ id: response.data.id ?? "",
1233
+ status: response.data.status ?? ""
1234
+ };
1235
+ }
1236
+ async deleteEvent(params) {
1237
+ const { eventId } = params;
1238
+ await this.client.events.delete({
1239
+ calendarId: this.calendarId(params),
1240
+ eventId
1241
+ });
1242
+ return { id: eventId, deleted: true };
1243
+ }
1244
+ async freeBusy(params) {
1245
+ const { timeMin, timeMax } = params;
1246
+ const id = this.calendarId(params);
1247
+ const response = await this.client.freebusy.query({
1248
+ requestBody: {
1249
+ timeMin,
1250
+ timeMax,
1251
+ items: [{ id }]
1252
+ }
1253
+ });
1254
+ const busy = response.data.calendars?.[id]?.busy ?? [];
1255
+ return {
1256
+ busy: busy.map((slot) => ({ start: slot.start ?? "", end: slot.end ?? "" }))
1257
+ };
1258
+ }
1259
+ async watch(params) {
1260
+ const { channelId, address, ttlSeconds } = params;
1261
+ const response = await this.client.events.watch({
1262
+ calendarId: this.calendarId(params),
1263
+ requestBody: {
1264
+ id: channelId,
1265
+ type: "web_hook",
1266
+ address,
1267
+ params: ttlSeconds ? { ttl: String(ttlSeconds) } : void 0
1268
+ }
1269
+ });
1270
+ return {
1271
+ channelId: response.data.id ?? channelId,
1272
+ resourceId: response.data.resourceId ?? "",
1273
+ expiration: response.data.expiration ?? ""
1274
+ };
1275
+ }
1276
+ async stopWatch(params) {
1277
+ const { channelId, resourceId } = params;
1278
+ await this.client.channels.stop({
1279
+ requestBody: {
1280
+ id: channelId,
1281
+ resourceId
1282
+ }
1283
+ });
1284
+ return { stopped: true };
1285
+ }
1286
+ };
1287
+ registerIntegration("calendar", CalendarIntegration);
1288
+ function decodeContent(content) {
1289
+ const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
1290
+ if (dataUrlMatch) {
1291
+ const [, mime, isB64, body] = dataUrlMatch;
1292
+ const bytes = isB64 ? Buffer.from(body, "base64") : Buffer.from(decodeURIComponent(body), "utf8");
1293
+ return { bytes, contentType: mime || null };
1294
+ }
1295
+ return { bytes: Buffer.from(content, "utf8"), contentType: null };
1296
+ }
1297
+ var DriveIntegration = class extends BaseIntegration {
1298
+ constructor(config) {
1299
+ super(config);
1300
+ const rawKey = config.env.GOOGLE_DRIVE_SA_KEY;
1301
+ if (!rawKey) {
1302
+ throw new Error("GOOGLE_DRIVE_SA_KEY not configured");
1303
+ }
1304
+ const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
1305
+ const key = JSON.parse(keyJson);
1306
+ const subject = config.env.GOOGLE_DRIVE_SUBJECT || void 0;
1307
+ const auth = new google.auth.JWT({
1308
+ email: key.client_email,
1309
+ key: key.private_key,
1310
+ scopes: ["https://www.googleapis.com/auth/drive"],
1311
+ subject
1312
+ });
1313
+ this.client = google.drive({ version: "v3", auth });
1314
+ this.logger.info("Drive integration initialized", { delegated: Boolean(subject) });
1315
+ }
1316
+ async execute(action, params) {
1317
+ const validation = this.validateParams(action, params);
1318
+ if (!validation.valid) {
1319
+ return {
1320
+ success: false,
1321
+ error: {
1322
+ name: "IntegrationError",
1323
+ message: "Validation failed",
1324
+ code: "VALIDATION_ERROR",
1325
+ details: validation.errors
1326
+ },
1327
+ metadata: this.createMetadata(action, 0)
1328
+ };
1329
+ }
1330
+ const startTime = Date.now();
1331
+ try {
1332
+ let data;
1333
+ switch (action) {
1334
+ case "listFiles":
1335
+ data = await this.executeWithRetry(() => this.listFiles(params));
1336
+ break;
1337
+ case "getFile":
1338
+ data = await this.executeWithRetry(() => this.getFile(params));
1339
+ break;
1340
+ case "uploadFile":
1341
+ data = await this.executeWithRetry(() => this.uploadFile(params));
1342
+ break;
1343
+ case "createFolder":
1344
+ data = await this.executeWithRetry(() => this.createFolder(params));
1345
+ break;
1346
+ case "shareFile":
1347
+ data = await this.executeWithRetry(() => this.shareFile(params));
1348
+ break;
1349
+ default:
1350
+ throw new Error(`Unknown action: ${action}`);
1351
+ }
1352
+ return {
1353
+ success: true,
1354
+ data,
1355
+ metadata: this.createMetadata(action, Date.now() - startTime)
1356
+ };
1357
+ } catch (error) {
1358
+ return this.handleError(action, error);
1359
+ }
1360
+ }
1361
+ async listFiles(params) {
1362
+ const { folderId, query, maxResults } = params;
1363
+ const clauses = ["trashed = false"];
1364
+ if (folderId) clauses.push(`'${String(folderId).replace(/'/g, "\\'")}' in parents`);
1365
+ if (query) clauses.push(String(query));
1366
+ const response = await this.client.files.list({
1367
+ q: clauses.join(" and "),
1368
+ pageSize: maxResults || 100,
1369
+ fields: "files(id, name, mimeType, size, modifiedTime, webViewLink)"
1370
+ });
1371
+ return {
1372
+ files: (response.data.files ?? []).map((file) => ({
1373
+ id: file.id ?? "",
1374
+ name: file.name ?? "",
1375
+ mimeType: file.mimeType ?? "",
1376
+ size: Number(file.size ?? 0),
1377
+ modifiedTime: file.modifiedTime ?? "",
1378
+ webViewLink: file.webViewLink ?? ""
1379
+ }))
1380
+ };
1381
+ }
1382
+ async getFile(params) {
1383
+ const fileId = params.fileId;
1384
+ const meta = await this.client.files.get({
1385
+ fileId,
1386
+ fields: "id, name, mimeType, size"
1387
+ });
1388
+ const content = await this.client.files.get(
1389
+ { fileId, alt: "media" },
1390
+ { responseType: "arraybuffer" }
1391
+ );
1392
+ const bytes = Buffer.from(content.data);
1393
+ return {
1394
+ id: meta.data.id ?? fileId,
1395
+ name: meta.data.name ?? "",
1396
+ mimeType: meta.data.mimeType ?? "application/octet-stream",
1397
+ content: bytes.toString("base64"),
1398
+ size: bytes.length
1399
+ };
1400
+ }
1401
+ async uploadFile(params) {
1402
+ const { name, content, mimeType, folderId } = params;
1403
+ const { bytes, contentType } = decodeContent(content);
1404
+ const response = await this.client.files.create({
1405
+ requestBody: {
1406
+ name,
1407
+ parents: folderId ? [folderId] : void 0
1408
+ },
1409
+ media: {
1410
+ mimeType: mimeType || contentType || "application/octet-stream",
1411
+ body: Readable.from(bytes)
1412
+ },
1413
+ fields: "id, name, webViewLink"
1414
+ });
1415
+ return {
1416
+ id: response.data.id ?? "",
1417
+ name: response.data.name ?? name,
1418
+ webViewLink: response.data.webViewLink ?? ""
1419
+ };
1420
+ }
1421
+ async createFolder(params) {
1422
+ const { name, parentId } = params;
1423
+ const response = await this.client.files.create({
1424
+ requestBody: {
1425
+ name,
1426
+ mimeType: "application/vnd.google-apps.folder",
1427
+ parents: parentId ? [parentId] : void 0
1428
+ },
1429
+ fields: "id, name"
1430
+ });
1431
+ return { id: response.data.id ?? "", name: response.data.name ?? name };
1432
+ }
1433
+ async shareFile(params) {
1434
+ const { fileId, email, role } = params;
1435
+ const response = await this.client.permissions.create({
1436
+ fileId,
1437
+ requestBody: {
1438
+ type: "user",
1439
+ role: role || "reader",
1440
+ emailAddress: email
1441
+ },
1442
+ fields: "id"
1443
+ });
1444
+ return { shared: true, permissionId: response.data.id ?? "" };
1445
+ }
1446
+ };
1447
+ registerIntegration("drive", DriveIntegration);
1448
+
1449
+ // src/integrations/metaAds/index.ts
1450
+ var GRAPH_BASE = "https://graph.facebook.com/v21.0";
1451
+ var MetaAdsIntegration = class extends BaseIntegration {
1452
+ constructor(config) {
1453
+ super(config);
1454
+ this.accessToken = config.env.META_ACCESS_TOKEN || "";
1455
+ if (!this.accessToken) {
1456
+ throw new Error("META_ACCESS_TOKEN not configured");
1457
+ }
1458
+ this.defaultAccountId = config.env.META_AD_ACCOUNT_ID || "";
1459
+ this.logger.info("Meta Ads integration initialized");
1460
+ }
1461
+ async execute(action, params) {
1462
+ const validation = this.validateParams(action, params);
1463
+ if (!validation.valid) {
1464
+ return {
1465
+ success: false,
1466
+ error: {
1467
+ name: "IntegrationError",
1468
+ message: "Validation failed",
1469
+ code: "VALIDATION_ERROR",
1470
+ details: validation.errors
1471
+ },
1472
+ metadata: this.createMetadata(action, 0)
1473
+ };
1474
+ }
1475
+ const startTime = Date.now();
1476
+ try {
1477
+ let data;
1478
+ switch (action) {
1479
+ case "getSpend":
1480
+ data = await this.executeWithRetry(() => this.getSpend(params));
1481
+ break;
1482
+ case "listCampaigns":
1483
+ data = await this.executeWithRetry(() => this.listCampaigns(params));
1484
+ break;
1485
+ default:
1486
+ throw new Error(`Unknown action: ${action}`);
1487
+ }
1488
+ return {
1489
+ success: true,
1490
+ data,
1491
+ metadata: this.createMetadata(action, Date.now() - startTime)
1492
+ };
1493
+ } catch (error) {
1494
+ return this.handleError(action, error);
1495
+ }
1496
+ }
1497
+ accountId(params) {
1498
+ const id = params.accountId || this.defaultAccountId;
1499
+ if (!id) {
1500
+ throw new Error("No ad account: pass `accountId` or set META_AD_ACCOUNT_ID");
1501
+ }
1502
+ return id.startsWith("act_") ? id : `act_${id}`;
1503
+ }
1504
+ async graphGet(path, query) {
1505
+ const url = new URL(`${GRAPH_BASE}/${path}`);
1506
+ for (const [key, value] of Object.entries(query)) {
1507
+ url.searchParams.set(key, value);
1508
+ }
1509
+ url.searchParams.set("access_token", this.accessToken);
1510
+ const response = await fetch(url, { signal: AbortSignal.timeout(15e3) });
1511
+ if (response.status >= 500) {
1512
+ throw new Error(`Meta Graph API returned ${response.status}`);
1513
+ }
1514
+ const body = await response.json();
1515
+ if (!response.ok) {
1516
+ throw new Error(`Meta Graph API error: ${body.error?.message ?? response.status}`);
1517
+ }
1518
+ return body.data ?? body;
1519
+ }
1520
+ async getSpend(params) {
1521
+ const { since, until } = params;
1522
+ const rows = await this.graphGet(`${this.accountId(params)}/insights`, {
1523
+ fields: "spend,impressions,clicks,account_currency",
1524
+ time_range: JSON.stringify({ since, until }),
1525
+ level: "account"
1526
+ });
1527
+ const row = Array.isArray(rows) ? rows[0] : void 0;
1528
+ return {
1529
+ spend: Number(row?.spend ?? 0),
1530
+ currency: row?.account_currency ?? "",
1531
+ impressions: Number(row?.impressions ?? 0),
1532
+ clicks: Number(row?.clicks ?? 0)
1533
+ };
1534
+ }
1535
+ async listCampaigns(params) {
1536
+ const { status } = params;
1537
+ const rows = await this.graphGet(`${this.accountId(params)}/campaigns`, {
1538
+ fields: "id,name,status,daily_budget",
1539
+ ...status ? { effective_status: JSON.stringify([status]) } : {}
1540
+ });
1541
+ return {
1542
+ campaigns: (Array.isArray(rows) ? rows : []).map((row) => ({
1543
+ id: row.id ?? "",
1544
+ name: row.name ?? "",
1545
+ status: row.status ?? "",
1546
+ // Meta reports budgets in minor units (cents).
1547
+ dailyBudget: Number(row.daily_budget ?? 0) / 100
1548
+ }))
1549
+ };
1550
+ }
1551
+ };
1552
+ registerIntegration("metaAds", MetaAdsIntegration);
1553
+
1554
+ // src/integrations/accounting/index.ts
1555
+ var AccountingIntegration = class extends BaseIntegration {
1556
+ constructor(config) {
1557
+ super(config);
1558
+ this.logger.info("Accounting integration initialized (generic CSV export)");
1559
+ }
1560
+ async execute(action, params) {
1561
+ const validation = this.validateParams(action, params);
1562
+ if (!validation.valid) {
1563
+ return {
1564
+ success: false,
1565
+ error: {
1566
+ name: "IntegrationError",
1567
+ message: "Validation failed",
1568
+ code: "VALIDATION_ERROR",
1569
+ details: validation.errors
1570
+ },
1571
+ metadata: this.createMetadata(action, 0)
1572
+ };
1573
+ }
1574
+ const startTime = Date.now();
1575
+ try {
1576
+ let data;
1577
+ switch (action) {
1578
+ case "exportInvoices":
1579
+ data = this.exportRows(params.invoices, INVOICE_COLUMNS, "invoices");
1580
+ break;
1581
+ case "exportJournal":
1582
+ data = this.exportRows(params.entries, JOURNAL_COLUMNS, "journal");
1583
+ break;
1584
+ default:
1585
+ throw new Error(`Unknown action: ${action}`);
1586
+ }
1587
+ return {
1588
+ success: true,
1589
+ data,
1590
+ metadata: this.createMetadata(action, Date.now() - startTime)
1591
+ };
1592
+ } catch (error) {
1593
+ return this.handleError(action, error);
1594
+ }
1595
+ }
1596
+ exportRows(rowsValue, columns, kind) {
1597
+ if (!Array.isArray(rowsValue)) {
1598
+ throw new Error(`${kind} export requires an array of rows`);
1599
+ }
1600
+ const lines = [columns.join(",")];
1601
+ for (const row of rowsValue) {
1602
+ if (row === null || typeof row !== "object" || Array.isArray(row) || row instanceof Date) {
1603
+ throw new Error(`${kind} export: every row must be an object`);
1604
+ }
1605
+ const record = row;
1606
+ lines.push(columns.map((column) => csvCell(record[column])).join(","));
1607
+ }
1608
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1609
+ return {
1610
+ content: lines.join("\r\n") + "\r\n",
1611
+ filename: `${kind}-export-${stamp}.csv`,
1612
+ count: rowsValue.length
1613
+ };
1614
+ }
1615
+ };
1616
+ var INVOICE_COLUMNS = ["id", "number", "customer", "issuedAt", "dueAt", "currency", "net", "tax", "gross", "status"];
1617
+ var JOURNAL_COLUMNS = ["date", "account", "description", "debit", "credit", "reference"];
1618
+ function csvCell(value) {
1619
+ if (value === void 0 || value === null) return "";
1620
+ const raw = value instanceof Date ? value.toISOString() : String(value);
1621
+ return /[",\r\n]/.test(raw) ? `"${raw.replace(/"/g, '""')}"` : raw;
1622
+ }
1623
+ registerIntegration("accounting", AccountingIntegration);
1624
+
1625
+ // src/integrations/banking/index.ts
1626
+ var GC_BASE = "https://bankaccountdata.gocardless.com/api/v2";
1627
+ var BankingIntegration = class extends BaseIntegration {
1628
+ constructor(config) {
1629
+ super(config);
1630
+ this.accessToken = null;
1631
+ this.accessTokenExpiresAt = 0;
1632
+ this.secretId = config.env.GOCARDLESS_SECRET_ID || "";
1633
+ this.secretKey = config.env.GOCARDLESS_SECRET_KEY || "";
1634
+ if (!this.secretId || !this.secretKey) {
1635
+ throw new Error("GOCARDLESS_SECRET_ID / GOCARDLESS_SECRET_KEY not configured");
1636
+ }
1637
+ this.logger.info("Banking integration initialized (GoCardless Bank Account Data)");
1638
+ }
1639
+ async execute(action, params) {
1640
+ const validation = this.validateParams(action, params);
1641
+ if (!validation.valid) {
1642
+ return {
1643
+ success: false,
1644
+ error: {
1645
+ name: "IntegrationError",
1646
+ message: "Validation failed",
1647
+ code: "VALIDATION_ERROR",
1648
+ details: validation.errors
1649
+ },
1650
+ metadata: this.createMetadata(action, 0)
1651
+ };
1652
+ }
1653
+ const startTime = Date.now();
1654
+ try {
1655
+ let data;
1656
+ switch (action) {
1657
+ case "createRequisition":
1658
+ data = await this.executeWithRetry(() => this.createRequisition(params));
1659
+ break;
1660
+ case "listAccounts":
1661
+ data = await this.executeWithRetry(() => this.listAccounts(params));
1662
+ break;
1663
+ case "listTransactions":
1664
+ data = await this.executeWithRetry(() => this.listTransactions(params));
1665
+ break;
1666
+ default:
1667
+ throw new Error(`Unknown action: ${action}`);
1668
+ }
1669
+ return {
1670
+ success: true,
1671
+ data,
1672
+ metadata: this.createMetadata(action, Date.now() - startTime)
1673
+ };
1674
+ } catch (error) {
1675
+ return this.handleError(action, error);
1676
+ }
1677
+ }
1678
+ async token() {
1679
+ if (this.accessToken && Date.now() < this.accessTokenExpiresAt - 6e4) {
1680
+ return this.accessToken;
1681
+ }
1682
+ const response = await fetch(`${GC_BASE}/token/new/`, {
1683
+ method: "POST",
1684
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
1685
+ body: JSON.stringify({ secret_id: this.secretId, secret_key: this.secretKey }),
1686
+ signal: AbortSignal.timeout(15e3)
1687
+ });
1688
+ if (!response.ok) {
1689
+ throw new Error(`GoCardless token request failed: ${response.status}`);
1690
+ }
1691
+ const body = await response.json();
1692
+ this.accessToken = body.access;
1693
+ this.accessTokenExpiresAt = Date.now() + body.access_expires * 1e3;
1694
+ return this.accessToken;
1695
+ }
1696
+ async gcRequest(path, init2) {
1697
+ const response = await fetch(`${GC_BASE}${path}`, {
1698
+ method: init2?.method ?? "GET",
1699
+ headers: {
1700
+ Accept: "application/json",
1701
+ "Content-Type": "application/json",
1702
+ Authorization: `Bearer ${await this.token()}`
1703
+ },
1704
+ body: init2?.body === void 0 ? void 0 : JSON.stringify(init2.body),
1705
+ signal: AbortSignal.timeout(2e4)
1706
+ });
1707
+ if (response.status >= 500) {
1708
+ throw new Error(`GoCardless returned ${response.status}`);
1709
+ }
1710
+ const body = await response.json();
1711
+ if (!response.ok) {
1712
+ throw new Error(`GoCardless error ${response.status}: ${JSON.stringify(body).slice(0, 300)}`);
1713
+ }
1714
+ return body;
1715
+ }
1716
+ async createRequisition(params) {
1717
+ const { institutionId, redirectUrl, reference } = params;
1718
+ const body = await this.gcRequest("/requisitions/", {
1719
+ method: "POST",
1720
+ body: {
1721
+ institution_id: institutionId,
1722
+ redirect: redirectUrl,
1723
+ reference: reference || void 0
1724
+ }
1725
+ });
1726
+ return { requisitionId: body.id ?? "", link: body.link ?? "" };
1727
+ }
1728
+ async listAccounts(params) {
1729
+ const { requisitionId } = params;
1730
+ const body = await this.gcRequest(`/requisitions/${requisitionId}/`);
1731
+ return { accounts: body.accounts ?? [] };
1732
+ }
1733
+ async listTransactions(params) {
1734
+ const { accountId, dateFrom, dateTo } = params;
1735
+ const query = new URLSearchParams();
1736
+ if (dateFrom) query.set("date_from", dateFrom);
1737
+ if (dateTo) query.set("date_to", dateTo);
1738
+ const suffix = query.size > 0 ? `?${query.toString()}` : "";
1739
+ const body = await this.gcRequest(`/accounts/${accountId}/transactions/${suffix}`);
1740
+ return {
1741
+ transactions: (body.transactions?.booked ?? []).map((tx) => ({
1742
+ id: tx.transactionId ?? tx.internalTransactionId ?? "",
1743
+ amount: Number(tx.transactionAmount?.amount ?? 0),
1744
+ currency: tx.transactionAmount?.currency ?? "",
1745
+ date: tx.bookingDate ?? "",
1746
+ description: tx.remittanceInformationUnstructured ?? "",
1747
+ counterparty: tx.creditorName ?? tx.debtorName ?? ""
1748
+ }))
1749
+ };
1750
+ }
1751
+ };
1752
+ registerIntegration("banking", BankingIntegration);
1753
+
1754
+ // src/integrations/esign/index.ts
1755
+ var EsignIntegration = class extends BaseIntegration {
1756
+ constructor(config) {
1757
+ super(config);
1758
+ if (!config.env.DOCUSIGN_BASE_URL || !config.env.DOCUSIGN_ACCESS_TOKEN) {
1759
+ throw new Error("DOCUSIGN_BASE_URL / DOCUSIGN_ACCESS_TOKEN not configured");
1760
+ }
1761
+ this.logger.info("E-sign integration initialized (DocuSign)");
1762
+ }
1763
+ async execute(action, params) {
1764
+ const validation = this.validateParams(action, params);
1765
+ if (!validation.valid) {
1766
+ return {
1767
+ success: false,
1768
+ error: {
1769
+ name: "IntegrationError",
1770
+ message: "Validation failed",
1771
+ code: "VALIDATION_ERROR",
1772
+ details: validation.errors
1773
+ },
1774
+ metadata: this.createMetadata(action, 0)
1775
+ };
1776
+ }
1777
+ const startTime = Date.now();
1778
+ try {
1779
+ let data;
1780
+ switch (action) {
1781
+ case "sendEnvelope":
1782
+ data = await this.executeWithRetry(() => this.sendEnvelope(params));
1783
+ break;
1784
+ case "getEnvelopeStatus":
1785
+ data = await this.executeWithRetry(() => this.getEnvelopeStatus(params));
1786
+ break;
1787
+ case "downloadDocument":
1788
+ data = await this.executeWithRetry(() => this.downloadDocument(params));
1789
+ break;
1790
+ default:
1791
+ throw new Error(`Unknown action: ${action}`);
1792
+ }
1793
+ return {
1794
+ success: true,
1795
+ data,
1796
+ metadata: this.createMetadata(action, Date.now() - startTime)
804
1797
  };
805
1798
  } catch (error) {
806
1799
  return this.handleError(action, error);
807
1800
  }
808
1801
  }
809
- async send(params) {
810
- const { to, subject, body, from } = params;
811
- this.logger.debug("Sending email", { to: String(to), subject: String(subject), provider: this.provider });
812
- if (this.provider === "sendgrid") {
813
- return await this.sendViaSendGrid(
814
- to,
815
- subject,
816
- body,
817
- from || this.fromEmail
818
- );
819
- } else if (this.provider === "resend") {
820
- return await this.sendViaResend(
821
- to,
822
- subject,
823
- body,
824
- from || this.fromEmail
825
- );
1802
+ async dsRequest(path, init2) {
1803
+ const base = this.config.env.DOCUSIGN_BASE_URL.replace(/\/$/, "");
1804
+ const response = await fetch(`${base}${path}`, {
1805
+ method: init2?.method ?? "GET",
1806
+ headers: {
1807
+ Accept: init2?.raw ? "application/pdf" : "application/json",
1808
+ "Content-Type": "application/json",
1809
+ Authorization: `Bearer ${this.config.env.DOCUSIGN_ACCESS_TOKEN}`
1810
+ },
1811
+ body: init2?.body === void 0 ? void 0 : JSON.stringify(init2.body),
1812
+ signal: AbortSignal.timeout(3e4)
1813
+ });
1814
+ if (response.status >= 500) {
1815
+ throw new Error(`DocuSign returned ${response.status}`);
826
1816
  }
827
- throw new Error(`Unknown email provider: ${this.provider}`);
1817
+ if (!response.ok) {
1818
+ const detail = await response.text();
1819
+ throw new Error(`DocuSign error ${response.status}: ${detail.slice(0, 300)}`);
1820
+ }
1821
+ if (init2?.raw) {
1822
+ return Buffer.from(await response.arrayBuffer());
1823
+ }
1824
+ return response.json();
828
1825
  }
829
- async sendViaSendGrid(to, subject, body, from) {
830
- const msg = {
831
- to,
832
- from,
833
- subject,
834
- html: body
835
- };
836
- const response = await sgMail.send(msg);
837
- return {
838
- id: response[0].headers["x-message-id"],
839
- status: "sent"
840
- };
1826
+ async sendEnvelope(params) {
1827
+ const { recipientEmail, recipientName, documentName, documentContent, emailSubject } = params;
1828
+ const rawContent = documentContent;
1829
+ const base64 = rawContent.startsWith("data:") ? rawContent.slice(rawContent.indexOf(",") + 1) : rawContent;
1830
+ const body = await this.dsRequest("/envelopes", {
1831
+ method: "POST",
1832
+ body: {
1833
+ emailSubject: emailSubject || `Please sign: ${documentName}`,
1834
+ status: "sent",
1835
+ documents: [
1836
+ {
1837
+ documentBase64: base64,
1838
+ name: documentName,
1839
+ fileExtension: String(documentName).split(".").pop() || "pdf",
1840
+ documentId: "1"
1841
+ }
1842
+ ],
1843
+ recipients: {
1844
+ signers: [
1845
+ {
1846
+ email: recipientEmail,
1847
+ name: recipientName,
1848
+ recipientId: "1",
1849
+ routingOrder: "1"
1850
+ }
1851
+ ]
1852
+ }
1853
+ }
1854
+ });
1855
+ return { envelopeId: body.envelopeId ?? "", status: body.status ?? "sent" };
841
1856
  }
842
- async sendViaResend(to, subject, body, from) {
843
- if (!this.resendClient) {
844
- throw new Error("Resend client not initialized");
845
- }
846
- const response = await this.resendClient.emails.send({
847
- from,
848
- to,
849
- subject,
850
- html: body
1857
+ async getEnvelopeStatus(params) {
1858
+ const { envelopeId } = params;
1859
+ const body = await this.dsRequest(`/envelopes/${envelopeId}`);
1860
+ return { status: body.status ?? "", completedAt: body.completedDateTime ?? "" };
1861
+ }
1862
+ async downloadDocument(params) {
1863
+ const { envelopeId } = params;
1864
+ const bytes = await this.dsRequest(`/envelopes/${envelopeId}/documents/combined`, {
1865
+ raw: true
851
1866
  });
852
- return {
853
- id: response.data?.id,
854
- status: "sent"
855
- };
1867
+ return { content: bytes.toString("base64"), documentName: `envelope-${envelopeId}.pdf` };
856
1868
  }
857
1869
  };
858
- registerIntegration("email", EmailIntegration);
1870
+ registerIntegration("esign", EsignIntegration);
859
1871
  var LLMIntegration = class extends BaseIntegration {
860
1872
  constructor(config) {
861
1873
  super(config);
@@ -2426,25 +3438,35 @@ var OtelIntegration = class extends BaseIntegration {
2426
3438
  }
2427
3439
  };
2428
3440
  registerIntegration("otel", OtelIntegration);
2429
-
2430
- // src/integrations/oauth/index.ts
2431
3441
  var PROVIDER_AUTH_URLS = {
2432
3442
  google: "https://accounts.google.com/o/oauth2/v2/auth",
2433
3443
  github: "https://github.com/login/oauth/authorize",
2434
3444
  auth0: "https://auth.example.com/authorize"
2435
3445
  };
3446
+ var PROVIDER_ISSUERS = {
3447
+ google: "https://accounts.google.com"
3448
+ };
2436
3449
  var OAuthIntegration = class extends BaseIntegration {
2437
3450
  constructor(config) {
2438
3451
  super(config);
2439
- /** Maps state token -> provider for pending authorization flows */
3452
+ /** Maps state token -> provider for pending MOCK authorization flows */
2440
3453
  this.states = /* @__PURE__ */ new Map();
2441
- /** Maps access token -> token set */
3454
+ /** Maps access token -> token set (mock backend) */
2442
3455
  this.tokens = /* @__PURE__ */ new Map();
2443
- /** Maps refresh token -> access token for refresh lookups */
3456
+ /** Maps refresh token -> access token for refresh lookups (mock backend) */
2444
3457
  this.refreshIndex = /* @__PURE__ */ new Map();
2445
3458
  /** Maps access token -> mock user session */
2446
3459
  this.sessions = /* @__PURE__ */ new Map();
2447
- this.logger.info("OAuth integration initialized (mock backend)");
3460
+ /** Maps state -> pending OIDC authorization (real backend) */
3461
+ this.pending = /* @__PURE__ */ new Map();
3462
+ /** Maps access token -> ID-token subject, for userinfo subject checks */
3463
+ this.subjects = /* @__PURE__ */ new Map();
3464
+ /** Discovered issuer configurations, keyed by issuer URL */
3465
+ this.discovered = /* @__PURE__ */ new Map();
3466
+ this.real = config.env.OAUTH_MODE !== "mock" && Boolean(config.env.OAUTH_CLIENT_ID) && Boolean(config.env.OAUTH_CLIENT_SECRET);
3467
+ this.logger.info(
3468
+ this.real ? "OAuth integration initialized (OIDC backend via openid-client)" : "OAuth integration initialized (mock backend)"
3469
+ );
2448
3470
  }
2449
3471
  async execute(action, params) {
2450
3472
  const validation = this.validateParams(action, params);
@@ -2465,19 +3487,19 @@ var OAuthIntegration = class extends BaseIntegration {
2465
3487
  let data;
2466
3488
  switch (action) {
2467
3489
  case "authorize":
2468
- data = await this.executeWithRetry(() => this.authorize(params));
3490
+ data = await this.executeWithRetry(() => this.real ? this.oidcAuthorize(params) : this.authorize(params));
2469
3491
  break;
2470
3492
  case "token":
2471
- data = await this.executeWithRetry(() => this.token(params));
3493
+ data = await this.executeWithRetry(() => this.real ? this.oidcToken(params) : this.token(params));
2472
3494
  break;
2473
3495
  case "refresh":
2474
- data = await this.executeWithRetry(() => this.refresh(params));
3496
+ data = await this.executeWithRetry(() => this.real ? this.oidcRefresh(params) : this.refresh(params));
2475
3497
  break;
2476
3498
  case "revoke":
2477
- data = await this.executeWithRetry(() => this.revoke(params));
3499
+ data = await this.executeWithRetry(() => this.real ? this.oidcRevoke(params) : this.revoke(params));
2478
3500
  break;
2479
3501
  case "userinfo":
2480
- data = await this.executeWithRetry(() => this.userinfo(params));
3502
+ data = await this.executeWithRetry(() => this.real ? this.oidcUserinfo(params) : this.userinfo(params));
2481
3503
  break;
2482
3504
  default:
2483
3505
  throw new Error(`Unknown action: ${action}`);
@@ -2492,7 +3514,119 @@ var OAuthIntegration = class extends BaseIntegration {
2492
3514
  }
2493
3515
  }
2494
3516
  // ---------------------------------------------------------------------------
2495
- // Helpers
3517
+ // OIDC backend (openid-client)
3518
+ // ---------------------------------------------------------------------------
3519
+ issuerFor(provider) {
3520
+ const configured = this.config.env.OIDC_ISSUER_URL;
3521
+ if (configured) return configured;
3522
+ const issuer = PROVIDER_ISSUERS[provider];
3523
+ if (!issuer) {
3524
+ throw new Error(
3525
+ `Provider "${provider}" has no OIDC issuer \u2014 set OIDC_ISSUER_URL to an OIDC-compliant issuer, or use OAUTH_MODE=mock`
3526
+ );
3527
+ }
3528
+ return issuer;
3529
+ }
3530
+ async configurationFor(provider) {
3531
+ const issuer = this.issuerFor(provider);
3532
+ const cached = this.discovered.get(issuer);
3533
+ if (cached) return cached;
3534
+ const configuration = await oidc.discovery(
3535
+ new URL(issuer),
3536
+ this.config.env.OAUTH_CLIENT_ID,
3537
+ this.config.env.OAUTH_CLIENT_SECRET
3538
+ );
3539
+ this.discovered.set(issuer, configuration);
3540
+ return configuration;
3541
+ }
3542
+ async oidcAuthorize(params) {
3543
+ const provider = params.provider;
3544
+ const scopes = params.scopes;
3545
+ const redirectUri = params.redirectUri || this.config.env.OAUTH_REDIRECT_URI;
3546
+ const configuration = await this.configurationFor(provider);
3547
+ const state = oidc.randomState();
3548
+ const pkceVerifier = oidc.randomPKCECodeVerifier();
3549
+ const codeChallenge = await oidc.calculatePKCECodeChallenge(pkceVerifier);
3550
+ const parameters = {
3551
+ redirect_uri: redirectUri,
3552
+ scope: scopes.join(" "),
3553
+ state,
3554
+ code_challenge: codeChallenge,
3555
+ code_challenge_method: "S256"
3556
+ };
3557
+ if (provider === "google") {
3558
+ parameters.access_type = "offline";
3559
+ parameters.prompt = "consent";
3560
+ }
3561
+ const authUrl = oidc.buildAuthorizationUrl(configuration, parameters);
3562
+ this.pending.set(state, { provider, redirectUri, pkceVerifier });
3563
+ return { authUrl: authUrl.toString(), state };
3564
+ }
3565
+ async oidcToken(params) {
3566
+ const code = params.code;
3567
+ const state = params.state;
3568
+ const pendingAuth = this.pending.get(state);
3569
+ if (!pendingAuth) {
3570
+ throw new Error(`Invalid or expired state token: ${state}`);
3571
+ }
3572
+ this.pending.delete(state);
3573
+ const configuration = await this.configurationFor(pendingAuth.provider);
3574
+ const callbackUrl = new URL(pendingAuth.redirectUri);
3575
+ callbackUrl.searchParams.set("code", code);
3576
+ callbackUrl.searchParams.set("state", state);
3577
+ const tokens = await oidc.authorizationCodeGrant(configuration, callbackUrl, {
3578
+ expectedState: state,
3579
+ pkceCodeVerifier: pendingAuth.pkceVerifier
3580
+ });
3581
+ const claims = tokens.claims();
3582
+ if (claims?.sub) {
3583
+ this.subjects.set(tokens.access_token, claims.sub);
3584
+ }
3585
+ return {
3586
+ accessToken: tokens.access_token,
3587
+ refreshToken: tokens.refresh_token ?? "",
3588
+ expiresIn: tokens.expires_in ?? 3600,
3589
+ tokenType: "bearer"
3590
+ };
3591
+ }
3592
+ async oidcRefresh(params) {
3593
+ const refreshToken = params.refreshToken;
3594
+ const configuration = await this.configurationFor("google");
3595
+ const tokens = await oidc.refreshTokenGrant(configuration, refreshToken);
3596
+ const claims = tokens.claims();
3597
+ if (claims?.sub) {
3598
+ this.subjects.set(tokens.access_token, claims.sub);
3599
+ }
3600
+ return {
3601
+ accessToken: tokens.access_token,
3602
+ expiresIn: tokens.expires_in ?? 3600
3603
+ };
3604
+ }
3605
+ async oidcRevoke(params) {
3606
+ const token = params.token;
3607
+ const configuration = await this.configurationFor("google");
3608
+ await oidc.tokenRevocation(configuration, token);
3609
+ this.subjects.delete(token);
3610
+ return { revoked: true };
3611
+ }
3612
+ async oidcUserinfo(params) {
3613
+ const accessToken = params.accessToken;
3614
+ const configuration = await this.configurationFor("google");
3615
+ const subject = this.subjects.get(accessToken);
3616
+ const info = await oidc.fetchUserInfo(
3617
+ configuration,
3618
+ accessToken,
3619
+ subject ?? oidc.skipSubjectCheck
3620
+ );
3621
+ return {
3622
+ sub: info.sub,
3623
+ email: typeof info.email === "string" ? info.email : "",
3624
+ name: typeof info.name === "string" ? info.name : "",
3625
+ picture: typeof info.picture === "string" ? info.picture : ""
3626
+ };
3627
+ }
3628
+ // ---------------------------------------------------------------------------
3629
+ // Mock backend helpers
2496
3630
  // ---------------------------------------------------------------------------
2497
3631
  /** Generate a random hex token of the given byte length. */
2498
3632
  generateToken(bytes = 32) {
@@ -2513,7 +3647,7 @@ var OAuthIntegration = class extends BaseIntegration {
2513
3647
  };
2514
3648
  }
2515
3649
  // ---------------------------------------------------------------------------
2516
- // Actions
3650
+ // Mock backend actions
2517
3651
  // ---------------------------------------------------------------------------
2518
3652
  async authorize(params) {
2519
3653
  const provider = params.provider;
@@ -2622,19 +3756,333 @@ var OAuthIntegration = class extends BaseIntegration {
2622
3756
  };
2623
3757
  registerIntegration("oauth", OAuthIntegration);
2624
3758
 
2625
- // src/integrations/storage/index.ts
3759
+ // src/contracts.ts
3760
+ var serviceCredentials = {
3761
+ stripe: [
3762
+ { envVar: "STRIPE_SECRET_KEY", required: true, description: "Stripe secret API key" },
3763
+ { envVar: "STRIPE_WEBHOOK_SECRET", required: false, description: "Webhook endpoint signing secret" }
3764
+ ],
3765
+ youtube: [
3766
+ { envVar: "YOUTUBE_API_KEY", required: true, description: "YouTube Data API key" }
3767
+ ],
3768
+ twilio: [
3769
+ { envVar: "TWILIO_ACCOUNT_SID", required: true, description: "Twilio account SID" },
3770
+ { envVar: "TWILIO_AUTH_TOKEN", required: true, description: "Twilio auth token" },
3771
+ { envVar: "TWILIO_PHONE_NUMBER", required: false, description: "Default sender phone number" }
3772
+ ],
3773
+ email: [
3774
+ { envVar: "SENDGRID_API_KEY", required: false, description: "SendGrid API key (use this OR RESEND_API_KEY)" },
3775
+ { envVar: "RESEND_API_KEY", required: false, description: "Resend API key (use this OR SENDGRID_API_KEY)" },
3776
+ { envVar: "FROM_EMAIL", required: false, description: "Default sender email address" }
3777
+ ],
3778
+ webhook: [
3779
+ { envVar: "WEBHOOK_SIGNING_SECRET", required: false, description: "Default HMAC-SHA256 signing secret (per-call secret overrides)" },
3780
+ { envVar: "WEBHOOK_TIMEOUT_MS", required: false, description: "Per-request timeout (ms, default 10000)" }
3781
+ ],
3782
+ push: [
3783
+ { envVar: "VAPID_PUBLIC_KEY", required: true, description: "VAPID public key (web-push generate-vapid-keys)" },
3784
+ { envVar: "VAPID_PRIVATE_KEY", required: true, description: "VAPID private key" },
3785
+ { envVar: "VAPID_SUBJECT", required: true, description: "VAPID subject (mailto: or https: contact URI)" }
3786
+ ],
3787
+ calendar: [
3788
+ { 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" },
3789
+ { envVar: "GOOGLE_CALENDAR_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" },
3790
+ { envVar: "GOOGLE_CALENDAR_ID", required: false, description: "Default calendar id (default: primary)" }
3791
+ ],
3792
+ drive: [
3793
+ { 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" },
3794
+ { envVar: "GOOGLE_DRIVE_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" }
3795
+ ],
3796
+ metaAds: [
3797
+ { envVar: "META_ACCESS_TOKEN", required: true, description: "Meta Graph API access token (Marketing API, ads_read)" },
3798
+ { envVar: "META_AD_ACCOUNT_ID", required: false, description: "Default ad account id (act_\u2026); per-call accountId overrides" }
3799
+ ],
3800
+ accounting: [],
3801
+ banking: [
3802
+ { envVar: "GOCARDLESS_SECRET_ID", required: true, description: "GoCardless Bank Account Data secret id" },
3803
+ { envVar: "GOCARDLESS_SECRET_KEY", required: true, description: "GoCardless Bank Account Data secret key" }
3804
+ ],
3805
+ esign: [
3806
+ { envVar: "DOCUSIGN_BASE_URL", required: true, description: "DocuSign REST base (e.g. https://demo.docusign.net/restapi/v2.1/accounts/<accountId>)" },
3807
+ { envVar: "DOCUSIGN_ACCESS_TOKEN", required: true, description: "DocuSign OAuth access token (JWT grant rotation is the deployment concern)" }
3808
+ ],
3809
+ llm: [
3810
+ { envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key (use this OR OPENAI_API_KEY)" },
3811
+ { envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key (use this OR ANTHROPIC_API_KEY)" }
3812
+ ],
3813
+ "llm-integration": [
3814
+ { envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key" },
3815
+ { envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key" }
3816
+ ],
3817
+ ml: [
3818
+ { envVar: "MASAR_URL", required: true, description: "Base URL of the deployed model-serving orbital" },
3819
+ { envVar: "MASAR_ML_TRAIT", required: true, description: "Kebab-case trait name the serving orbital exposes its /events route under" }
3820
+ ],
3821
+ deepagent: [
3822
+ { envVar: "DEEPAGENT_API_URL", required: true, description: "DeepAgent server URL" },
3823
+ { envVar: "DEEPAGENT_API_KEY", required: false, description: "DeepAgent API key" }
3824
+ ],
3825
+ github: [
3826
+ { envVar: "GITHUB_TOKEN", required: true, description: "GitHub personal access token" },
3827
+ { envVar: "GITHUB_OWNER", required: false, description: "Default repository owner" },
3828
+ { envVar: "GITHUB_REPO", required: false, description: "Default repository name" }
3829
+ ],
3830
+ docker: [
3831
+ { envVar: "DOCKER_HOST", required: false, description: "Docker daemon host URL" }
3832
+ ],
3833
+ storage: [
3834
+ { envVar: "STORAGE_ACCESS_KEY_ID", required: true, description: "S3-compatible access key id" },
3835
+ { envVar: "STORAGE_SECRET_ACCESS_KEY", required: true, description: "S3-compatible secret access key" },
3836
+ { envVar: "STORAGE_BUCKET", required: true, description: "Default bucket name" },
3837
+ { envVar: "STORAGE_REGION", required: false, description: "Region (default us-east-1)" },
3838
+ { envVar: "STORAGE_ENDPOINT", required: false, description: "Custom S3-compatible endpoint (R2/MinIO/\u2026); empty = AWS S3" },
3839
+ { envVar: "STORAGE_PUBLIC_URL_BASE", required: false, description: "Base URL for public-acl object links; empty = endpoint-derived" }
3840
+ ],
3841
+ queue: [
3842
+ { envVar: "QUEUE_URL", required: true, description: "Message queue connection URL" }
3843
+ ],
3844
+ redis: [
3845
+ { envVar: "REDIS_URL", required: true, description: "Redis connection URL" }
3846
+ ],
3847
+ oauth: [
3848
+ { envVar: "OAUTH_CLIENT_ID", required: true, description: "OIDC client ID" },
3849
+ { envVar: "OAUTH_CLIENT_SECRET", required: true, description: "OIDC client secret" },
3850
+ { envVar: "OAUTH_REDIRECT_URI", required: false, description: "OIDC redirect URI (default per-call param)" },
3851
+ { envVar: "OIDC_ISSUER_URL", required: false, description: "OIDC issuer URL (default https://accounts.google.com)" }
3852
+ ],
3853
+ credentials: [
3854
+ { 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" }
3855
+ ],
3856
+ otel: [
3857
+ { envVar: "OTEL_EXPORTER_OTLP_ENDPOINT", required: true, description: "OpenTelemetry collector endpoint" },
3858
+ { envVar: "OTEL_SERVICE_NAME", required: false, description: "Service name for traces" }
3859
+ ],
3860
+ cli: [],
3861
+ // No fixed env vars — connection strings are resolved per-query from the
3862
+ // caller-supplied connectionRef, so credentials cannot be declared statically.
3863
+ database: [],
3864
+ wikimedia: [
3865
+ { envVar: "WIKIMEDIA_USER_AGENT", required: false, description: "Descriptive User-Agent for the Wikipedia API" },
3866
+ { envVar: "WIKIMEDIA_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
3867
+ ],
3868
+ iconify: [
3869
+ { envVar: "ICONIFY_USER_AGENT", required: false, description: "User-Agent for the Iconify API" },
3870
+ { envVar: "ICONIFY_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
3871
+ ],
3872
+ arxiv: [
3873
+ { envVar: "ARXIV_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
3874
+ ]
3875
+ };
3876
+ var serviceProbes = {
3877
+ calendar: { action: "listEvents", params: { maxResults: 1 } },
3878
+ drive: { action: "listFiles", params: { maxResults: 1 } },
3879
+ metaAds: { action: "listCampaigns", params: {} }
3880
+ };
3881
+
3882
+ // src/integrations/credentials/index.ts
3883
+ var ENV_VAR_NAME = /^[A-Z][A-Z0-9_]*$/;
3884
+ function isProbeService(service) {
3885
+ return service in serviceProbes;
3886
+ }
3887
+ var CredentialsIntegration = class extends BaseIntegration {
3888
+ constructor(config) {
3889
+ super(config);
3890
+ this.logger.info("Credentials integration initialized (tenant credential store surface)");
3891
+ }
3892
+ async execute(action, params) {
3893
+ const validation = this.validateParams(action, params);
3894
+ if (!validation.valid) {
3895
+ return {
3896
+ success: false,
3897
+ error: {
3898
+ name: "IntegrationError",
3899
+ message: "Validation failed",
3900
+ code: "VALIDATION_ERROR",
3901
+ details: validation.errors
3902
+ },
3903
+ metadata: this.createMetadata(action, 0)
3904
+ };
3905
+ }
3906
+ const startTime = Date.now();
3907
+ try {
3908
+ let data;
3909
+ switch (action) {
3910
+ case "list":
3911
+ data = this.list(typeof params.service === "string" ? params.service : void 0);
3912
+ break;
3913
+ case "set":
3914
+ data = await this.set(params.service, params.envVar, params.value);
3915
+ break;
3916
+ case "remove":
3917
+ data = await this.remove(params.service, params.envVar);
3918
+ break;
3919
+ case "test":
3920
+ data = await this.test(params.service);
3921
+ break;
3922
+ default:
3923
+ throw new Error(`Unknown action: ${action}`);
3924
+ }
3925
+ return {
3926
+ success: true,
3927
+ data,
3928
+ metadata: this.createMetadata(action, Date.now() - startTime)
3929
+ };
3930
+ } catch (error) {
3931
+ return this.handleError(action, error);
3932
+ }
3933
+ }
3934
+ declaredFor(service) {
3935
+ return serviceCredentials[service] ?? [];
3936
+ }
3937
+ assertSettable(service, envVar) {
3938
+ if (service === "database") {
3939
+ if (!ENV_VAR_NAME.test(envVar)) {
3940
+ throw new Error(`"${envVar}" is not a well-formed connection reference (expected an env-var name)`);
3941
+ }
3942
+ return;
3943
+ }
3944
+ const declared = this.declaredFor(service);
3945
+ if (declared.length === 0) {
3946
+ throw new Error(`Service "${service}" declares no credentials`);
3947
+ }
3948
+ if (!declared.some((c) => c.envVar === envVar)) {
3949
+ const valid = declared.map((c) => c.envVar).join(", ");
3950
+ throw new Error(`"${envVar}" is not a declared credential of "${service}" (declared: ${valid})`);
3951
+ }
3952
+ }
3953
+ list(serviceFilter) {
3954
+ const store = getInstalledCredentialStore();
3955
+ const stored = new Map((store?.entries() ?? []).map((e) => [`${e.service}\0${e.envVar}`, e]));
3956
+ const entries = [];
3957
+ for (const [service, declared] of Object.entries(serviceCredentials)) {
3958
+ if (serviceFilter && service !== serviceFilter) continue;
3959
+ for (const { envVar, required, description } of declared) {
3960
+ const fromStore = stored.get(`${service}\0${envVar}`);
3961
+ stored.delete(`${service}\0${envVar}`);
3962
+ const fromEnv = process.env[envVar];
3963
+ const source = fromStore ? "store" : fromEnv ? "env" : "none";
3964
+ entries.push({
3965
+ service,
3966
+ envVar,
3967
+ required,
3968
+ description,
3969
+ configured: source !== "none",
3970
+ source,
3971
+ last4: fromStore ? fromStore.last4 : fromEnv ? fromEnv.slice(-4) : ""
3972
+ });
3973
+ }
3974
+ }
3975
+ for (const e of stored.values()) {
3976
+ if (serviceFilter && e.service !== serviceFilter) continue;
3977
+ entries.push({
3978
+ service: e.service,
3979
+ envVar: e.envVar,
3980
+ required: false,
3981
+ description: "Stored connection reference",
3982
+ configured: true,
3983
+ source: "store",
3984
+ last4: e.last4
3985
+ });
3986
+ }
3987
+ return { enabled: store?.enabled ?? false, entries };
3988
+ }
3989
+ async set(service, envVar, value) {
3990
+ const store = getInstalledCredentialStore();
3991
+ if (!store) {
3992
+ throw new Error("No credential store is installed on this host \u2014 set credentials via the environment");
3993
+ }
3994
+ this.assertSettable(service, envVar);
3995
+ const entry = await store.set(service, envVar, value);
3996
+ return { saved: true, service, envVar, last4: entry.last4 };
3997
+ }
3998
+ async remove(service, envVar) {
3999
+ const store = getInstalledCredentialStore();
4000
+ if (!store) {
4001
+ throw new Error("No credential store is installed on this host");
4002
+ }
4003
+ this.assertSettable(service, envVar);
4004
+ return { removed: await store.remove(envVar) };
4005
+ }
4006
+ async test(service) {
4007
+ const declared = this.declaredFor(service);
4008
+ const missing = declared.filter((c) => c.required && !resolveCredentialRef(c.envVar)).map((c) => c.envVar);
4009
+ const configured = missing.length === 0;
4010
+ if (!configured) {
4011
+ return { service, configured, missing, probed: false, ok: false, message: `Missing required credentials: ${missing.join(", ")}` };
4012
+ }
4013
+ const factory = getActiveFactory();
4014
+ const probe = isProbeService(service) ? serviceProbes[service] : void 0;
4015
+ if (!factory || !probe) {
4016
+ return { service, configured, missing, probed: false, ok: true, message: "Credentials present (no live probe declared for this service)" };
4017
+ }
4018
+ if (!factory.isConfigured(service)) {
4019
+ 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" };
4020
+ }
4021
+ const result = await factory.execute(service, probe.action, probe.params);
4022
+ if (!result.success) {
4023
+ return { service, configured, missing, probed: true, ok: false, message: result.error?.message ?? `Probe ${probe.action} failed` };
4024
+ }
4025
+ const echoed = result.data !== null && typeof result.data === "object" && "_mock" in result.data;
4026
+ if (echoed) {
4027
+ return { service, configured, missing, probed: false, ok: false, message: "Probe was mock-echoed \u2014 the service is not actually configured" };
4028
+ }
4029
+ return { service, configured, missing, probed: true, ok: true, message: `Probe ${probe.action} succeeded` };
4030
+ }
4031
+ };
4032
+ registerIntegration("credentials", CredentialsIntegration);
4033
+ function isParamRecord2(value) {
4034
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
4035
+ }
4036
+ function toFilePayload(value) {
4037
+ if (!isParamRecord2(value)) return null;
4038
+ const { name, size, type, content } = value;
4039
+ if (typeof name !== "string") return null;
4040
+ return {
4041
+ name,
4042
+ size: typeof size === "number" ? size : 0,
4043
+ type: typeof type === "string" ? type : "application/octet-stream",
4044
+ content: typeof content === "string" ? content : void 0
4045
+ };
4046
+ }
4047
+ function decodeContent2(content) {
4048
+ const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
4049
+ if (dataUrlMatch) {
4050
+ const [, mime, isB64, body] = dataUrlMatch;
4051
+ const bytes = isB64 ? Buffer.from(body, "base64") : Buffer.from(decodeURIComponent(body), "utf8");
4052
+ return { bytes, contentType: mime || null };
4053
+ }
4054
+ return { bytes: Buffer.from(content, "utf8"), contentType: null };
4055
+ }
2626
4056
  var StorageIntegration = class extends BaseIntegration {
2627
4057
  constructor(config) {
2628
4058
  super(config);
2629
4059
  this.objects = /* @__PURE__ */ new Map();
2630
- const storageUrl = config.env.STORAGE_URL;
2631
- if (storageUrl) {
2632
- this.logger.warn(
2633
- "STORAGE_URL is configured but real storage client is not yet implemented. Falling back to in-memory store.",
2634
- { storageUrl }
2635
- );
4060
+ this.s3 = null;
4061
+ this.defaultBucket = config.env.STORAGE_BUCKET || "";
4062
+ this.publicUrlBase = config.env.STORAGE_PUBLIC_URL_BASE || "";
4063
+ const accessKeyId = config.env.STORAGE_ACCESS_KEY_ID || "";
4064
+ const secretAccessKey = config.env.STORAGE_SECRET_ACCESS_KEY || "";
4065
+ if (accessKeyId && secretAccessKey) {
4066
+ const endpoint = config.env.STORAGE_ENDPOINT || void 0;
4067
+ this.s3 = new S3Client({
4068
+ region: config.env.STORAGE_REGION || "us-east-1",
4069
+ endpoint,
4070
+ // Path-style is what MinIO/R2-style endpoints expect.
4071
+ forcePathStyle: Boolean(endpoint),
4072
+ credentials: { accessKeyId, secretAccessKey }
4073
+ });
4074
+ this.logger.info("Storage integration initialized (S3 backend)", {
4075
+ endpoint: endpoint ?? "aws",
4076
+ bucket: this.defaultBucket
4077
+ });
4078
+ } else {
4079
+ if (process.env.NODE_ENV === "production") {
4080
+ throw new Error(
4081
+ "Storage credentials missing in production (STORAGE_ACCESS_KEY_ID / STORAGE_SECRET_ACCESS_KEY) \u2014 refusing the in-memory fallback. See SECRETS.md."
4082
+ );
4083
+ }
4084
+ this.logger.warn("Storage integration initialized (in-memory backend \u2014 dev only, nothing persists)");
2636
4085
  }
2637
- this.logger.info("Storage integration initialized (in-memory backend)");
2638
4086
  }
2639
4087
  async execute(action, params) {
2640
4088
  const validation = this.validateParams(action, params);
@@ -2684,107 +4132,182 @@ var StorageIntegration = class extends BaseIntegration {
2684
4132
  // ---------------------------------------------------------------------------
2685
4133
  // Helpers
2686
4134
  // ---------------------------------------------------------------------------
2687
- /** Build a composite key from bucket and object key. */
4135
+ bucketOf(params) {
4136
+ return params.bucket || this.defaultBucket;
4137
+ }
2688
4138
  compositeKey(bucket, key) {
2689
4139
  return `${bucket}/${key}`;
2690
4140
  }
2691
- /** Generate a deterministic etag from content. */
2692
- generateEtag(content) {
2693
- const raw = typeof content === "string" ? content : JSON.stringify(content);
2694
- let hash = 0;
2695
- for (let i = 0; i < raw.length; i++) {
2696
- const ch = raw.charCodeAt(i);
2697
- hash = (hash << 5) - hash + ch | 0;
4141
+ generateEtag(bytes) {
4142
+ return `"${createHash("md5").update(bytes).digest("hex")}"`;
4143
+ }
4144
+ /** Resolve the upload inputs from either admitted shape. */
4145
+ resolveUpload(params) {
4146
+ const file = toFilePayload(params.file);
4147
+ if (file) {
4148
+ const maxSize = typeof params.maxSize === "number" ? params.maxSize : 0;
4149
+ if (maxSize > 0 && file.size > maxSize) {
4150
+ throw new Error(`Upload rejected: ${file.name} is ${file.size} bytes (max ${maxSize})`);
4151
+ }
4152
+ if (!file.content) {
4153
+ throw new Error(
4154
+ `Upload rejected: file payload for '${file.name}' carries no content \u2014 the uploader must include the base64 data URL`
4155
+ );
4156
+ }
4157
+ const { bytes: bytes2, contentType: contentType2 } = decodeContent2(file.content);
4158
+ const safeName = file.name.replace(/[^A-Za-z0-9._-]/g, "_");
4159
+ return {
4160
+ key: `${Date.now()}-${safeName}`,
4161
+ bytes: bytes2,
4162
+ contentType: contentType2 ?? file.type,
4163
+ acl: params.acl === "public" ? "public-read" : void 0
4164
+ };
4165
+ }
4166
+ const key = params.key;
4167
+ const content = params.content;
4168
+ if (!key || content === void 0 || content === null) {
4169
+ throw new Error("upload requires either `file` (with content) or the `key` + `content` pair");
2698
4170
  }
2699
- return `"${Math.abs(hash).toString(16).padStart(8, "0")}"`;
4171
+ const raw = typeof content === "string" ? content : JSON.stringify(content);
4172
+ const { bytes, contentType } = decodeContent2(raw);
4173
+ return {
4174
+ key,
4175
+ bytes,
4176
+ contentType: params.contentType || contentType || "application/octet-stream",
4177
+ acl: params.acl === "public" ? "public-read" : void 0
4178
+ };
2700
4179
  }
2701
- /** Compute the byte size of content. */
2702
- computeSize(content) {
2703
- if (typeof content === "string") {
2704
- return new TextEncoder().encode(content).byteLength;
4180
+ publicUrl(bucket, key) {
4181
+ if (this.publicUrlBase) {
4182
+ return `${this.publicUrlBase.replace(/\/$/, "")}/${key}`;
2705
4183
  }
2706
- return JSON.stringify(content).length;
4184
+ const endpoint = this.config.env.STORAGE_ENDPOINT;
4185
+ if (endpoint) {
4186
+ return `${endpoint.replace(/\/$/, "")}/${bucket}/${key}`;
4187
+ }
4188
+ const region = this.config.env.STORAGE_REGION || "us-east-1";
4189
+ return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
2707
4190
  }
2708
4191
  // ---------------------------------------------------------------------------
2709
4192
  // Actions
2710
4193
  // ---------------------------------------------------------------------------
2711
4194
  async upload(params) {
2712
- const bucket = params.bucket;
2713
- const key = params.key;
2714
- const content = params.content;
2715
- const contentType = params.contentType ?? "application/octet-stream";
2716
- const metadata = params.metadata ?? {};
2717
- this.logger.debug("Storage UPLOAD", { bucket, key, contentType });
2718
- const size = this.computeSize(content);
2719
- const etag = this.generateEtag(content);
2720
- const obj = {
2721
- content,
2722
- contentType,
2723
- size,
2724
- metadata,
2725
- lastModified: Date.now(),
2726
- etag
2727
- };
2728
- this.objects.set(this.compositeKey(bucket, key), obj);
2729
- return { key, bucket, size, etag };
4195
+ const bucket = this.bucketOf(params);
4196
+ const { key, bytes, contentType, acl } = this.resolveUpload(params);
4197
+ const etag = this.generateEtag(bytes);
4198
+ this.logger.debug("Storage UPLOAD", { bucket, key, contentType, size: bytes.length });
4199
+ if (this.s3) {
4200
+ await this.s3.send(
4201
+ new PutObjectCommand({
4202
+ Bucket: bucket,
4203
+ Key: key,
4204
+ Body: bytes,
4205
+ ContentType: contentType,
4206
+ ACL: acl
4207
+ })
4208
+ );
4209
+ } else {
4210
+ this.objects.set(this.compositeKey(bucket, key), {
4211
+ content: bytes.toString("base64"),
4212
+ contentType,
4213
+ size: bytes.length,
4214
+ metadata: params.metadata ?? {},
4215
+ lastModified: Date.now(),
4216
+ etag
4217
+ });
4218
+ }
4219
+ const url = acl === "public-read" ? this.publicUrl(bucket, key) : (await this.signUrl(bucket, key, "get", 3600)).url;
4220
+ return { key, bucket, size: bytes.length, etag, id: key, url };
2730
4221
  }
2731
4222
  async download(params) {
2732
- const bucket = params.bucket;
4223
+ const bucket = this.bucketOf(params);
2733
4224
  const key = params.key;
2734
4225
  this.logger.debug("Storage DOWNLOAD", { bucket, key });
4226
+ if (this.s3) {
4227
+ const response = await this.s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
4228
+ const bytes = Buffer.from(await response.Body.transformToByteArray());
4229
+ return {
4230
+ content: bytes.toString("base64"),
4231
+ contentType: response.ContentType ?? "application/octet-stream",
4232
+ size: bytes.length,
4233
+ metadata: {}
4234
+ };
4235
+ }
2735
4236
  const obj = this.objects.get(this.compositeKey(bucket, key));
2736
4237
  if (!obj) {
2737
4238
  throw new Error(`Object not found: ${bucket}/${key}`);
2738
4239
  }
2739
4240
  return {
2740
- content: obj.content,
4241
+ content: String(obj.content),
2741
4242
  contentType: obj.contentType,
2742
4243
  size: obj.size,
2743
4244
  metadata: obj.metadata
2744
4245
  };
2745
4246
  }
2746
4247
  async list(params) {
2747
- const bucket = params.bucket;
4248
+ const bucket = this.bucketOf(params);
2748
4249
  const prefix = params.prefix ?? "";
2749
4250
  const maxKeys = params.maxKeys ?? 1e3;
2750
4251
  this.logger.debug("Storage LIST", { bucket, prefix, maxKeys });
4252
+ if (this.s3) {
4253
+ const response = await this.s3.send(
4254
+ new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix || void 0, MaxKeys: maxKeys })
4255
+ );
4256
+ return {
4257
+ keys: (response.Contents ?? []).map((entry) => ({
4258
+ key: entry.Key ?? "",
4259
+ size: entry.Size ?? 0,
4260
+ lastModified: entry.LastModified?.getTime() ?? 0
4261
+ })),
4262
+ // Continuation tokens are unrepresentable in the result type (ledger
4263
+ // I-11) — surface the clamp honestly.
4264
+ truncated: Boolean(response.IsTruncated)
4265
+ };
4266
+ }
2751
4267
  const bucketPrefix = `${bucket}/`;
2752
4268
  const fullPrefix = `${bucket}/${prefix}`;
2753
4269
  const results = [];
2754
4270
  for (const [compositeKey, obj] of this.objects) {
2755
4271
  if (!compositeKey.startsWith(fullPrefix)) continue;
2756
- const objectKey = compositeKey.slice(bucketPrefix.length);
2757
4272
  results.push({
2758
- key: objectKey,
4273
+ key: compositeKey.slice(bucketPrefix.length),
2759
4274
  size: obj.size,
2760
4275
  lastModified: obj.lastModified
2761
4276
  });
2762
4277
  }
2763
4278
  results.sort((a, b) => a.key.localeCompare(b.key));
2764
- const truncated = results.length > maxKeys;
2765
- return {
2766
- keys: results.slice(0, maxKeys),
2767
- truncated
2768
- };
4279
+ return { keys: results.slice(0, maxKeys), truncated: results.length > maxKeys };
2769
4280
  }
2770
4281
  async deleteObject(params) {
2771
- const bucket = params.bucket;
4282
+ const bucket = this.bucketOf(params);
2772
4283
  const key = params.key;
2773
4284
  this.logger.debug("Storage DELETE", { bucket, key });
4285
+ if (this.s3) {
4286
+ await this.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
4287
+ return { deleted: true };
4288
+ }
2774
4289
  const existed = this.objects.has(this.compositeKey(bucket, key));
2775
4290
  this.objects.delete(this.compositeKey(bucket, key));
2776
4291
  return { deleted: existed };
2777
4292
  }
4293
+ async signUrl(bucket, key, operation, expiresIn) {
4294
+ const expiresAt = Date.now() + expiresIn * 1e3;
4295
+ if (this.s3) {
4296
+ const command = operation === "put" ? new PutObjectCommand({ Bucket: bucket, Key: key }) : new GetObjectCommand({ Bucket: bucket, Key: key });
4297
+ const url2 = await getSignedUrl(this.s3, command, { expiresIn });
4298
+ return { url: url2, expiresAt };
4299
+ }
4300
+ const token = createHash("sha256").update(`${bucket}/${key}/${expiresAt}`).digest("hex").slice(0, 16);
4301
+ 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}`;
4302
+ return { url, expiresAt };
4303
+ }
2778
4304
  async getSignedUrl(params) {
2779
- const bucket = params.bucket;
4305
+ const bucket = this.bucketOf(params);
2780
4306
  const key = params.key;
2781
4307
  const expiresIn = params.expiresIn ?? 3600;
2782
4308
  const operation = params.operation ?? "get";
2783
4309
  this.logger.debug("Storage GET_SIGNED_URL", { bucket, key, expiresIn, operation });
2784
- const expiresAt = Date.now() + expiresIn * 1e3;
2785
- const token = Math.random().toString(36).slice(2, 18);
2786
- 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}`;
2787
- return { url, expiresAt };
4310
+ return this.signUrl(bucket, key, operation, expiresIn);
2788
4311
  }
2789
4312
  };
2790
4313
  registerIntegration("storage", StorageIntegration);
@@ -3291,10 +4814,10 @@ var DatabaseIntegration = class extends BaseIntegration {
3291
4814
  }
3292
4815
  /** Resolve (and cache) the driver for a connection reference. */
3293
4816
  driverFor(connectionRef) {
3294
- const connectionString = process.env[connectionRef];
4817
+ const connectionString = resolveCredentialRef(connectionRef);
3295
4818
  if (!connectionString) {
3296
4819
  throw new IntegrationError(
3297
- `Connection reference "${connectionRef}" is not set in the environment`,
4820
+ `Connection reference "${connectionRef}" is not set in the credential store or environment`,
3298
4821
  "AUTH_ERROR"
3299
4822
  );
3300
4823
  }
@@ -3594,8 +5117,8 @@ var MockIntegration = class extends BaseIntegration {
3594
5117
 
3595
5118
  // src/runtime/effectHandler.ts
3596
5119
  function createCallServiceHandler(factory) {
3597
- const handler = async (service, action, params) => {
3598
- const result = await factory.execute(service, action, params || {});
5120
+ const handler = async (service, action, params, context) => {
5121
+ const result = await factory.execute(service, action, params || {}, context);
3599
5122
  if (!result.success) {
3600
5123
  throw result.error;
3601
5124
  }
@@ -3632,8 +5155,33 @@ function generateMockFromShape(shape) {
3632
5155
  }
3633
5156
  var RuntimeIntegrationManager = class {
3634
5157
  constructor() {
5158
+ this.credentialStore = null;
5159
+ this.storeEnvBase = null;
3635
5160
  this.factory = new IntegrationFactory();
3636
5161
  this.installNotConfiguredFallback();
5162
+ installActiveFactory(this.factory);
5163
+ }
5164
+ /**
5165
+ * W4: configure with the tenant credential store layered over the env —
5166
+ * store → env → unconfigured. Installs the store as the process-wide
5167
+ * `resolveCredentialRef` source, warms it, and re-configures whenever a
5168
+ * credential changes (dropping cached instances so new keys go live
5169
+ * without a restart). Mock mode short-circuits inside `configureFromEnv`
5170
+ * exactly as before, so verify harnesses are unaffected.
5171
+ */
5172
+ async configureFromStore(store, envOverride) {
5173
+ this.credentialStore = store;
5174
+ this.storeEnvBase = envOverride ?? process.env;
5175
+ installCredentialStore(store);
5176
+ await store.warm();
5177
+ store.subscribe(() => this.refreshFromStore());
5178
+ this.refreshFromStore();
5179
+ }
5180
+ /** Re-derive configs from base env + warmed store values; drop stale instances. */
5181
+ refreshFromStore() {
5182
+ if (!this.credentialStore || !this.storeEnvBase) return;
5183
+ this.factory.invalidate();
5184
+ this.configureFromEnv({ ...this.storeEnvBase, ...this.credentialStore.snapshotEnv() });
3637
5185
  }
3638
5186
  /**
3639
5187
  * Wrap `factory.execute` so an unknown/unconfigured service echoes its
@@ -3644,15 +5192,18 @@ var RuntimeIntegrationManager = class {
3644
5192
  * events fire — only the two "not set up" errors (`Unknown integration`,
3645
5193
  * `Integration not configured`) are caught. Subsumes the broader wrapper
3646
5194
  * that previously lived inside `configureMockMode`.
5195
+ *
5196
+ * In production the fallback is OFF: a missing key must surface as the
5197
+ * circuit's failure event and a degraded health check, never a fake success.
3647
5198
  */
3648
5199
  installNotConfiguredFallback() {
3649
5200
  const originalExecute = this.factory.execute.bind(this.factory);
3650
- this.factory.execute = async (integration, action, params) => {
5201
+ this.factory.execute = async (integration, action, params, context) => {
3651
5202
  try {
3652
- return await originalExecute(integration, action, params);
5203
+ return await originalExecute(integration, action, params, context);
3653
5204
  } catch (err) {
3654
5205
  const msg = err instanceof Error ? err.message : String(err);
3655
- if (/Unknown integration|not configured/i.test(msg)) {
5206
+ if (process.env.NODE_ENV !== "production" && /Unknown integration|not configured/i.test(msg)) {
3656
5207
  return {
3657
5208
  success: true,
3658
5209
  data: { ...params, _mock: true, _service: integration, _action: action },
@@ -3666,10 +5217,18 @@ var RuntimeIntegrationManager = class {
3666
5217
  /**
3667
5218
  * Configure from environment variables.
3668
5219
  * In mock mode (USE_MOCK_DATA=true), all services return realistic mock data.
5220
+ *
5221
+ * Pass an explicit env map to configure hermetically (verify harnesses,
5222
+ * per-tenant resolution); defaults to `process.env`.
3669
5223
  */
3670
- configureFromEnv() {
3671
- const env = process.env;
3672
- if (env.USE_MOCK_DATA === "true") {
5224
+ configureFromEnv(envOverride) {
5225
+ const env = envOverride ?? process.env;
5226
+ if (env.ALMADAR_INTEGRATIONS_MODE === "mock" || env.USE_MOCK_DATA === "true") {
5227
+ if (env.NODE_ENV === "production") {
5228
+ throw new Error(
5229
+ "Mocked integrations are not permitted when NODE_ENV=production \u2014 unset ALMADAR_INTEGRATIONS_MODE/USE_MOCK_DATA."
5230
+ );
5231
+ }
3673
5232
  this.configureMockMode();
3674
5233
  return;
3675
5234
  }
@@ -3713,6 +5272,85 @@ var RuntimeIntegrationManager = class {
3713
5272
  }
3714
5273
  });
3715
5274
  }
5275
+ if (env.VAPID_PUBLIC_KEY && env.VAPID_PRIVATE_KEY && env.VAPID_SUBJECT) {
5276
+ this.factory.configure("push", {
5277
+ env: {
5278
+ VAPID_PUBLIC_KEY: env.VAPID_PUBLIC_KEY,
5279
+ VAPID_PRIVATE_KEY: env.VAPID_PRIVATE_KEY,
5280
+ VAPID_SUBJECT: env.VAPID_SUBJECT
5281
+ }
5282
+ });
5283
+ }
5284
+ if (env.GOOGLE_CALENDAR_SA_KEY) {
5285
+ this.factory.configure("calendar", {
5286
+ env: {
5287
+ GOOGLE_CALENDAR_SA_KEY: env.GOOGLE_CALENDAR_SA_KEY,
5288
+ GOOGLE_CALENDAR_SUBJECT: env.GOOGLE_CALENDAR_SUBJECT || "",
5289
+ GOOGLE_CALENDAR_ID: env.GOOGLE_CALENDAR_ID || ""
5290
+ }
5291
+ });
5292
+ }
5293
+ if (env.GOOGLE_DRIVE_SA_KEY) {
5294
+ this.factory.configure("drive", {
5295
+ env: {
5296
+ GOOGLE_DRIVE_SA_KEY: env.GOOGLE_DRIVE_SA_KEY,
5297
+ GOOGLE_DRIVE_SUBJECT: env.GOOGLE_DRIVE_SUBJECT || ""
5298
+ }
5299
+ });
5300
+ }
5301
+ if (env.META_ACCESS_TOKEN) {
5302
+ this.factory.configure("metaAds", {
5303
+ env: {
5304
+ META_ACCESS_TOKEN: env.META_ACCESS_TOKEN,
5305
+ META_AD_ACCOUNT_ID: env.META_AD_ACCOUNT_ID || ""
5306
+ }
5307
+ });
5308
+ }
5309
+ this.factory.configure("accounting", { env: {} });
5310
+ if (env.GOCARDLESS_SECRET_ID && env.GOCARDLESS_SECRET_KEY) {
5311
+ this.factory.configure("banking", {
5312
+ env: {
5313
+ GOCARDLESS_SECRET_ID: env.GOCARDLESS_SECRET_ID,
5314
+ GOCARDLESS_SECRET_KEY: env.GOCARDLESS_SECRET_KEY
5315
+ }
5316
+ });
5317
+ }
5318
+ if (env.DOCUSIGN_BASE_URL && env.DOCUSIGN_ACCESS_TOKEN) {
5319
+ this.factory.configure("esign", {
5320
+ env: {
5321
+ DOCUSIGN_BASE_URL: env.DOCUSIGN_BASE_URL,
5322
+ DOCUSIGN_ACCESS_TOKEN: env.DOCUSIGN_ACCESS_TOKEN
5323
+ }
5324
+ });
5325
+ }
5326
+ if (env.OAUTH_CLIENT_ID && env.OAUTH_CLIENT_SECRET || env.OAUTH_MODE === "mock") {
5327
+ this.factory.configure("oauth", {
5328
+ env: {
5329
+ OAUTH_CLIENT_ID: env.OAUTH_CLIENT_ID || "",
5330
+ OAUTH_CLIENT_SECRET: env.OAUTH_CLIENT_SECRET || "",
5331
+ OAUTH_REDIRECT_URI: env.OAUTH_REDIRECT_URI || "",
5332
+ OIDC_ISSUER_URL: env.OIDC_ISSUER_URL || "",
5333
+ OAUTH_MODE: env.OAUTH_MODE || ""
5334
+ }
5335
+ });
5336
+ }
5337
+ this.factory.configure("credentials", { env: {} });
5338
+ this.factory.configure("storage", {
5339
+ env: {
5340
+ STORAGE_ACCESS_KEY_ID: env.STORAGE_ACCESS_KEY_ID || "",
5341
+ STORAGE_SECRET_ACCESS_KEY: env.STORAGE_SECRET_ACCESS_KEY || "",
5342
+ STORAGE_BUCKET: env.STORAGE_BUCKET || "",
5343
+ STORAGE_REGION: env.STORAGE_REGION || "",
5344
+ STORAGE_ENDPOINT: env.STORAGE_ENDPOINT || "",
5345
+ STORAGE_PUBLIC_URL_BASE: env.STORAGE_PUBLIC_URL_BASE || ""
5346
+ }
5347
+ });
5348
+ this.factory.configure("webhook", {
5349
+ env: {
5350
+ WEBHOOK_SIGNING_SECRET: env.WEBHOOK_SIGNING_SECRET || "",
5351
+ WEBHOOK_TIMEOUT_MS: env.WEBHOOK_TIMEOUT_MS || ""
5352
+ }
5353
+ });
3716
5354
  const llmEnv = {};
3717
5355
  for (const k of ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "KIMI_API_KEY", "OPEN_ROUTER_API_KEY"]) {
3718
5356
  if (env[k]) llmEnv[k] = env[k];