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