@almadar/integrations 2.24.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,17 +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';
8
- import { createHmac } from 'crypto';
9
+ import webpush from 'web-push';
10
+ import { Readable } from 'stream';
9
11
  import { getAvailableProvider, LLMClient, EmbeddingClient } from '@almadar/llm';
10
12
  import { z } from 'zod';
11
13
  import { execSync, spawn } from 'child_process';
12
14
  import { mkdtempSync, writeFileSync, rmSync, promises } from 'fs';
13
15
  import { join } from 'path';
14
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';
15
20
  import { Pool } from 'pg';
16
21
  import { createRequire } from 'module';
17
22
 
@@ -217,56 +222,80 @@ function getIntegration(name) {
217
222
  }
218
223
 
219
224
  // src/factory.ts
225
+ function instanceKey(name, principal) {
226
+ return principal ? `${name}\0${principal}` : name;
227
+ }
220
228
  var IntegrationFactory = class {
221
229
  constructor() {
222
230
  this.instances = /* @__PURE__ */ new Map();
223
231
  this.configs = /* @__PURE__ */ new Map();
224
232
  }
225
233
  /**
226
- * 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.
227
237
  */
228
- configure(name, config) {
229
- this.configs.set(name, { name, ...config });
238
+ configure(name, config, principal) {
239
+ this.configs.set(instanceKey(name, principal), { name, ...config });
230
240
  }
231
241
  /**
232
- * 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.
233
244
  */
234
- get(name) {
235
- if (this.instances.has(name)) {
236
- 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;
237
250
  }
238
251
  const Constructor = getIntegration(name);
239
252
  if (!Constructor) {
240
253
  throw new Error(`Unknown integration: ${name}. Make sure it's imported.`);
241
254
  }
242
- const config = this.configs.get(name);
255
+ const config = this.configs.get(key) ?? this.configs.get(name);
243
256
  if (!config) {
244
257
  throw new Error(
245
258
  `Integration not configured: ${name}. Call configure() first.`
246
259
  );
247
260
  }
248
261
  const instance = new Constructor(config);
249
- this.instances.set(name, instance);
262
+ this.instances.set(key, instance);
250
263
  return instance;
251
264
  }
252
265
  /**
253
266
  * Execute an action on an integration
254
267
  */
255
- async execute(integration, action, params) {
256
- const instance = this.get(integration);
268
+ async execute(integration, action, params, context) {
269
+ const instance = this.get(integration, context?.principal);
257
270
  return await instance.execute(action, params);
258
271
  }
259
272
  /**
260
273
  * Check if integration is configured
261
274
  */
262
- isConfigured(name) {
263
- return this.configs.has(name);
275
+ isConfigured(name, principal) {
276
+ return this.configs.has(instanceKey(name, principal)) || this.configs.has(name);
264
277
  }
265
278
  /**
266
279
  * Register an integration instance directly (used by mock infrastructure)
267
280
  */
268
- registerInstance(name, instance) {
269
- 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
+ }
270
299
  }
271
300
  /**
272
301
  * Clear all instances (useful for testing)
@@ -283,6 +312,25 @@ var IntegrationFactory = class {
283
312
  }
284
313
  };
285
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
+
286
334
  // src/integrations/stripe/index.ts
287
335
  var STRIPE_API_VERSION = "2025-02-24.acacia";
288
336
  function isoFromUnix(seconds) {
@@ -851,18 +899,866 @@ var EmailIntegration = class extends BaseIntegration {
851
899
  html: body
852
900
  });
853
901
  return {
854
- id: response.data?.id,
855
- status: "sent"
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
+ }))
856
1749
  };
857
1750
  }
858
1751
  };
859
- registerIntegration("email", EmailIntegration);
860
- var WebhookIntegration = class extends BaseIntegration {
1752
+ registerIntegration("banking", BankingIntegration);
1753
+
1754
+ // src/integrations/esign/index.ts
1755
+ var EsignIntegration = class extends BaseIntegration {
861
1756
  constructor(config) {
862
1757
  super(config);
863
- this.signingSecret = config.env.WEBHOOK_SIGNING_SECRET || "";
864
- this.timeoutMs = Number(config.env.WEBHOOK_TIMEOUT_MS) || 1e4;
865
- this.logger.info("Webhook integration initialized");
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)");
866
1762
  }
867
1763
  async execute(action, params) {
868
1764
  const validation = this.validateParams(action, params);
@@ -879,12 +1775,17 @@ var WebhookIntegration = class extends BaseIntegration {
879
1775
  };
880
1776
  }
881
1777
  const startTime = Date.now();
882
- let retries = 0;
883
1778
  try {
884
1779
  let data;
885
1780
  switch (action) {
886
- case "send":
887
- data = await this.executeWithRetry(() => this.send(params));
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));
888
1789
  break;
889
1790
  default:
890
1791
  throw new Error(`Unknown action: ${action}`);
@@ -892,46 +1793,81 @@ var WebhookIntegration = class extends BaseIntegration {
892
1793
  return {
893
1794
  success: true,
894
1795
  data,
895
- metadata: this.createMetadata(action, Date.now() - startTime, retries)
1796
+ metadata: this.createMetadata(action, Date.now() - startTime)
896
1797
  };
897
1798
  } catch (error) {
898
1799
  return this.handleError(action, error);
899
1800
  }
900
1801
  }
901
- async send(params) {
902
- const { url, event, payload, secret } = params;
903
- const body = JSON.stringify({
904
- event,
905
- payload: payload ?? {},
906
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
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)
907
1813
  });
908
- const headers = {
909
- "Content-Type": "application/json",
910
- "X-Almadar-Event": event
911
- };
912
- const signingSecret = secret || this.signingSecret;
913
- if (signingSecret) {
914
- headers["X-Almadar-Signature"] = "sha256=" + createHmac("sha256", signingSecret).update(body).digest("hex");
1814
+ if (response.status >= 500) {
1815
+ throw new Error(`DocuSign returned ${response.status}`);
915
1816
  }
916
- this.logger.debug("Sending webhook", { url: String(url), event: String(event) });
917
- const startTime = Date.now();
918
- const response = await fetch(url, {
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();
1825
+ }
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", {
919
1831
  method: "POST",
920
- headers,
921
- body,
922
- signal: AbortSignal.timeout(this.timeoutMs)
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
+ }
923
1854
  });
924
- if (response.status >= 500) {
925
- throw new Error(`Webhook endpoint returned ${response.status}`);
926
- }
927
- return {
928
- status: response.status,
929
- ok: response.ok,
930
- durationMs: Date.now() - startTime
931
- };
1855
+ return { envelopeId: body.envelopeId ?? "", status: body.status ?? "sent" };
1856
+ }
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
1866
+ });
1867
+ return { content: bytes.toString("base64"), documentName: `envelope-${envelopeId}.pdf` };
932
1868
  }
933
1869
  };
934
- registerIntegration("webhook", WebhookIntegration);
1870
+ registerIntegration("esign", EsignIntegration);
935
1871
  var LLMIntegration = class extends BaseIntegration {
936
1872
  constructor(config) {
937
1873
  super(config);
@@ -2502,25 +3438,35 @@ var OtelIntegration = class extends BaseIntegration {
2502
3438
  }
2503
3439
  };
2504
3440
  registerIntegration("otel", OtelIntegration);
2505
-
2506
- // src/integrations/oauth/index.ts
2507
3441
  var PROVIDER_AUTH_URLS = {
2508
3442
  google: "https://accounts.google.com/o/oauth2/v2/auth",
2509
3443
  github: "https://github.com/login/oauth/authorize",
2510
3444
  auth0: "https://auth.example.com/authorize"
2511
3445
  };
3446
+ var PROVIDER_ISSUERS = {
3447
+ google: "https://accounts.google.com"
3448
+ };
2512
3449
  var OAuthIntegration = class extends BaseIntegration {
2513
3450
  constructor(config) {
2514
3451
  super(config);
2515
- /** Maps state token -> provider for pending authorization flows */
3452
+ /** Maps state token -> provider for pending MOCK authorization flows */
2516
3453
  this.states = /* @__PURE__ */ new Map();
2517
- /** Maps access token -> token set */
3454
+ /** Maps access token -> token set (mock backend) */
2518
3455
  this.tokens = /* @__PURE__ */ new Map();
2519
- /** Maps refresh token -> access token for refresh lookups */
3456
+ /** Maps refresh token -> access token for refresh lookups (mock backend) */
2520
3457
  this.refreshIndex = /* @__PURE__ */ new Map();
2521
3458
  /** Maps access token -> mock user session */
2522
3459
  this.sessions = /* @__PURE__ */ new Map();
2523
- 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
+ );
2524
3470
  }
2525
3471
  async execute(action, params) {
2526
3472
  const validation = this.validateParams(action, params);
@@ -2541,19 +3487,19 @@ var OAuthIntegration = class extends BaseIntegration {
2541
3487
  let data;
2542
3488
  switch (action) {
2543
3489
  case "authorize":
2544
- data = await this.executeWithRetry(() => this.authorize(params));
3490
+ data = await this.executeWithRetry(() => this.real ? this.oidcAuthorize(params) : this.authorize(params));
2545
3491
  break;
2546
3492
  case "token":
2547
- data = await this.executeWithRetry(() => this.token(params));
3493
+ data = await this.executeWithRetry(() => this.real ? this.oidcToken(params) : this.token(params));
2548
3494
  break;
2549
3495
  case "refresh":
2550
- data = await this.executeWithRetry(() => this.refresh(params));
3496
+ data = await this.executeWithRetry(() => this.real ? this.oidcRefresh(params) : this.refresh(params));
2551
3497
  break;
2552
3498
  case "revoke":
2553
- data = await this.executeWithRetry(() => this.revoke(params));
3499
+ data = await this.executeWithRetry(() => this.real ? this.oidcRevoke(params) : this.revoke(params));
2554
3500
  break;
2555
3501
  case "userinfo":
2556
- data = await this.executeWithRetry(() => this.userinfo(params));
3502
+ data = await this.executeWithRetry(() => this.real ? this.oidcUserinfo(params) : this.userinfo(params));
2557
3503
  break;
2558
3504
  default:
2559
3505
  throw new Error(`Unknown action: ${action}`);
@@ -2568,7 +3514,119 @@ var OAuthIntegration = class extends BaseIntegration {
2568
3514
  }
2569
3515
  }
2570
3516
  // ---------------------------------------------------------------------------
2571
- // 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
2572
3630
  // ---------------------------------------------------------------------------
2573
3631
  /** Generate a random hex token of the given byte length. */
2574
3632
  generateToken(bytes = 32) {
@@ -2589,7 +3647,7 @@ var OAuthIntegration = class extends BaseIntegration {
2589
3647
  };
2590
3648
  }
2591
3649
  // ---------------------------------------------------------------------------
2592
- // Actions
3650
+ // Mock backend actions
2593
3651
  // ---------------------------------------------------------------------------
2594
3652
  async authorize(params) {
2595
3653
  const provider = params.provider;
@@ -2698,19 +3756,333 @@ var OAuthIntegration = class extends BaseIntegration {
2698
3756
  };
2699
3757
  registerIntegration("oauth", OAuthIntegration);
2700
3758
 
2701
- // 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
+ }
2702
4056
  var StorageIntegration = class extends BaseIntegration {
2703
4057
  constructor(config) {
2704
4058
  super(config);
2705
4059
  this.objects = /* @__PURE__ */ new Map();
2706
- const storageUrl = config.env.STORAGE_URL;
2707
- if (storageUrl) {
2708
- this.logger.warn(
2709
- "STORAGE_URL is configured but real storage client is not yet implemented. Falling back to in-memory store.",
2710
- { storageUrl }
2711
- );
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)");
2712
4085
  }
2713
- this.logger.info("Storage integration initialized (in-memory backend)");
2714
4086
  }
2715
4087
  async execute(action, params) {
2716
4088
  const validation = this.validateParams(action, params);
@@ -2760,107 +4132,182 @@ var StorageIntegration = class extends BaseIntegration {
2760
4132
  // ---------------------------------------------------------------------------
2761
4133
  // Helpers
2762
4134
  // ---------------------------------------------------------------------------
2763
- /** Build a composite key from bucket and object key. */
4135
+ bucketOf(params) {
4136
+ return params.bucket || this.defaultBucket;
4137
+ }
2764
4138
  compositeKey(bucket, key) {
2765
4139
  return `${bucket}/${key}`;
2766
4140
  }
2767
- /** Generate a deterministic etag from content. */
2768
- generateEtag(content) {
2769
- const raw = typeof content === "string" ? content : JSON.stringify(content);
2770
- let hash = 0;
2771
- for (let i = 0; i < raw.length; i++) {
2772
- const ch = raw.charCodeAt(i);
2773
- hash = (hash << 5) - hash + ch | 0;
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");
2774
4170
  }
2775
- 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
+ };
2776
4179
  }
2777
- /** Compute the byte size of content. */
2778
- computeSize(content) {
2779
- if (typeof content === "string") {
2780
- return new TextEncoder().encode(content).byteLength;
4180
+ publicUrl(bucket, key) {
4181
+ if (this.publicUrlBase) {
4182
+ return `${this.publicUrlBase.replace(/\/$/, "")}/${key}`;
4183
+ }
4184
+ const endpoint = this.config.env.STORAGE_ENDPOINT;
4185
+ if (endpoint) {
4186
+ return `${endpoint.replace(/\/$/, "")}/${bucket}/${key}`;
2781
4187
  }
2782
- return JSON.stringify(content).length;
4188
+ const region = this.config.env.STORAGE_REGION || "us-east-1";
4189
+ return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
2783
4190
  }
2784
4191
  // ---------------------------------------------------------------------------
2785
4192
  // Actions
2786
4193
  // ---------------------------------------------------------------------------
2787
4194
  async upload(params) {
2788
- const bucket = params.bucket;
2789
- const key = params.key;
2790
- const content = params.content;
2791
- const contentType = params.contentType ?? "application/octet-stream";
2792
- const metadata = params.metadata ?? {};
2793
- this.logger.debug("Storage UPLOAD", { bucket, key, contentType });
2794
- const size = this.computeSize(content);
2795
- const etag = this.generateEtag(content);
2796
- const obj = {
2797
- content,
2798
- contentType,
2799
- size,
2800
- metadata,
2801
- lastModified: Date.now(),
2802
- etag
2803
- };
2804
- this.objects.set(this.compositeKey(bucket, key), obj);
2805
- return { key, bucket, size, etag };
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 };
2806
4221
  }
2807
4222
  async download(params) {
2808
- const bucket = params.bucket;
4223
+ const bucket = this.bucketOf(params);
2809
4224
  const key = params.key;
2810
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
+ }
2811
4236
  const obj = this.objects.get(this.compositeKey(bucket, key));
2812
4237
  if (!obj) {
2813
4238
  throw new Error(`Object not found: ${bucket}/${key}`);
2814
4239
  }
2815
4240
  return {
2816
- content: obj.content,
4241
+ content: String(obj.content),
2817
4242
  contentType: obj.contentType,
2818
4243
  size: obj.size,
2819
4244
  metadata: obj.metadata
2820
4245
  };
2821
4246
  }
2822
4247
  async list(params) {
2823
- const bucket = params.bucket;
4248
+ const bucket = this.bucketOf(params);
2824
4249
  const prefix = params.prefix ?? "";
2825
4250
  const maxKeys = params.maxKeys ?? 1e3;
2826
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
+ }
2827
4267
  const bucketPrefix = `${bucket}/`;
2828
4268
  const fullPrefix = `${bucket}/${prefix}`;
2829
4269
  const results = [];
2830
4270
  for (const [compositeKey, obj] of this.objects) {
2831
4271
  if (!compositeKey.startsWith(fullPrefix)) continue;
2832
- const objectKey = compositeKey.slice(bucketPrefix.length);
2833
4272
  results.push({
2834
- key: objectKey,
4273
+ key: compositeKey.slice(bucketPrefix.length),
2835
4274
  size: obj.size,
2836
4275
  lastModified: obj.lastModified
2837
4276
  });
2838
4277
  }
2839
4278
  results.sort((a, b) => a.key.localeCompare(b.key));
2840
- const truncated = results.length > maxKeys;
2841
- return {
2842
- keys: results.slice(0, maxKeys),
2843
- truncated
2844
- };
4279
+ return { keys: results.slice(0, maxKeys), truncated: results.length > maxKeys };
2845
4280
  }
2846
4281
  async deleteObject(params) {
2847
- const bucket = params.bucket;
4282
+ const bucket = this.bucketOf(params);
2848
4283
  const key = params.key;
2849
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
+ }
2850
4289
  const existed = this.objects.has(this.compositeKey(bucket, key));
2851
4290
  this.objects.delete(this.compositeKey(bucket, key));
2852
4291
  return { deleted: existed };
2853
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
+ }
2854
4304
  async getSignedUrl(params) {
2855
- const bucket = params.bucket;
4305
+ const bucket = this.bucketOf(params);
2856
4306
  const key = params.key;
2857
4307
  const expiresIn = params.expiresIn ?? 3600;
2858
4308
  const operation = params.operation ?? "get";
2859
4309
  this.logger.debug("Storage GET_SIGNED_URL", { bucket, key, expiresIn, operation });
2860
- const expiresAt = Date.now() + expiresIn * 1e3;
2861
- const token = Math.random().toString(36).slice(2, 18);
2862
- const url = `https://storage.mock.local/${bucket}/${key}?X-Amz-Algorithm=MOCK-HMAC-SHA256&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host&X-Amz-Signature=${token}&operation=${operation}`;
2863
- return { url, expiresAt };
4310
+ return this.signUrl(bucket, key, operation, expiresIn);
2864
4311
  }
2865
4312
  };
2866
4313
  registerIntegration("storage", StorageIntegration);
@@ -3367,10 +4814,10 @@ var DatabaseIntegration = class extends BaseIntegration {
3367
4814
  }
3368
4815
  /** Resolve (and cache) the driver for a connection reference. */
3369
4816
  driverFor(connectionRef) {
3370
- const connectionString = process.env[connectionRef];
4817
+ const connectionString = resolveCredentialRef(connectionRef);
3371
4818
  if (!connectionString) {
3372
4819
  throw new IntegrationError(
3373
- `Connection reference "${connectionRef}" is not set in the environment`,
4820
+ `Connection reference "${connectionRef}" is not set in the credential store or environment`,
3374
4821
  "AUTH_ERROR"
3375
4822
  );
3376
4823
  }
@@ -3670,8 +5117,8 @@ var MockIntegration = class extends BaseIntegration {
3670
5117
 
3671
5118
  // src/runtime/effectHandler.ts
3672
5119
  function createCallServiceHandler(factory) {
3673
- const handler = async (service, action, params) => {
3674
- 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);
3675
5122
  if (!result.success) {
3676
5123
  throw result.error;
3677
5124
  }
@@ -3708,8 +5155,33 @@ function generateMockFromShape(shape) {
3708
5155
  }
3709
5156
  var RuntimeIntegrationManager = class {
3710
5157
  constructor() {
5158
+ this.credentialStore = null;
5159
+ this.storeEnvBase = null;
3711
5160
  this.factory = new IntegrationFactory();
3712
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() });
3713
5185
  }
3714
5186
  /**
3715
5187
  * Wrap `factory.execute` so an unknown/unconfigured service echoes its
@@ -3720,15 +5192,18 @@ var RuntimeIntegrationManager = class {
3720
5192
  * events fire — only the two "not set up" errors (`Unknown integration`,
3721
5193
  * `Integration not configured`) are caught. Subsumes the broader wrapper
3722
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.
3723
5198
  */
3724
5199
  installNotConfiguredFallback() {
3725
5200
  const originalExecute = this.factory.execute.bind(this.factory);
3726
- this.factory.execute = async (integration, action, params) => {
5201
+ this.factory.execute = async (integration, action, params, context) => {
3727
5202
  try {
3728
- return await originalExecute(integration, action, params);
5203
+ return await originalExecute(integration, action, params, context);
3729
5204
  } catch (err) {
3730
5205
  const msg = err instanceof Error ? err.message : String(err);
3731
- if (/Unknown integration|not configured/i.test(msg)) {
5206
+ if (process.env.NODE_ENV !== "production" && /Unknown integration|not configured/i.test(msg)) {
3732
5207
  return {
3733
5208
  success: true,
3734
5209
  data: { ...params, _mock: true, _service: integration, _action: action },
@@ -3742,10 +5217,18 @@ var RuntimeIntegrationManager = class {
3742
5217
  /**
3743
5218
  * Configure from environment variables.
3744
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`.
3745
5223
  */
3746
- configureFromEnv() {
3747
- const env = process.env;
3748
- 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
+ }
3749
5232
  this.configureMockMode();
3750
5233
  return;
3751
5234
  }
@@ -3789,6 +5272,79 @@ var RuntimeIntegrationManager = class {
3789
5272
  }
3790
5273
  });
3791
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
+ });
3792
5348
  this.factory.configure("webhook", {
3793
5349
  env: {
3794
5350
  WEBHOOK_SIGNING_SECRET: env.WEBHOOK_SIGNING_SECRET || "",