@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.
- package/dist/factory-BPVhvv5q.d.ts +59 -0
- package/dist/index.d.ts +265 -18
- package/dist/index.js +1768 -148
- package/dist/index.js.map +1 -1
- package/dist/mocks/index.d.ts +1 -1
- package/dist/mocks/index.js +39 -15
- package/dist/mocks/index.js.map +1 -1
- package/dist/runtime/index.d.ts +24 -5
- package/dist/runtime/index.js +1690 -134
- package/dist/runtime/index.js.map +1 -1
- package/dist/{contracts-Dv9PM_Cz.d.ts → store-CW1v7Apc.d.ts} +476 -5
- package/package.json +12 -7
- package/dist/factory-DTdVeyAi.d.ts +0 -41
package/dist/runtime/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
236
|
-
|
|
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(
|
|
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("
|
|
860
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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 "
|
|
887
|
-
data = await this.executeWithRetry(() => this.
|
|
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
|
|
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
|
|
902
|
-
const
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
909
|
-
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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/
|
|
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
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
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
|
-
|
|
4135
|
+
bucketOf(params) {
|
|
4136
|
+
return params.bucket || this.defaultBucket;
|
|
4137
|
+
}
|
|
2764
4138
|
compositeKey(bucket, key) {
|
|
2765
4139
|
return `${bucket}/${key}`;
|
|
2766
4140
|
}
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
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
|
-
|
|
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
|
|
2789
|
-
const key = params
|
|
2790
|
-
const
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 || "",
|