@almadar/integrations 2.24.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/factory-BPVhvv5q.d.ts +59 -0
- package/dist/index.d.ts +265 -18
- package/dist/index.js +1768 -148
- package/dist/index.js.map +1 -1
- package/dist/mocks/index.d.ts +1 -1
- package/dist/mocks/index.js +39 -15
- package/dist/mocks/index.js.map +1 -1
- package/dist/runtime/index.d.ts +24 -5
- package/dist/runtime/index.js +1690 -134
- package/dist/runtime/index.js.map +1 -1
- package/dist/{contracts-Dv9PM_Cz.d.ts → store-CW1v7Apc.d.ts} +476 -5
- package/package.json +12 -7
- package/dist/factory-DTdVeyAi.d.ts +0 -41
package/dist/index.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { createLogger } from '@almadar/logger';
|
|
2
2
|
import { integratorsRegistry } from '@almadar/core/patterns';
|
|
3
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHmac, createHash } from 'crypto';
|
|
3
4
|
import Stripe from 'stripe';
|
|
4
5
|
import { google } from 'googleapis';
|
|
5
6
|
import twilio from 'twilio';
|
|
6
7
|
import sgMail from '@sendgrid/mail';
|
|
7
8
|
import { Resend } from 'resend';
|
|
8
|
-
import
|
|
9
|
+
import webpush from 'web-push';
|
|
10
|
+
import { Readable } from 'stream';
|
|
9
11
|
import { getAvailableProvider, LLMClient, EmbeddingClient } from '@almadar/llm';
|
|
10
12
|
import { z } from 'zod';
|
|
11
13
|
import { execSync, spawn } from 'child_process';
|
|
12
14
|
import { mkdtempSync, writeFileSync, rmSync, promises } from 'fs';
|
|
13
15
|
import { join } from 'path';
|
|
14
16
|
import { tmpdir } from 'os';
|
|
17
|
+
import * as oidc from 'openid-client';
|
|
18
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
|
19
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
15
20
|
import { Pool } from 'pg';
|
|
16
21
|
|
|
17
22
|
// src/types.ts
|
|
@@ -222,56 +227,80 @@ function getRegisteredIntegrations() {
|
|
|
222
227
|
}
|
|
223
228
|
|
|
224
229
|
// src/factory.ts
|
|
230
|
+
function instanceKey(name, principal) {
|
|
231
|
+
return principal ? `${name}\0${principal}` : name;
|
|
232
|
+
}
|
|
225
233
|
var IntegrationFactory = class {
|
|
226
234
|
constructor() {
|
|
227
235
|
this.instances = /* @__PURE__ */ new Map();
|
|
228
236
|
this.configs = /* @__PURE__ */ new Map();
|
|
229
237
|
}
|
|
230
238
|
/**
|
|
231
|
-
* Configure an integration (doesn't instantiate yet)
|
|
239
|
+
* Configure an integration (doesn't instantiate yet). A `principal` scopes
|
|
240
|
+
* the config to that principal; the app-wide config (no principal) is the
|
|
241
|
+
* fallback for every principal.
|
|
232
242
|
*/
|
|
233
|
-
configure(name, config) {
|
|
234
|
-
this.configs.set(name, { name, ...config });
|
|
243
|
+
configure(name, config, principal) {
|
|
244
|
+
this.configs.set(instanceKey(name, principal), { name, ...config });
|
|
235
245
|
}
|
|
236
246
|
/**
|
|
237
|
-
* Get or create an integration instance
|
|
247
|
+
* Get or create an integration instance. Principal-scoped lookups fall
|
|
248
|
+
* back to the app-wide config when no per-principal config exists.
|
|
238
249
|
*/
|
|
239
|
-
get(name) {
|
|
240
|
-
|
|
241
|
-
|
|
250
|
+
get(name, principal) {
|
|
251
|
+
const key = instanceKey(name, principal);
|
|
252
|
+
const cached = this.instances.get(key);
|
|
253
|
+
if (cached) {
|
|
254
|
+
return cached;
|
|
242
255
|
}
|
|
243
256
|
const Constructor = getIntegration(name);
|
|
244
257
|
if (!Constructor) {
|
|
245
258
|
throw new Error(`Unknown integration: ${name}. Make sure it's imported.`);
|
|
246
259
|
}
|
|
247
|
-
const config = this.configs.get(name);
|
|
260
|
+
const config = this.configs.get(key) ?? this.configs.get(name);
|
|
248
261
|
if (!config) {
|
|
249
262
|
throw new Error(
|
|
250
263
|
`Integration not configured: ${name}. Call configure() first.`
|
|
251
264
|
);
|
|
252
265
|
}
|
|
253
266
|
const instance = new Constructor(config);
|
|
254
|
-
this.instances.set(
|
|
267
|
+
this.instances.set(key, instance);
|
|
255
268
|
return instance;
|
|
256
269
|
}
|
|
257
270
|
/**
|
|
258
271
|
* Execute an action on an integration
|
|
259
272
|
*/
|
|
260
|
-
async execute(integration, action, params) {
|
|
261
|
-
const instance = this.get(integration);
|
|
273
|
+
async execute(integration, action, params, context) {
|
|
274
|
+
const instance = this.get(integration, context?.principal);
|
|
262
275
|
return await instance.execute(action, params);
|
|
263
276
|
}
|
|
264
277
|
/**
|
|
265
278
|
* Check if integration is configured
|
|
266
279
|
*/
|
|
267
|
-
isConfigured(name) {
|
|
268
|
-
return this.configs.has(name);
|
|
280
|
+
isConfigured(name, principal) {
|
|
281
|
+
return this.configs.has(instanceKey(name, principal)) || this.configs.has(name);
|
|
269
282
|
}
|
|
270
283
|
/**
|
|
271
284
|
* Register an integration instance directly (used by mock infrastructure)
|
|
272
285
|
*/
|
|
273
|
-
registerInstance(name, instance) {
|
|
274
|
-
this.instances.set(name, instance);
|
|
286
|
+
registerInstance(name, instance, principal) {
|
|
287
|
+
this.instances.set(instanceKey(name, principal), instance);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Drop the cached instance(s) for a name so the next `get` rebuilds from
|
|
291
|
+
* the current config — how a credential change goes live without restart.
|
|
292
|
+
* Configs are kept; without a name, every instance is dropped.
|
|
293
|
+
*/
|
|
294
|
+
invalidate(name) {
|
|
295
|
+
if (name === void 0) {
|
|
296
|
+
this.instances.clear();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
for (const key of this.instances.keys()) {
|
|
300
|
+
if (key === name || key.startsWith(`${name}\0`)) {
|
|
301
|
+
this.instances.delete(key);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
275
304
|
}
|
|
276
305
|
/**
|
|
277
306
|
* Clear all instances (useful for testing)
|
|
@@ -298,6 +327,157 @@ function resetIntegrationFactory() {
|
|
|
298
327
|
_factory?.reset();
|
|
299
328
|
_factory = null;
|
|
300
329
|
}
|
|
330
|
+
var CREDENTIAL_ENTITY_TYPE = "AlmadarIntegrationCredential";
|
|
331
|
+
var CREDENTIAL_MASTER_KEY_ENV = "ALMADAR_CREDENTIAL_MASTER_KEY";
|
|
332
|
+
var ENCRYPTION_ALGORITHM = "aes-256-gcm";
|
|
333
|
+
function isNonEmptyString(v) {
|
|
334
|
+
return typeof v === "string" && v.length > 0;
|
|
335
|
+
}
|
|
336
|
+
var CredentialStore = class {
|
|
337
|
+
constructor(adapter, masterKey) {
|
|
338
|
+
this.byEnvVar = /* @__PURE__ */ new Map();
|
|
339
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
340
|
+
this.warmed = false;
|
|
341
|
+
this.adapter = adapter;
|
|
342
|
+
const key = masterKey ?? process.env[CREDENTIAL_MASTER_KEY_ENV];
|
|
343
|
+
const valid = typeof key === "string" && /^[0-9a-fA-F]{64}$/.test(key);
|
|
344
|
+
if (!valid && process.env.NODE_ENV === "production") {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`${CREDENTIAL_MASTER_KEY_ENV} must be a 64-character hex string (32 bytes) in production \u2014 generate one with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
this.masterKeyHex = valid ? key : void 0;
|
|
350
|
+
}
|
|
351
|
+
/** True when a valid master key is present (writes and decryption enabled). */
|
|
352
|
+
get enabled() {
|
|
353
|
+
return this.masterKeyHex !== void 0;
|
|
354
|
+
}
|
|
355
|
+
/** Subscribe to credential changes; returns an unsubscribe function. */
|
|
356
|
+
subscribe(listener) {
|
|
357
|
+
this.listeners.add(listener);
|
|
358
|
+
return () => this.listeners.delete(listener);
|
|
359
|
+
}
|
|
360
|
+
notify() {
|
|
361
|
+
for (const listener of this.listeners) listener();
|
|
362
|
+
}
|
|
363
|
+
encrypt(plaintext) {
|
|
364
|
+
if (!this.masterKeyHex) {
|
|
365
|
+
throw new Error(`${CREDENTIAL_MASTER_KEY_ENV} is not configured \u2014 cannot store credentials`);
|
|
366
|
+
}
|
|
367
|
+
const iv = randomBytes(12);
|
|
368
|
+
const cipher = createCipheriv(ENCRYPTION_ALGORITHM, Buffer.from(this.masterKeyHex, "hex"), iv);
|
|
369
|
+
let ciphertext = cipher.update(plaintext, "utf8", "hex");
|
|
370
|
+
ciphertext += cipher.final("hex");
|
|
371
|
+
return { ciphertext, iv: iv.toString("hex"), authTag: cipher.getAuthTag().toString("hex") };
|
|
372
|
+
}
|
|
373
|
+
decrypt(ciphertext, ivHex, authTagHex) {
|
|
374
|
+
if (!this.masterKeyHex) {
|
|
375
|
+
throw new Error(`${CREDENTIAL_MASTER_KEY_ENV} is not configured \u2014 cannot decrypt credentials`);
|
|
376
|
+
}
|
|
377
|
+
const decipher = createDecipheriv(
|
|
378
|
+
ENCRYPTION_ALGORITHM,
|
|
379
|
+
Buffer.from(this.masterKeyHex, "hex"),
|
|
380
|
+
Buffer.from(ivHex, "hex")
|
|
381
|
+
);
|
|
382
|
+
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
383
|
+
let plaintext = decipher.update(ciphertext, "hex", "utf8");
|
|
384
|
+
plaintext += decipher.final("utf8");
|
|
385
|
+
return plaintext;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Load and decrypt every stored row into memory. Returns the number of
|
|
389
|
+
* resolvable credentials. Without a master key nothing decrypts (rows are
|
|
390
|
+
* left in place, resolution falls through to env).
|
|
391
|
+
*/
|
|
392
|
+
async warm() {
|
|
393
|
+
this.byEnvVar.clear();
|
|
394
|
+
this.warmed = true;
|
|
395
|
+
if (!this.enabled) return 0;
|
|
396
|
+
const rows = await this.adapter.list(CREDENTIAL_ENTITY_TYPE);
|
|
397
|
+
for (const row of rows) {
|
|
398
|
+
const { id, service, envVar, ciphertext, iv, authTag } = row;
|
|
399
|
+
if (!isNonEmptyString(id) || !isNonEmptyString(service) || !isNonEmptyString(envVar) || !isNonEmptyString(ciphertext) || !isNonEmptyString(iv) || !isNonEmptyString(authTag)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const value = this.decrypt(ciphertext, iv, authTag);
|
|
403
|
+
this.byEnvVar.set(envVar, {
|
|
404
|
+
id,
|
|
405
|
+
service,
|
|
406
|
+
envVar,
|
|
407
|
+
value,
|
|
408
|
+
last4: value.slice(-4),
|
|
409
|
+
updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : 0
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return this.byEnvVar.size;
|
|
413
|
+
}
|
|
414
|
+
/** Resolve one credential by env-var name (warmed values only). */
|
|
415
|
+
resolve(envVar) {
|
|
416
|
+
return this.byEnvVar.get(envVar)?.value;
|
|
417
|
+
}
|
|
418
|
+
/** All warmed values as an env-shaped map, for merging over the process env. */
|
|
419
|
+
snapshotEnv() {
|
|
420
|
+
const env = {};
|
|
421
|
+
for (const [envVar, entry] of this.byEnvVar) env[envVar] = entry.value;
|
|
422
|
+
return env;
|
|
423
|
+
}
|
|
424
|
+
/** Masked entries for display — never includes plaintext. */
|
|
425
|
+
entries() {
|
|
426
|
+
return Array.from(this.byEnvVar.values()).map(({ service, envVar, last4, updatedAt }) => ({ service, envVar, last4, updatedAt })).sort((a, b) => a.service.localeCompare(b.service) || a.envVar.localeCompare(b.envVar));
|
|
427
|
+
}
|
|
428
|
+
/** Upsert one credential (encrypts, persists, re-warms the entry, notifies). */
|
|
429
|
+
async set(service, envVar, value) {
|
|
430
|
+
if (!this.warmed) await this.warm();
|
|
431
|
+
const { ciphertext, iv, authTag } = this.encrypt(value);
|
|
432
|
+
const updatedAt = Date.now();
|
|
433
|
+
const existing = this.byEnvVar.get(envVar);
|
|
434
|
+
const data = { service, envVar, ciphertext, iv, authTag, updatedAt };
|
|
435
|
+
let id;
|
|
436
|
+
if (existing) {
|
|
437
|
+
id = existing.id;
|
|
438
|
+
await this.adapter.update(CREDENTIAL_ENTITY_TYPE, id, data);
|
|
439
|
+
} else {
|
|
440
|
+
({ id } = await this.adapter.create(CREDENTIAL_ENTITY_TYPE, data));
|
|
441
|
+
}
|
|
442
|
+
const entry = { id, service, envVar, value, last4: value.slice(-4), updatedAt };
|
|
443
|
+
this.byEnvVar.set(envVar, entry);
|
|
444
|
+
this.notify();
|
|
445
|
+
const { service: s, envVar: e, last4, updatedAt: u } = entry;
|
|
446
|
+
return { service: s, envVar: e, last4, updatedAt: u };
|
|
447
|
+
}
|
|
448
|
+
/** Delete one credential row; resolution falls back to env afterwards. */
|
|
449
|
+
async remove(envVar) {
|
|
450
|
+
if (!this.warmed) await this.warm();
|
|
451
|
+
const existing = this.byEnvVar.get(envVar);
|
|
452
|
+
if (!existing) return false;
|
|
453
|
+
await this.adapter.delete(CREDENTIAL_ENTITY_TYPE, existing.id);
|
|
454
|
+
this.byEnvVar.delete(envVar);
|
|
455
|
+
this.notify();
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// src/credentials/resolver.ts
|
|
461
|
+
var installedStore = null;
|
|
462
|
+
var activeFactory = null;
|
|
463
|
+
function installActiveFactory(factory) {
|
|
464
|
+
activeFactory = factory;
|
|
465
|
+
}
|
|
466
|
+
function getActiveFactory() {
|
|
467
|
+
return activeFactory;
|
|
468
|
+
}
|
|
469
|
+
function installCredentialStore(store) {
|
|
470
|
+
installedStore = store;
|
|
471
|
+
}
|
|
472
|
+
function getInstalledCredentialStore() {
|
|
473
|
+
return installedStore;
|
|
474
|
+
}
|
|
475
|
+
function uninstallCredentialStore() {
|
|
476
|
+
installedStore = null;
|
|
477
|
+
}
|
|
478
|
+
function resolveCredentialRef(ref, env = process.env) {
|
|
479
|
+
return installedStore?.resolve(ref) ?? env[ref];
|
|
480
|
+
}
|
|
301
481
|
var STRIPE_API_VERSION = "2025-02-24.acacia";
|
|
302
482
|
function priceToTier(priceId, prices) {
|
|
303
483
|
if (priceId === prices.solo) return "solo";
|
|
@@ -962,44 +1142,933 @@ var EmailIntegration = class extends BaseIntegration {
|
|
|
962
1142
|
from || this.fromEmail
|
|
963
1143
|
);
|
|
964
1144
|
}
|
|
965
|
-
throw new Error(`Unknown email provider: ${this.provider}`);
|
|
1145
|
+
throw new Error(`Unknown email provider: ${this.provider}`);
|
|
1146
|
+
}
|
|
1147
|
+
async sendViaSendGrid(to, subject, body, from) {
|
|
1148
|
+
const msg = {
|
|
1149
|
+
to,
|
|
1150
|
+
from,
|
|
1151
|
+
subject,
|
|
1152
|
+
html: body
|
|
1153
|
+
};
|
|
1154
|
+
const response = await sgMail.send(msg);
|
|
1155
|
+
return {
|
|
1156
|
+
id: response[0].headers["x-message-id"],
|
|
1157
|
+
status: "sent"
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
async sendViaResend(to, subject, body, from) {
|
|
1161
|
+
if (!this.resendClient) {
|
|
1162
|
+
throw new Error("Resend client not initialized");
|
|
1163
|
+
}
|
|
1164
|
+
const response = await this.resendClient.emails.send({
|
|
1165
|
+
from,
|
|
1166
|
+
to,
|
|
1167
|
+
subject,
|
|
1168
|
+
html: body
|
|
1169
|
+
});
|
|
1170
|
+
return {
|
|
1171
|
+
id: response.data?.id,
|
|
1172
|
+
status: "sent"
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
registerIntegration("email", EmailIntegration);
|
|
1177
|
+
var WebhookIntegration = class extends BaseIntegration {
|
|
1178
|
+
constructor(config) {
|
|
1179
|
+
super(config);
|
|
1180
|
+
this.signingSecret = config.env.WEBHOOK_SIGNING_SECRET || "";
|
|
1181
|
+
this.timeoutMs = Number(config.env.WEBHOOK_TIMEOUT_MS) || 1e4;
|
|
1182
|
+
this.logger.info("Webhook integration initialized");
|
|
1183
|
+
}
|
|
1184
|
+
async execute(action, params) {
|
|
1185
|
+
const validation = this.validateParams(action, params);
|
|
1186
|
+
if (!validation.valid) {
|
|
1187
|
+
return {
|
|
1188
|
+
success: false,
|
|
1189
|
+
error: {
|
|
1190
|
+
name: "IntegrationError",
|
|
1191
|
+
message: "Validation failed",
|
|
1192
|
+
code: "VALIDATION_ERROR",
|
|
1193
|
+
details: validation.errors
|
|
1194
|
+
},
|
|
1195
|
+
metadata: this.createMetadata(action, 0)
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
const startTime = Date.now();
|
|
1199
|
+
let retries = 0;
|
|
1200
|
+
try {
|
|
1201
|
+
let data;
|
|
1202
|
+
switch (action) {
|
|
1203
|
+
case "send":
|
|
1204
|
+
data = await this.executeWithRetry(() => this.send(params));
|
|
1205
|
+
break;
|
|
1206
|
+
default:
|
|
1207
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
success: true,
|
|
1211
|
+
data,
|
|
1212
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1213
|
+
};
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
return this.handleError(action, error);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
async send(params) {
|
|
1219
|
+
const { url, event, payload, secret } = params;
|
|
1220
|
+
const body = JSON.stringify({
|
|
1221
|
+
event,
|
|
1222
|
+
payload: payload ?? {},
|
|
1223
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1224
|
+
});
|
|
1225
|
+
const headers = {
|
|
1226
|
+
"Content-Type": "application/json",
|
|
1227
|
+
"X-Almadar-Event": event
|
|
1228
|
+
};
|
|
1229
|
+
const signingSecret = secret || this.signingSecret;
|
|
1230
|
+
if (signingSecret) {
|
|
1231
|
+
headers["X-Almadar-Signature"] = "sha256=" + createHmac("sha256", signingSecret).update(body).digest("hex");
|
|
1232
|
+
}
|
|
1233
|
+
this.logger.debug("Sending webhook", { url: String(url), event: String(event) });
|
|
1234
|
+
const startTime = Date.now();
|
|
1235
|
+
const response = await fetch(url, {
|
|
1236
|
+
method: "POST",
|
|
1237
|
+
headers,
|
|
1238
|
+
body,
|
|
1239
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
1240
|
+
});
|
|
1241
|
+
if (response.status >= 500) {
|
|
1242
|
+
throw new Error(`Webhook endpoint returned ${response.status}`);
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
status: response.status,
|
|
1246
|
+
ok: response.ok,
|
|
1247
|
+
durationMs: Date.now() - startTime
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
registerIntegration("webhook", WebhookIntegration);
|
|
1252
|
+
function isParamRecord(value) {
|
|
1253
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
1254
|
+
}
|
|
1255
|
+
function toSubscription(value) {
|
|
1256
|
+
if (isParamRecord(value)) {
|
|
1257
|
+
const { endpoint, keys } = value;
|
|
1258
|
+
if (typeof endpoint === "string" && isParamRecord(keys)) {
|
|
1259
|
+
const { p256dh, auth } = keys;
|
|
1260
|
+
if (typeof p256dh === "string" && typeof auth === "string") {
|
|
1261
|
+
return { endpoint, keys: { p256dh, auth } };
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
throw new Error("push.send: subscription must be { endpoint, keys: { p256dh, auth } }");
|
|
1266
|
+
}
|
|
1267
|
+
var PushIntegration = class extends BaseIntegration {
|
|
1268
|
+
constructor(config) {
|
|
1269
|
+
super(config);
|
|
1270
|
+
this.vapidPublicKey = config.env.VAPID_PUBLIC_KEY || "";
|
|
1271
|
+
this.vapidPrivateKey = config.env.VAPID_PRIVATE_KEY || "";
|
|
1272
|
+
this.vapidSubject = config.env.VAPID_SUBJECT || "";
|
|
1273
|
+
this.logger.info("Push integration initialized");
|
|
1274
|
+
}
|
|
1275
|
+
async execute(action, params) {
|
|
1276
|
+
const validation = this.validateParams(action, params);
|
|
1277
|
+
if (!validation.valid) {
|
|
1278
|
+
return {
|
|
1279
|
+
success: false,
|
|
1280
|
+
error: {
|
|
1281
|
+
name: "IntegrationError",
|
|
1282
|
+
message: "Validation failed",
|
|
1283
|
+
code: "VALIDATION_ERROR",
|
|
1284
|
+
details: validation.errors
|
|
1285
|
+
},
|
|
1286
|
+
metadata: this.createMetadata(action, 0)
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const startTime = Date.now();
|
|
1290
|
+
let retries = 0;
|
|
1291
|
+
try {
|
|
1292
|
+
let data;
|
|
1293
|
+
switch (action) {
|
|
1294
|
+
case "send":
|
|
1295
|
+
data = await this.executeWithRetry(() => this.send(params));
|
|
1296
|
+
break;
|
|
1297
|
+
default:
|
|
1298
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
success: true,
|
|
1302
|
+
data,
|
|
1303
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1304
|
+
};
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
return this.handleError(action, error);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async send(params) {
|
|
1310
|
+
const { subscription, title, body, url, icon } = params;
|
|
1311
|
+
const sub = toSubscription(subscription);
|
|
1312
|
+
const payload = JSON.stringify({
|
|
1313
|
+
title,
|
|
1314
|
+
body,
|
|
1315
|
+
url: url || void 0,
|
|
1316
|
+
icon: icon || void 0
|
|
1317
|
+
});
|
|
1318
|
+
this.logger.debug("Sending push notification", { endpoint: sub.endpoint });
|
|
1319
|
+
try {
|
|
1320
|
+
const response = await webpush.sendNotification(sub, payload, {
|
|
1321
|
+
vapidDetails: {
|
|
1322
|
+
subject: this.vapidSubject,
|
|
1323
|
+
publicKey: this.vapidPublicKey,
|
|
1324
|
+
privateKey: this.vapidPrivateKey
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
return { statusCode: response.statusCode, ok: true, expired: false };
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
if (error instanceof webpush.WebPushError) {
|
|
1330
|
+
if (error.statusCode >= 500) {
|
|
1331
|
+
throw new Error(`Push endpoint returned ${error.statusCode}`);
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
statusCode: error.statusCode,
|
|
1335
|
+
ok: false,
|
|
1336
|
+
expired: error.statusCode === 404 || error.statusCode === 410
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
throw error;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
registerIntegration("push", PushIntegration);
|
|
1344
|
+
|
|
1345
|
+
// src/integrations/calendar/webhooks.ts
|
|
1346
|
+
function header(headers, name) {
|
|
1347
|
+
const direct = headers[name] ?? headers[name.toLowerCase()];
|
|
1348
|
+
if (direct !== void 0) return direct;
|
|
1349
|
+
const found = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
1350
|
+
return (found !== void 0 ? headers[found] : void 0) ?? "";
|
|
1351
|
+
}
|
|
1352
|
+
function parseCalendarPushNotification(headers, expectedToken) {
|
|
1353
|
+
const channelId = header(headers, "x-goog-channel-id");
|
|
1354
|
+
const resourceId = header(headers, "x-goog-resource-id");
|
|
1355
|
+
const resourceState = header(headers, "x-goog-resource-state");
|
|
1356
|
+
if (!channelId || !resourceId || !resourceState) {
|
|
1357
|
+
return { error: "missing-headers" };
|
|
1358
|
+
}
|
|
1359
|
+
if (expectedToken && header(headers, "x-goog-channel-token") !== expectedToken) {
|
|
1360
|
+
return { error: "bad-token" };
|
|
1361
|
+
}
|
|
1362
|
+
return {
|
|
1363
|
+
type: "calendar.changed",
|
|
1364
|
+
channelId,
|
|
1365
|
+
resourceId,
|
|
1366
|
+
resourceState,
|
|
1367
|
+
messageNumber: Number(header(headers, "x-goog-message-number")) || 0
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function googleCalendarHookProvider(expectedToken) {
|
|
1371
|
+
return (input) => {
|
|
1372
|
+
const parsed = parseCalendarPushNotification(input.headers, expectedToken);
|
|
1373
|
+
if ("error" in parsed) return { error: parsed.error };
|
|
1374
|
+
if (parsed.resourceState === "sync") return { ack: true };
|
|
1375
|
+
return {
|
|
1376
|
+
event: "CAL_REMOTE_CHANGED",
|
|
1377
|
+
payload: {
|
|
1378
|
+
channelId: parsed.channelId,
|
|
1379
|
+
resourceId: parsed.resourceId,
|
|
1380
|
+
resourceState: parsed.resourceState
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
var CalendarIntegration = class extends BaseIntegration {
|
|
1386
|
+
constructor(config) {
|
|
1387
|
+
super(config);
|
|
1388
|
+
const rawKey = config.env.GOOGLE_CALENDAR_SA_KEY;
|
|
1389
|
+
if (!rawKey) {
|
|
1390
|
+
throw new Error("GOOGLE_CALENDAR_SA_KEY not configured");
|
|
1391
|
+
}
|
|
1392
|
+
const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
|
|
1393
|
+
const key = JSON.parse(keyJson);
|
|
1394
|
+
const subject = config.env.GOOGLE_CALENDAR_SUBJECT || void 0;
|
|
1395
|
+
const auth = new google.auth.JWT({
|
|
1396
|
+
email: key.client_email,
|
|
1397
|
+
key: key.private_key,
|
|
1398
|
+
scopes: ["https://www.googleapis.com/auth/calendar"],
|
|
1399
|
+
subject
|
|
1400
|
+
});
|
|
1401
|
+
this.client = google.calendar({ version: "v3", auth });
|
|
1402
|
+
this.defaultCalendarId = config.env.GOOGLE_CALENDAR_ID || "primary";
|
|
1403
|
+
this.logger.info("Calendar integration initialized", {
|
|
1404
|
+
delegated: Boolean(subject)
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
async execute(action, params) {
|
|
1408
|
+
const validation = this.validateParams(action, params);
|
|
1409
|
+
if (!validation.valid) {
|
|
1410
|
+
return {
|
|
1411
|
+
success: false,
|
|
1412
|
+
error: {
|
|
1413
|
+
name: "IntegrationError",
|
|
1414
|
+
message: "Validation failed",
|
|
1415
|
+
code: "VALIDATION_ERROR",
|
|
1416
|
+
details: validation.errors
|
|
1417
|
+
},
|
|
1418
|
+
metadata: this.createMetadata(action, 0)
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
const startTime = Date.now();
|
|
1422
|
+
let retries = 0;
|
|
1423
|
+
try {
|
|
1424
|
+
let data;
|
|
1425
|
+
switch (action) {
|
|
1426
|
+
case "listEvents":
|
|
1427
|
+
data = await this.executeWithRetry(() => this.listEvents(params));
|
|
1428
|
+
break;
|
|
1429
|
+
case "createEvent":
|
|
1430
|
+
data = await this.executeWithRetry(() => this.createEvent(params));
|
|
1431
|
+
break;
|
|
1432
|
+
case "updateEvent":
|
|
1433
|
+
data = await this.executeWithRetry(() => this.updateEvent(params));
|
|
1434
|
+
break;
|
|
1435
|
+
case "deleteEvent":
|
|
1436
|
+
data = await this.executeWithRetry(() => this.deleteEvent(params));
|
|
1437
|
+
break;
|
|
1438
|
+
case "freeBusy":
|
|
1439
|
+
data = await this.executeWithRetry(() => this.freeBusy(params));
|
|
1440
|
+
break;
|
|
1441
|
+
case "watch":
|
|
1442
|
+
data = await this.executeWithRetry(() => this.watch(params));
|
|
1443
|
+
break;
|
|
1444
|
+
case "stopWatch":
|
|
1445
|
+
data = await this.executeWithRetry(() => this.stopWatch(params));
|
|
1446
|
+
break;
|
|
1447
|
+
default:
|
|
1448
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
success: true,
|
|
1452
|
+
data,
|
|
1453
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1454
|
+
};
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
return this.handleError(action, error);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
calendarId(params) {
|
|
1460
|
+
return params.calendarId || this.defaultCalendarId;
|
|
1461
|
+
}
|
|
1462
|
+
toEventTime(value) {
|
|
1463
|
+
return value.length === 10 ? { date: value } : { dateTime: value };
|
|
1464
|
+
}
|
|
1465
|
+
fromEventTime(time) {
|
|
1466
|
+
return time?.dateTime ?? time?.date ?? "";
|
|
1467
|
+
}
|
|
1468
|
+
async listEvents(params) {
|
|
1469
|
+
const { timeMin, timeMax, syncToken, maxResults } = params;
|
|
1470
|
+
const response = await this.client.events.list({
|
|
1471
|
+
calendarId: this.calendarId(params),
|
|
1472
|
+
// Incremental sync: a syncToken supersedes the window params (the API
|
|
1473
|
+
// rejects combining them).
|
|
1474
|
+
...syncToken ? { syncToken } : {
|
|
1475
|
+
timeMin: timeMin || void 0,
|
|
1476
|
+
timeMax: timeMax || void 0,
|
|
1477
|
+
singleEvents: true,
|
|
1478
|
+
orderBy: "startTime"
|
|
1479
|
+
},
|
|
1480
|
+
maxResults: maxResults || 250
|
|
1481
|
+
});
|
|
1482
|
+
const items = response.data.items ?? [];
|
|
1483
|
+
return {
|
|
1484
|
+
events: items.map((event) => ({
|
|
1485
|
+
id: event.id ?? "",
|
|
1486
|
+
summary: event.summary ?? "",
|
|
1487
|
+
description: event.description ?? "",
|
|
1488
|
+
location: event.location ?? "",
|
|
1489
|
+
start: this.fromEventTime(event.start ?? void 0),
|
|
1490
|
+
end: this.fromEventTime(event.end ?? void 0),
|
|
1491
|
+
status: event.status ?? "",
|
|
1492
|
+
updated: event.updated ?? ""
|
|
1493
|
+
})),
|
|
1494
|
+
nextSyncToken: response.data.nextSyncToken ?? null
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
resolveEnd(start, end, durationMinutes) {
|
|
1498
|
+
if (end) return end;
|
|
1499
|
+
if (start.length === 10) {
|
|
1500
|
+
const next = new Date(Date.parse(start) + 24 * 60 * 6e4);
|
|
1501
|
+
return next.toISOString().slice(0, 10);
|
|
1502
|
+
}
|
|
1503
|
+
if (durationMinutes && durationMinutes > 0) {
|
|
1504
|
+
return new Date(Date.parse(start) + durationMinutes * 6e4).toISOString();
|
|
1505
|
+
}
|
|
1506
|
+
throw new Error("createEvent requires `end` or a positive `durationMinutes`");
|
|
1507
|
+
}
|
|
1508
|
+
async createEvent(params) {
|
|
1509
|
+
const { summary, description, location, start, end, durationMinutes } = params;
|
|
1510
|
+
const response = await this.client.events.insert({
|
|
1511
|
+
calendarId: this.calendarId(params),
|
|
1512
|
+
requestBody: {
|
|
1513
|
+
summary,
|
|
1514
|
+
description: description || void 0,
|
|
1515
|
+
location: location || void 0,
|
|
1516
|
+
start: this.toEventTime(start),
|
|
1517
|
+
end: this.toEventTime(
|
|
1518
|
+
this.resolveEnd(start, end || void 0, durationMinutes || void 0)
|
|
1519
|
+
)
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
return {
|
|
1523
|
+
id: response.data.id ?? "",
|
|
1524
|
+
status: response.data.status ?? "",
|
|
1525
|
+
htmlLink: response.data.htmlLink ?? ""
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
async updateEvent(params) {
|
|
1529
|
+
const { eventId, summary, description, location, start, end } = params;
|
|
1530
|
+
const requestBody = {};
|
|
1531
|
+
if (typeof summary === "string") requestBody.summary = summary;
|
|
1532
|
+
if (typeof description === "string") requestBody.description = description;
|
|
1533
|
+
if (typeof location === "string") requestBody.location = location;
|
|
1534
|
+
if (typeof start === "string" && start) requestBody.start = this.toEventTime(start);
|
|
1535
|
+
if (typeof end === "string" && end) requestBody.end = this.toEventTime(end);
|
|
1536
|
+
const response = await this.client.events.patch({
|
|
1537
|
+
calendarId: this.calendarId(params),
|
|
1538
|
+
eventId,
|
|
1539
|
+
requestBody
|
|
1540
|
+
});
|
|
1541
|
+
return {
|
|
1542
|
+
id: response.data.id ?? "",
|
|
1543
|
+
status: response.data.status ?? ""
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
async deleteEvent(params) {
|
|
1547
|
+
const { eventId } = params;
|
|
1548
|
+
await this.client.events.delete({
|
|
1549
|
+
calendarId: this.calendarId(params),
|
|
1550
|
+
eventId
|
|
1551
|
+
});
|
|
1552
|
+
return { id: eventId, deleted: true };
|
|
1553
|
+
}
|
|
1554
|
+
async freeBusy(params) {
|
|
1555
|
+
const { timeMin, timeMax } = params;
|
|
1556
|
+
const id = this.calendarId(params);
|
|
1557
|
+
const response = await this.client.freebusy.query({
|
|
1558
|
+
requestBody: {
|
|
1559
|
+
timeMin,
|
|
1560
|
+
timeMax,
|
|
1561
|
+
items: [{ id }]
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
const busy = response.data.calendars?.[id]?.busy ?? [];
|
|
1565
|
+
return {
|
|
1566
|
+
busy: busy.map((slot) => ({ start: slot.start ?? "", end: slot.end ?? "" }))
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
async watch(params) {
|
|
1570
|
+
const { channelId, address, ttlSeconds } = params;
|
|
1571
|
+
const response = await this.client.events.watch({
|
|
1572
|
+
calendarId: this.calendarId(params),
|
|
1573
|
+
requestBody: {
|
|
1574
|
+
id: channelId,
|
|
1575
|
+
type: "web_hook",
|
|
1576
|
+
address,
|
|
1577
|
+
params: ttlSeconds ? { ttl: String(ttlSeconds) } : void 0
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
return {
|
|
1581
|
+
channelId: response.data.id ?? channelId,
|
|
1582
|
+
resourceId: response.data.resourceId ?? "",
|
|
1583
|
+
expiration: response.data.expiration ?? ""
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
async stopWatch(params) {
|
|
1587
|
+
const { channelId, resourceId } = params;
|
|
1588
|
+
await this.client.channels.stop({
|
|
1589
|
+
requestBody: {
|
|
1590
|
+
id: channelId,
|
|
1591
|
+
resourceId
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
return { stopped: true };
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
registerIntegration("calendar", CalendarIntegration);
|
|
1598
|
+
function decodeContent(content) {
|
|
1599
|
+
const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
|
|
1600
|
+
if (dataUrlMatch) {
|
|
1601
|
+
const [, mime, isB64, body] = dataUrlMatch;
|
|
1602
|
+
const bytes = isB64 ? Buffer.from(body, "base64") : Buffer.from(decodeURIComponent(body), "utf8");
|
|
1603
|
+
return { bytes, contentType: mime || null };
|
|
1604
|
+
}
|
|
1605
|
+
return { bytes: Buffer.from(content, "utf8"), contentType: null };
|
|
1606
|
+
}
|
|
1607
|
+
var DriveIntegration = class extends BaseIntegration {
|
|
1608
|
+
constructor(config) {
|
|
1609
|
+
super(config);
|
|
1610
|
+
const rawKey = config.env.GOOGLE_DRIVE_SA_KEY;
|
|
1611
|
+
if (!rawKey) {
|
|
1612
|
+
throw new Error("GOOGLE_DRIVE_SA_KEY not configured");
|
|
1613
|
+
}
|
|
1614
|
+
const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
|
|
1615
|
+
const key = JSON.parse(keyJson);
|
|
1616
|
+
const subject = config.env.GOOGLE_DRIVE_SUBJECT || void 0;
|
|
1617
|
+
const auth = new google.auth.JWT({
|
|
1618
|
+
email: key.client_email,
|
|
1619
|
+
key: key.private_key,
|
|
1620
|
+
scopes: ["https://www.googleapis.com/auth/drive"],
|
|
1621
|
+
subject
|
|
1622
|
+
});
|
|
1623
|
+
this.client = google.drive({ version: "v3", auth });
|
|
1624
|
+
this.logger.info("Drive integration initialized", { delegated: Boolean(subject) });
|
|
1625
|
+
}
|
|
1626
|
+
async execute(action, params) {
|
|
1627
|
+
const validation = this.validateParams(action, params);
|
|
1628
|
+
if (!validation.valid) {
|
|
1629
|
+
return {
|
|
1630
|
+
success: false,
|
|
1631
|
+
error: {
|
|
1632
|
+
name: "IntegrationError",
|
|
1633
|
+
message: "Validation failed",
|
|
1634
|
+
code: "VALIDATION_ERROR",
|
|
1635
|
+
details: validation.errors
|
|
1636
|
+
},
|
|
1637
|
+
metadata: this.createMetadata(action, 0)
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
const startTime = Date.now();
|
|
1641
|
+
try {
|
|
1642
|
+
let data;
|
|
1643
|
+
switch (action) {
|
|
1644
|
+
case "listFiles":
|
|
1645
|
+
data = await this.executeWithRetry(() => this.listFiles(params));
|
|
1646
|
+
break;
|
|
1647
|
+
case "getFile":
|
|
1648
|
+
data = await this.executeWithRetry(() => this.getFile(params));
|
|
1649
|
+
break;
|
|
1650
|
+
case "uploadFile":
|
|
1651
|
+
data = await this.executeWithRetry(() => this.uploadFile(params));
|
|
1652
|
+
break;
|
|
1653
|
+
case "createFolder":
|
|
1654
|
+
data = await this.executeWithRetry(() => this.createFolder(params));
|
|
1655
|
+
break;
|
|
1656
|
+
case "shareFile":
|
|
1657
|
+
data = await this.executeWithRetry(() => this.shareFile(params));
|
|
1658
|
+
break;
|
|
1659
|
+
default:
|
|
1660
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1661
|
+
}
|
|
1662
|
+
return {
|
|
1663
|
+
success: true,
|
|
1664
|
+
data,
|
|
1665
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1666
|
+
};
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
return this.handleError(action, error);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async listFiles(params) {
|
|
1672
|
+
const { folderId, query, maxResults } = params;
|
|
1673
|
+
const clauses = ["trashed = false"];
|
|
1674
|
+
if (folderId) clauses.push(`'${String(folderId).replace(/'/g, "\\'")}' in parents`);
|
|
1675
|
+
if (query) clauses.push(String(query));
|
|
1676
|
+
const response = await this.client.files.list({
|
|
1677
|
+
q: clauses.join(" and "),
|
|
1678
|
+
pageSize: maxResults || 100,
|
|
1679
|
+
fields: "files(id, name, mimeType, size, modifiedTime, webViewLink)"
|
|
1680
|
+
});
|
|
1681
|
+
return {
|
|
1682
|
+
files: (response.data.files ?? []).map((file) => ({
|
|
1683
|
+
id: file.id ?? "",
|
|
1684
|
+
name: file.name ?? "",
|
|
1685
|
+
mimeType: file.mimeType ?? "",
|
|
1686
|
+
size: Number(file.size ?? 0),
|
|
1687
|
+
modifiedTime: file.modifiedTime ?? "",
|
|
1688
|
+
webViewLink: file.webViewLink ?? ""
|
|
1689
|
+
}))
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
async getFile(params) {
|
|
1693
|
+
const fileId = params.fileId;
|
|
1694
|
+
const meta = await this.client.files.get({
|
|
1695
|
+
fileId,
|
|
1696
|
+
fields: "id, name, mimeType, size"
|
|
1697
|
+
});
|
|
1698
|
+
const content = await this.client.files.get(
|
|
1699
|
+
{ fileId, alt: "media" },
|
|
1700
|
+
{ responseType: "arraybuffer" }
|
|
1701
|
+
);
|
|
1702
|
+
const bytes = Buffer.from(content.data);
|
|
1703
|
+
return {
|
|
1704
|
+
id: meta.data.id ?? fileId,
|
|
1705
|
+
name: meta.data.name ?? "",
|
|
1706
|
+
mimeType: meta.data.mimeType ?? "application/octet-stream",
|
|
1707
|
+
content: bytes.toString("base64"),
|
|
1708
|
+
size: bytes.length
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
async uploadFile(params) {
|
|
1712
|
+
const { name, content, mimeType, folderId } = params;
|
|
1713
|
+
const { bytes, contentType } = decodeContent(content);
|
|
1714
|
+
const response = await this.client.files.create({
|
|
1715
|
+
requestBody: {
|
|
1716
|
+
name,
|
|
1717
|
+
parents: folderId ? [folderId] : void 0
|
|
1718
|
+
},
|
|
1719
|
+
media: {
|
|
1720
|
+
mimeType: mimeType || contentType || "application/octet-stream",
|
|
1721
|
+
body: Readable.from(bytes)
|
|
1722
|
+
},
|
|
1723
|
+
fields: "id, name, webViewLink"
|
|
1724
|
+
});
|
|
1725
|
+
return {
|
|
1726
|
+
id: response.data.id ?? "",
|
|
1727
|
+
name: response.data.name ?? name,
|
|
1728
|
+
webViewLink: response.data.webViewLink ?? ""
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
async createFolder(params) {
|
|
1732
|
+
const { name, parentId } = params;
|
|
1733
|
+
const response = await this.client.files.create({
|
|
1734
|
+
requestBody: {
|
|
1735
|
+
name,
|
|
1736
|
+
mimeType: "application/vnd.google-apps.folder",
|
|
1737
|
+
parents: parentId ? [parentId] : void 0
|
|
1738
|
+
},
|
|
1739
|
+
fields: "id, name"
|
|
1740
|
+
});
|
|
1741
|
+
return { id: response.data.id ?? "", name: response.data.name ?? name };
|
|
1742
|
+
}
|
|
1743
|
+
async shareFile(params) {
|
|
1744
|
+
const { fileId, email, role } = params;
|
|
1745
|
+
const response = await this.client.permissions.create({
|
|
1746
|
+
fileId,
|
|
1747
|
+
requestBody: {
|
|
1748
|
+
type: "user",
|
|
1749
|
+
role: role || "reader",
|
|
1750
|
+
emailAddress: email
|
|
1751
|
+
},
|
|
1752
|
+
fields: "id"
|
|
1753
|
+
});
|
|
1754
|
+
return { shared: true, permissionId: response.data.id ?? "" };
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
registerIntegration("drive", DriveIntegration);
|
|
1758
|
+
|
|
1759
|
+
// src/integrations/metaAds/index.ts
|
|
1760
|
+
var GRAPH_BASE = "https://graph.facebook.com/v21.0";
|
|
1761
|
+
var MetaAdsIntegration = class extends BaseIntegration {
|
|
1762
|
+
constructor(config) {
|
|
1763
|
+
super(config);
|
|
1764
|
+
this.accessToken = config.env.META_ACCESS_TOKEN || "";
|
|
1765
|
+
if (!this.accessToken) {
|
|
1766
|
+
throw new Error("META_ACCESS_TOKEN not configured");
|
|
1767
|
+
}
|
|
1768
|
+
this.defaultAccountId = config.env.META_AD_ACCOUNT_ID || "";
|
|
1769
|
+
this.logger.info("Meta Ads integration initialized");
|
|
1770
|
+
}
|
|
1771
|
+
async execute(action, params) {
|
|
1772
|
+
const validation = this.validateParams(action, params);
|
|
1773
|
+
if (!validation.valid) {
|
|
1774
|
+
return {
|
|
1775
|
+
success: false,
|
|
1776
|
+
error: {
|
|
1777
|
+
name: "IntegrationError",
|
|
1778
|
+
message: "Validation failed",
|
|
1779
|
+
code: "VALIDATION_ERROR",
|
|
1780
|
+
details: validation.errors
|
|
1781
|
+
},
|
|
1782
|
+
metadata: this.createMetadata(action, 0)
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
const startTime = Date.now();
|
|
1786
|
+
try {
|
|
1787
|
+
let data;
|
|
1788
|
+
switch (action) {
|
|
1789
|
+
case "getSpend":
|
|
1790
|
+
data = await this.executeWithRetry(() => this.getSpend(params));
|
|
1791
|
+
break;
|
|
1792
|
+
case "listCampaigns":
|
|
1793
|
+
data = await this.executeWithRetry(() => this.listCampaigns(params));
|
|
1794
|
+
break;
|
|
1795
|
+
default:
|
|
1796
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1797
|
+
}
|
|
1798
|
+
return {
|
|
1799
|
+
success: true,
|
|
1800
|
+
data,
|
|
1801
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1802
|
+
};
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
return this.handleError(action, error);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
accountId(params) {
|
|
1808
|
+
const id = params.accountId || this.defaultAccountId;
|
|
1809
|
+
if (!id) {
|
|
1810
|
+
throw new Error("No ad account: pass `accountId` or set META_AD_ACCOUNT_ID");
|
|
1811
|
+
}
|
|
1812
|
+
return id.startsWith("act_") ? id : `act_${id}`;
|
|
1813
|
+
}
|
|
1814
|
+
async graphGet(path, query) {
|
|
1815
|
+
const url = new URL(`${GRAPH_BASE}/${path}`);
|
|
1816
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1817
|
+
url.searchParams.set(key, value);
|
|
1818
|
+
}
|
|
1819
|
+
url.searchParams.set("access_token", this.accessToken);
|
|
1820
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
1821
|
+
if (response.status >= 500) {
|
|
1822
|
+
throw new Error(`Meta Graph API returned ${response.status}`);
|
|
1823
|
+
}
|
|
1824
|
+
const body = await response.json();
|
|
1825
|
+
if (!response.ok) {
|
|
1826
|
+
throw new Error(`Meta Graph API error: ${body.error?.message ?? response.status}`);
|
|
1827
|
+
}
|
|
1828
|
+
return body.data ?? body;
|
|
1829
|
+
}
|
|
1830
|
+
async getSpend(params) {
|
|
1831
|
+
const { since, until } = params;
|
|
1832
|
+
const rows = await this.graphGet(`${this.accountId(params)}/insights`, {
|
|
1833
|
+
fields: "spend,impressions,clicks,account_currency",
|
|
1834
|
+
time_range: JSON.stringify({ since, until }),
|
|
1835
|
+
level: "account"
|
|
1836
|
+
});
|
|
1837
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
1838
|
+
return {
|
|
1839
|
+
spend: Number(row?.spend ?? 0),
|
|
1840
|
+
currency: row?.account_currency ?? "",
|
|
1841
|
+
impressions: Number(row?.impressions ?? 0),
|
|
1842
|
+
clicks: Number(row?.clicks ?? 0)
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
async listCampaigns(params) {
|
|
1846
|
+
const { status } = params;
|
|
1847
|
+
const rows = await this.graphGet(`${this.accountId(params)}/campaigns`, {
|
|
1848
|
+
fields: "id,name,status,daily_budget",
|
|
1849
|
+
...status ? { effective_status: JSON.stringify([status]) } : {}
|
|
1850
|
+
});
|
|
1851
|
+
return {
|
|
1852
|
+
campaigns: (Array.isArray(rows) ? rows : []).map((row) => ({
|
|
1853
|
+
id: row.id ?? "",
|
|
1854
|
+
name: row.name ?? "",
|
|
1855
|
+
status: row.status ?? "",
|
|
1856
|
+
// Meta reports budgets in minor units (cents).
|
|
1857
|
+
dailyBudget: Number(row.daily_budget ?? 0) / 100
|
|
1858
|
+
}))
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
registerIntegration("metaAds", MetaAdsIntegration);
|
|
1863
|
+
|
|
1864
|
+
// src/integrations/accounting/index.ts
|
|
1865
|
+
var AccountingIntegration = class extends BaseIntegration {
|
|
1866
|
+
constructor(config) {
|
|
1867
|
+
super(config);
|
|
1868
|
+
this.logger.info("Accounting integration initialized (generic CSV export)");
|
|
1869
|
+
}
|
|
1870
|
+
async execute(action, params) {
|
|
1871
|
+
const validation = this.validateParams(action, params);
|
|
1872
|
+
if (!validation.valid) {
|
|
1873
|
+
return {
|
|
1874
|
+
success: false,
|
|
1875
|
+
error: {
|
|
1876
|
+
name: "IntegrationError",
|
|
1877
|
+
message: "Validation failed",
|
|
1878
|
+
code: "VALIDATION_ERROR",
|
|
1879
|
+
details: validation.errors
|
|
1880
|
+
},
|
|
1881
|
+
metadata: this.createMetadata(action, 0)
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
const startTime = Date.now();
|
|
1885
|
+
try {
|
|
1886
|
+
let data;
|
|
1887
|
+
switch (action) {
|
|
1888
|
+
case "exportInvoices":
|
|
1889
|
+
data = this.exportRows(params.invoices, INVOICE_COLUMNS, "invoices");
|
|
1890
|
+
break;
|
|
1891
|
+
case "exportJournal":
|
|
1892
|
+
data = this.exportRows(params.entries, JOURNAL_COLUMNS, "journal");
|
|
1893
|
+
break;
|
|
1894
|
+
default:
|
|
1895
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1896
|
+
}
|
|
1897
|
+
return {
|
|
1898
|
+
success: true,
|
|
1899
|
+
data,
|
|
1900
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1901
|
+
};
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
return this.handleError(action, error);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
exportRows(rowsValue, columns, kind) {
|
|
1907
|
+
if (!Array.isArray(rowsValue)) {
|
|
1908
|
+
throw new Error(`${kind} export requires an array of rows`);
|
|
1909
|
+
}
|
|
1910
|
+
const lines = [columns.join(",")];
|
|
1911
|
+
for (const row of rowsValue) {
|
|
1912
|
+
if (row === null || typeof row !== "object" || Array.isArray(row) || row instanceof Date) {
|
|
1913
|
+
throw new Error(`${kind} export: every row must be an object`);
|
|
1914
|
+
}
|
|
1915
|
+
const record = row;
|
|
1916
|
+
lines.push(columns.map((column) => csvCell(record[column])).join(","));
|
|
1917
|
+
}
|
|
1918
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1919
|
+
return {
|
|
1920
|
+
content: lines.join("\r\n") + "\r\n",
|
|
1921
|
+
filename: `${kind}-export-${stamp}.csv`,
|
|
1922
|
+
count: rowsValue.length
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
var INVOICE_COLUMNS = ["id", "number", "customer", "issuedAt", "dueAt", "currency", "net", "tax", "gross", "status"];
|
|
1927
|
+
var JOURNAL_COLUMNS = ["date", "account", "description", "debit", "credit", "reference"];
|
|
1928
|
+
function csvCell(value) {
|
|
1929
|
+
if (value === void 0 || value === null) return "";
|
|
1930
|
+
const raw = value instanceof Date ? value.toISOString() : String(value);
|
|
1931
|
+
return /[",\r\n]/.test(raw) ? `"${raw.replace(/"/g, '""')}"` : raw;
|
|
1932
|
+
}
|
|
1933
|
+
registerIntegration("accounting", AccountingIntegration);
|
|
1934
|
+
|
|
1935
|
+
// src/integrations/banking/index.ts
|
|
1936
|
+
var GC_BASE = "https://bankaccountdata.gocardless.com/api/v2";
|
|
1937
|
+
var BankingIntegration = class extends BaseIntegration {
|
|
1938
|
+
constructor(config) {
|
|
1939
|
+
super(config);
|
|
1940
|
+
this.accessToken = null;
|
|
1941
|
+
this.accessTokenExpiresAt = 0;
|
|
1942
|
+
this.secretId = config.env.GOCARDLESS_SECRET_ID || "";
|
|
1943
|
+
this.secretKey = config.env.GOCARDLESS_SECRET_KEY || "";
|
|
1944
|
+
if (!this.secretId || !this.secretKey) {
|
|
1945
|
+
throw new Error("GOCARDLESS_SECRET_ID / GOCARDLESS_SECRET_KEY not configured");
|
|
1946
|
+
}
|
|
1947
|
+
this.logger.info("Banking integration initialized (GoCardless Bank Account Data)");
|
|
1948
|
+
}
|
|
1949
|
+
async execute(action, params) {
|
|
1950
|
+
const validation = this.validateParams(action, params);
|
|
1951
|
+
if (!validation.valid) {
|
|
1952
|
+
return {
|
|
1953
|
+
success: false,
|
|
1954
|
+
error: {
|
|
1955
|
+
name: "IntegrationError",
|
|
1956
|
+
message: "Validation failed",
|
|
1957
|
+
code: "VALIDATION_ERROR",
|
|
1958
|
+
details: validation.errors
|
|
1959
|
+
},
|
|
1960
|
+
metadata: this.createMetadata(action, 0)
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
const startTime = Date.now();
|
|
1964
|
+
try {
|
|
1965
|
+
let data;
|
|
1966
|
+
switch (action) {
|
|
1967
|
+
case "createRequisition":
|
|
1968
|
+
data = await this.executeWithRetry(() => this.createRequisition(params));
|
|
1969
|
+
break;
|
|
1970
|
+
case "listAccounts":
|
|
1971
|
+
data = await this.executeWithRetry(() => this.listAccounts(params));
|
|
1972
|
+
break;
|
|
1973
|
+
case "listTransactions":
|
|
1974
|
+
data = await this.executeWithRetry(() => this.listTransactions(params));
|
|
1975
|
+
break;
|
|
1976
|
+
default:
|
|
1977
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1978
|
+
}
|
|
1979
|
+
return {
|
|
1980
|
+
success: true,
|
|
1981
|
+
data,
|
|
1982
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1983
|
+
};
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
return this.handleError(action, error);
|
|
1986
|
+
}
|
|
966
1987
|
}
|
|
967
|
-
async
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1988
|
+
async token() {
|
|
1989
|
+
if (this.accessToken && Date.now() < this.accessTokenExpiresAt - 6e4) {
|
|
1990
|
+
return this.accessToken;
|
|
1991
|
+
}
|
|
1992
|
+
const response = await fetch(`${GC_BASE}/token/new/`, {
|
|
1993
|
+
method: "POST",
|
|
1994
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1995
|
+
body: JSON.stringify({ secret_id: this.secretId, secret_key: this.secretKey }),
|
|
1996
|
+
signal: AbortSignal.timeout(15e3)
|
|
1997
|
+
});
|
|
1998
|
+
if (!response.ok) {
|
|
1999
|
+
throw new Error(`GoCardless token request failed: ${response.status}`);
|
|
2000
|
+
}
|
|
2001
|
+
const body = await response.json();
|
|
2002
|
+
this.accessToken = body.access;
|
|
2003
|
+
this.accessTokenExpiresAt = Date.now() + body.access_expires * 1e3;
|
|
2004
|
+
return this.accessToken;
|
|
979
2005
|
}
|
|
980
|
-
async
|
|
981
|
-
|
|
982
|
-
|
|
2006
|
+
async gcRequest(path, init2) {
|
|
2007
|
+
const response = await fetch(`${GC_BASE}${path}`, {
|
|
2008
|
+
method: init2?.method ?? "GET",
|
|
2009
|
+
headers: {
|
|
2010
|
+
Accept: "application/json",
|
|
2011
|
+
"Content-Type": "application/json",
|
|
2012
|
+
Authorization: `Bearer ${await this.token()}`
|
|
2013
|
+
},
|
|
2014
|
+
body: init2?.body === void 0 ? void 0 : JSON.stringify(init2.body),
|
|
2015
|
+
signal: AbortSignal.timeout(2e4)
|
|
2016
|
+
});
|
|
2017
|
+
if (response.status >= 500) {
|
|
2018
|
+
throw new Error(`GoCardless returned ${response.status}`);
|
|
983
2019
|
}
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
2020
|
+
const body = await response.json();
|
|
2021
|
+
if (!response.ok) {
|
|
2022
|
+
throw new Error(`GoCardless error ${response.status}: ${JSON.stringify(body).slice(0, 300)}`);
|
|
2023
|
+
}
|
|
2024
|
+
return body;
|
|
2025
|
+
}
|
|
2026
|
+
async createRequisition(params) {
|
|
2027
|
+
const { institutionId, redirectUrl, reference } = params;
|
|
2028
|
+
const body = await this.gcRequest("/requisitions/", {
|
|
2029
|
+
method: "POST",
|
|
2030
|
+
body: {
|
|
2031
|
+
institution_id: institutionId,
|
|
2032
|
+
redirect: redirectUrl,
|
|
2033
|
+
reference: reference || void 0
|
|
2034
|
+
}
|
|
989
2035
|
});
|
|
2036
|
+
return { requisitionId: body.id ?? "", link: body.link ?? "" };
|
|
2037
|
+
}
|
|
2038
|
+
async listAccounts(params) {
|
|
2039
|
+
const { requisitionId } = params;
|
|
2040
|
+
const body = await this.gcRequest(`/requisitions/${requisitionId}/`);
|
|
2041
|
+
return { accounts: body.accounts ?? [] };
|
|
2042
|
+
}
|
|
2043
|
+
async listTransactions(params) {
|
|
2044
|
+
const { accountId, dateFrom, dateTo } = params;
|
|
2045
|
+
const query = new URLSearchParams();
|
|
2046
|
+
if (dateFrom) query.set("date_from", dateFrom);
|
|
2047
|
+
if (dateTo) query.set("date_to", dateTo);
|
|
2048
|
+
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
|
2049
|
+
const body = await this.gcRequest(`/accounts/${accountId}/transactions/${suffix}`);
|
|
990
2050
|
return {
|
|
991
|
-
|
|
992
|
-
|
|
2051
|
+
transactions: (body.transactions?.booked ?? []).map((tx) => ({
|
|
2052
|
+
id: tx.transactionId ?? tx.internalTransactionId ?? "",
|
|
2053
|
+
amount: Number(tx.transactionAmount?.amount ?? 0),
|
|
2054
|
+
currency: tx.transactionAmount?.currency ?? "",
|
|
2055
|
+
date: tx.bookingDate ?? "",
|
|
2056
|
+
description: tx.remittanceInformationUnstructured ?? "",
|
|
2057
|
+
counterparty: tx.creditorName ?? tx.debtorName ?? ""
|
|
2058
|
+
}))
|
|
993
2059
|
};
|
|
994
2060
|
}
|
|
995
2061
|
};
|
|
996
|
-
registerIntegration("
|
|
997
|
-
|
|
2062
|
+
registerIntegration("banking", BankingIntegration);
|
|
2063
|
+
|
|
2064
|
+
// src/integrations/esign/index.ts
|
|
2065
|
+
var EsignIntegration = class extends BaseIntegration {
|
|
998
2066
|
constructor(config) {
|
|
999
2067
|
super(config);
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
2068
|
+
if (!config.env.DOCUSIGN_BASE_URL || !config.env.DOCUSIGN_ACCESS_TOKEN) {
|
|
2069
|
+
throw new Error("DOCUSIGN_BASE_URL / DOCUSIGN_ACCESS_TOKEN not configured");
|
|
2070
|
+
}
|
|
2071
|
+
this.logger.info("E-sign integration initialized (DocuSign)");
|
|
1003
2072
|
}
|
|
1004
2073
|
async execute(action, params) {
|
|
1005
2074
|
const validation = this.validateParams(action, params);
|
|
@@ -1016,12 +2085,17 @@ var WebhookIntegration = class extends BaseIntegration {
|
|
|
1016
2085
|
};
|
|
1017
2086
|
}
|
|
1018
2087
|
const startTime = Date.now();
|
|
1019
|
-
let retries = 0;
|
|
1020
2088
|
try {
|
|
1021
2089
|
let data;
|
|
1022
2090
|
switch (action) {
|
|
1023
|
-
case "
|
|
1024
|
-
data = await this.executeWithRetry(() => this.
|
|
2091
|
+
case "sendEnvelope":
|
|
2092
|
+
data = await this.executeWithRetry(() => this.sendEnvelope(params));
|
|
2093
|
+
break;
|
|
2094
|
+
case "getEnvelopeStatus":
|
|
2095
|
+
data = await this.executeWithRetry(() => this.getEnvelopeStatus(params));
|
|
2096
|
+
break;
|
|
2097
|
+
case "downloadDocument":
|
|
2098
|
+
data = await this.executeWithRetry(() => this.downloadDocument(params));
|
|
1025
2099
|
break;
|
|
1026
2100
|
default:
|
|
1027
2101
|
throw new Error(`Unknown action: ${action}`);
|
|
@@ -1029,46 +2103,81 @@ var WebhookIntegration = class extends BaseIntegration {
|
|
|
1029
2103
|
return {
|
|
1030
2104
|
success: true,
|
|
1031
2105
|
data,
|
|
1032
|
-
metadata: this.createMetadata(action, Date.now() - startTime
|
|
2106
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1033
2107
|
};
|
|
1034
2108
|
} catch (error) {
|
|
1035
2109
|
return this.handleError(action, error);
|
|
1036
2110
|
}
|
|
1037
2111
|
}
|
|
1038
|
-
async
|
|
1039
|
-
const
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
2112
|
+
async dsRequest(path, init2) {
|
|
2113
|
+
const base = this.config.env.DOCUSIGN_BASE_URL.replace(/\/$/, "");
|
|
2114
|
+
const response = await fetch(`${base}${path}`, {
|
|
2115
|
+
method: init2?.method ?? "GET",
|
|
2116
|
+
headers: {
|
|
2117
|
+
Accept: init2?.raw ? "application/pdf" : "application/json",
|
|
2118
|
+
"Content-Type": "application/json",
|
|
2119
|
+
Authorization: `Bearer ${this.config.env.DOCUSIGN_ACCESS_TOKEN}`
|
|
2120
|
+
},
|
|
2121
|
+
body: init2?.body === void 0 ? void 0 : JSON.stringify(init2.body),
|
|
2122
|
+
signal: AbortSignal.timeout(3e4)
|
|
1044
2123
|
});
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
"X-Almadar-Event": event
|
|
1048
|
-
};
|
|
1049
|
-
const signingSecret = secret || this.signingSecret;
|
|
1050
|
-
if (signingSecret) {
|
|
1051
|
-
headers["X-Almadar-Signature"] = "sha256=" + createHmac("sha256", signingSecret).update(body).digest("hex");
|
|
2124
|
+
if (response.status >= 500) {
|
|
2125
|
+
throw new Error(`DocuSign returned ${response.status}`);
|
|
1052
2126
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
2127
|
+
if (!response.ok) {
|
|
2128
|
+
const detail = await response.text();
|
|
2129
|
+
throw new Error(`DocuSign error ${response.status}: ${detail.slice(0, 300)}`);
|
|
2130
|
+
}
|
|
2131
|
+
if (init2?.raw) {
|
|
2132
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2133
|
+
}
|
|
2134
|
+
return response.json();
|
|
2135
|
+
}
|
|
2136
|
+
async sendEnvelope(params) {
|
|
2137
|
+
const { recipientEmail, recipientName, documentName, documentContent, emailSubject } = params;
|
|
2138
|
+
const rawContent = documentContent;
|
|
2139
|
+
const base64 = rawContent.startsWith("data:") ? rawContent.slice(rawContent.indexOf(",") + 1) : rawContent;
|
|
2140
|
+
const body = await this.dsRequest("/envelopes", {
|
|
1056
2141
|
method: "POST",
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
2142
|
+
body: {
|
|
2143
|
+
emailSubject: emailSubject || `Please sign: ${documentName}`,
|
|
2144
|
+
status: "sent",
|
|
2145
|
+
documents: [
|
|
2146
|
+
{
|
|
2147
|
+
documentBase64: base64,
|
|
2148
|
+
name: documentName,
|
|
2149
|
+
fileExtension: String(documentName).split(".").pop() || "pdf",
|
|
2150
|
+
documentId: "1"
|
|
2151
|
+
}
|
|
2152
|
+
],
|
|
2153
|
+
recipients: {
|
|
2154
|
+
signers: [
|
|
2155
|
+
{
|
|
2156
|
+
email: recipientEmail,
|
|
2157
|
+
name: recipientName,
|
|
2158
|
+
recipientId: "1",
|
|
2159
|
+
routingOrder: "1"
|
|
2160
|
+
}
|
|
2161
|
+
]
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
1060
2164
|
});
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
2165
|
+
return { envelopeId: body.envelopeId ?? "", status: body.status ?? "sent" };
|
|
2166
|
+
}
|
|
2167
|
+
async getEnvelopeStatus(params) {
|
|
2168
|
+
const { envelopeId } = params;
|
|
2169
|
+
const body = await this.dsRequest(`/envelopes/${envelopeId}`);
|
|
2170
|
+
return { status: body.status ?? "", completedAt: body.completedDateTime ?? "" };
|
|
2171
|
+
}
|
|
2172
|
+
async downloadDocument(params) {
|
|
2173
|
+
const { envelopeId } = params;
|
|
2174
|
+
const bytes = await this.dsRequest(`/envelopes/${envelopeId}/documents/combined`, {
|
|
2175
|
+
raw: true
|
|
2176
|
+
});
|
|
2177
|
+
return { content: bytes.toString("base64"), documentName: `envelope-${envelopeId}.pdf` };
|
|
1069
2178
|
}
|
|
1070
2179
|
};
|
|
1071
|
-
registerIntegration("
|
|
2180
|
+
registerIntegration("esign", EsignIntegration);
|
|
1072
2181
|
var LLMIntegration = class extends BaseIntegration {
|
|
1073
2182
|
constructor(config) {
|
|
1074
2183
|
super(config);
|
|
@@ -2639,25 +3748,35 @@ var OtelIntegration = class extends BaseIntegration {
|
|
|
2639
3748
|
}
|
|
2640
3749
|
};
|
|
2641
3750
|
registerIntegration("otel", OtelIntegration);
|
|
2642
|
-
|
|
2643
|
-
// src/integrations/oauth/index.ts
|
|
2644
3751
|
var PROVIDER_AUTH_URLS = {
|
|
2645
3752
|
google: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
2646
3753
|
github: "https://github.com/login/oauth/authorize",
|
|
2647
3754
|
auth0: "https://auth.example.com/authorize"
|
|
2648
3755
|
};
|
|
3756
|
+
var PROVIDER_ISSUERS = {
|
|
3757
|
+
google: "https://accounts.google.com"
|
|
3758
|
+
};
|
|
2649
3759
|
var OAuthIntegration = class extends BaseIntegration {
|
|
2650
3760
|
constructor(config) {
|
|
2651
3761
|
super(config);
|
|
2652
|
-
/** Maps state token -> provider for pending authorization flows */
|
|
3762
|
+
/** Maps state token -> provider for pending MOCK authorization flows */
|
|
2653
3763
|
this.states = /* @__PURE__ */ new Map();
|
|
2654
|
-
/** Maps access token -> token set */
|
|
3764
|
+
/** Maps access token -> token set (mock backend) */
|
|
2655
3765
|
this.tokens = /* @__PURE__ */ new Map();
|
|
2656
|
-
/** Maps refresh token -> access token for refresh lookups */
|
|
3766
|
+
/** Maps refresh token -> access token for refresh lookups (mock backend) */
|
|
2657
3767
|
this.refreshIndex = /* @__PURE__ */ new Map();
|
|
2658
3768
|
/** Maps access token -> mock user session */
|
|
2659
3769
|
this.sessions = /* @__PURE__ */ new Map();
|
|
2660
|
-
|
|
3770
|
+
/** Maps state -> pending OIDC authorization (real backend) */
|
|
3771
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
3772
|
+
/** Maps access token -> ID-token subject, for userinfo subject checks */
|
|
3773
|
+
this.subjects = /* @__PURE__ */ new Map();
|
|
3774
|
+
/** Discovered issuer configurations, keyed by issuer URL */
|
|
3775
|
+
this.discovered = /* @__PURE__ */ new Map();
|
|
3776
|
+
this.real = config.env.OAUTH_MODE !== "mock" && Boolean(config.env.OAUTH_CLIENT_ID) && Boolean(config.env.OAUTH_CLIENT_SECRET);
|
|
3777
|
+
this.logger.info(
|
|
3778
|
+
this.real ? "OAuth integration initialized (OIDC backend via openid-client)" : "OAuth integration initialized (mock backend)"
|
|
3779
|
+
);
|
|
2661
3780
|
}
|
|
2662
3781
|
async execute(action, params) {
|
|
2663
3782
|
const validation = this.validateParams(action, params);
|
|
@@ -2678,19 +3797,19 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2678
3797
|
let data;
|
|
2679
3798
|
switch (action) {
|
|
2680
3799
|
case "authorize":
|
|
2681
|
-
data = await this.executeWithRetry(() => this.authorize(params));
|
|
3800
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcAuthorize(params) : this.authorize(params));
|
|
2682
3801
|
break;
|
|
2683
3802
|
case "token":
|
|
2684
|
-
data = await this.executeWithRetry(() => this.token(params));
|
|
3803
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcToken(params) : this.token(params));
|
|
2685
3804
|
break;
|
|
2686
3805
|
case "refresh":
|
|
2687
|
-
data = await this.executeWithRetry(() => this.refresh(params));
|
|
3806
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcRefresh(params) : this.refresh(params));
|
|
2688
3807
|
break;
|
|
2689
3808
|
case "revoke":
|
|
2690
|
-
data = await this.executeWithRetry(() => this.revoke(params));
|
|
3809
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcRevoke(params) : this.revoke(params));
|
|
2691
3810
|
break;
|
|
2692
3811
|
case "userinfo":
|
|
2693
|
-
data = await this.executeWithRetry(() => this.userinfo(params));
|
|
3812
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcUserinfo(params) : this.userinfo(params));
|
|
2694
3813
|
break;
|
|
2695
3814
|
default:
|
|
2696
3815
|
throw new Error(`Unknown action: ${action}`);
|
|
@@ -2705,7 +3824,119 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2705
3824
|
}
|
|
2706
3825
|
}
|
|
2707
3826
|
// ---------------------------------------------------------------------------
|
|
2708
|
-
//
|
|
3827
|
+
// OIDC backend (openid-client)
|
|
3828
|
+
// ---------------------------------------------------------------------------
|
|
3829
|
+
issuerFor(provider) {
|
|
3830
|
+
const configured = this.config.env.OIDC_ISSUER_URL;
|
|
3831
|
+
if (configured) return configured;
|
|
3832
|
+
const issuer = PROVIDER_ISSUERS[provider];
|
|
3833
|
+
if (!issuer) {
|
|
3834
|
+
throw new Error(
|
|
3835
|
+
`Provider "${provider}" has no OIDC issuer \u2014 set OIDC_ISSUER_URL to an OIDC-compliant issuer, or use OAUTH_MODE=mock`
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3838
|
+
return issuer;
|
|
3839
|
+
}
|
|
3840
|
+
async configurationFor(provider) {
|
|
3841
|
+
const issuer = this.issuerFor(provider);
|
|
3842
|
+
const cached = this.discovered.get(issuer);
|
|
3843
|
+
if (cached) return cached;
|
|
3844
|
+
const configuration = await oidc.discovery(
|
|
3845
|
+
new URL(issuer),
|
|
3846
|
+
this.config.env.OAUTH_CLIENT_ID,
|
|
3847
|
+
this.config.env.OAUTH_CLIENT_SECRET
|
|
3848
|
+
);
|
|
3849
|
+
this.discovered.set(issuer, configuration);
|
|
3850
|
+
return configuration;
|
|
3851
|
+
}
|
|
3852
|
+
async oidcAuthorize(params) {
|
|
3853
|
+
const provider = params.provider;
|
|
3854
|
+
const scopes = params.scopes;
|
|
3855
|
+
const redirectUri = params.redirectUri || this.config.env.OAUTH_REDIRECT_URI;
|
|
3856
|
+
const configuration = await this.configurationFor(provider);
|
|
3857
|
+
const state = oidc.randomState();
|
|
3858
|
+
const pkceVerifier = oidc.randomPKCECodeVerifier();
|
|
3859
|
+
const codeChallenge = await oidc.calculatePKCECodeChallenge(pkceVerifier);
|
|
3860
|
+
const parameters = {
|
|
3861
|
+
redirect_uri: redirectUri,
|
|
3862
|
+
scope: scopes.join(" "),
|
|
3863
|
+
state,
|
|
3864
|
+
code_challenge: codeChallenge,
|
|
3865
|
+
code_challenge_method: "S256"
|
|
3866
|
+
};
|
|
3867
|
+
if (provider === "google") {
|
|
3868
|
+
parameters.access_type = "offline";
|
|
3869
|
+
parameters.prompt = "consent";
|
|
3870
|
+
}
|
|
3871
|
+
const authUrl = oidc.buildAuthorizationUrl(configuration, parameters);
|
|
3872
|
+
this.pending.set(state, { provider, redirectUri, pkceVerifier });
|
|
3873
|
+
return { authUrl: authUrl.toString(), state };
|
|
3874
|
+
}
|
|
3875
|
+
async oidcToken(params) {
|
|
3876
|
+
const code = params.code;
|
|
3877
|
+
const state = params.state;
|
|
3878
|
+
const pendingAuth = this.pending.get(state);
|
|
3879
|
+
if (!pendingAuth) {
|
|
3880
|
+
throw new Error(`Invalid or expired state token: ${state}`);
|
|
3881
|
+
}
|
|
3882
|
+
this.pending.delete(state);
|
|
3883
|
+
const configuration = await this.configurationFor(pendingAuth.provider);
|
|
3884
|
+
const callbackUrl = new URL(pendingAuth.redirectUri);
|
|
3885
|
+
callbackUrl.searchParams.set("code", code);
|
|
3886
|
+
callbackUrl.searchParams.set("state", state);
|
|
3887
|
+
const tokens = await oidc.authorizationCodeGrant(configuration, callbackUrl, {
|
|
3888
|
+
expectedState: state,
|
|
3889
|
+
pkceCodeVerifier: pendingAuth.pkceVerifier
|
|
3890
|
+
});
|
|
3891
|
+
const claims = tokens.claims();
|
|
3892
|
+
if (claims?.sub) {
|
|
3893
|
+
this.subjects.set(tokens.access_token, claims.sub);
|
|
3894
|
+
}
|
|
3895
|
+
return {
|
|
3896
|
+
accessToken: tokens.access_token,
|
|
3897
|
+
refreshToken: tokens.refresh_token ?? "",
|
|
3898
|
+
expiresIn: tokens.expires_in ?? 3600,
|
|
3899
|
+
tokenType: "bearer"
|
|
3900
|
+
};
|
|
3901
|
+
}
|
|
3902
|
+
async oidcRefresh(params) {
|
|
3903
|
+
const refreshToken = params.refreshToken;
|
|
3904
|
+
const configuration = await this.configurationFor("google");
|
|
3905
|
+
const tokens = await oidc.refreshTokenGrant(configuration, refreshToken);
|
|
3906
|
+
const claims = tokens.claims();
|
|
3907
|
+
if (claims?.sub) {
|
|
3908
|
+
this.subjects.set(tokens.access_token, claims.sub);
|
|
3909
|
+
}
|
|
3910
|
+
return {
|
|
3911
|
+
accessToken: tokens.access_token,
|
|
3912
|
+
expiresIn: tokens.expires_in ?? 3600
|
|
3913
|
+
};
|
|
3914
|
+
}
|
|
3915
|
+
async oidcRevoke(params) {
|
|
3916
|
+
const token = params.token;
|
|
3917
|
+
const configuration = await this.configurationFor("google");
|
|
3918
|
+
await oidc.tokenRevocation(configuration, token);
|
|
3919
|
+
this.subjects.delete(token);
|
|
3920
|
+
return { revoked: true };
|
|
3921
|
+
}
|
|
3922
|
+
async oidcUserinfo(params) {
|
|
3923
|
+
const accessToken = params.accessToken;
|
|
3924
|
+
const configuration = await this.configurationFor("google");
|
|
3925
|
+
const subject = this.subjects.get(accessToken);
|
|
3926
|
+
const info = await oidc.fetchUserInfo(
|
|
3927
|
+
configuration,
|
|
3928
|
+
accessToken,
|
|
3929
|
+
subject ?? oidc.skipSubjectCheck
|
|
3930
|
+
);
|
|
3931
|
+
return {
|
|
3932
|
+
sub: info.sub,
|
|
3933
|
+
email: typeof info.email === "string" ? info.email : "",
|
|
3934
|
+
name: typeof info.name === "string" ? info.name : "",
|
|
3935
|
+
picture: typeof info.picture === "string" ? info.picture : ""
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
// ---------------------------------------------------------------------------
|
|
3939
|
+
// Mock backend helpers
|
|
2709
3940
|
// ---------------------------------------------------------------------------
|
|
2710
3941
|
/** Generate a random hex token of the given byte length. */
|
|
2711
3942
|
generateToken(bytes = 32) {
|
|
@@ -2726,7 +3957,7 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2726
3957
|
};
|
|
2727
3958
|
}
|
|
2728
3959
|
// ---------------------------------------------------------------------------
|
|
2729
|
-
//
|
|
3960
|
+
// Mock backend actions
|
|
2730
3961
|
// ---------------------------------------------------------------------------
|
|
2731
3962
|
async authorize(params) {
|
|
2732
3963
|
const provider = params.provider;
|
|
@@ -2835,19 +4066,333 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2835
4066
|
};
|
|
2836
4067
|
registerIntegration("oauth", OAuthIntegration);
|
|
2837
4068
|
|
|
2838
|
-
// src/
|
|
4069
|
+
// src/contracts.ts
|
|
4070
|
+
var serviceCredentials = {
|
|
4071
|
+
stripe: [
|
|
4072
|
+
{ envVar: "STRIPE_SECRET_KEY", required: true, description: "Stripe secret API key" },
|
|
4073
|
+
{ envVar: "STRIPE_WEBHOOK_SECRET", required: false, description: "Webhook endpoint signing secret" }
|
|
4074
|
+
],
|
|
4075
|
+
youtube: [
|
|
4076
|
+
{ envVar: "YOUTUBE_API_KEY", required: true, description: "YouTube Data API key" }
|
|
4077
|
+
],
|
|
4078
|
+
twilio: [
|
|
4079
|
+
{ envVar: "TWILIO_ACCOUNT_SID", required: true, description: "Twilio account SID" },
|
|
4080
|
+
{ envVar: "TWILIO_AUTH_TOKEN", required: true, description: "Twilio auth token" },
|
|
4081
|
+
{ envVar: "TWILIO_PHONE_NUMBER", required: false, description: "Default sender phone number" }
|
|
4082
|
+
],
|
|
4083
|
+
email: [
|
|
4084
|
+
{ envVar: "SENDGRID_API_KEY", required: false, description: "SendGrid API key (use this OR RESEND_API_KEY)" },
|
|
4085
|
+
{ envVar: "RESEND_API_KEY", required: false, description: "Resend API key (use this OR SENDGRID_API_KEY)" },
|
|
4086
|
+
{ envVar: "FROM_EMAIL", required: false, description: "Default sender email address" }
|
|
4087
|
+
],
|
|
4088
|
+
webhook: [
|
|
4089
|
+
{ envVar: "WEBHOOK_SIGNING_SECRET", required: false, description: "Default HMAC-SHA256 signing secret (per-call secret overrides)" },
|
|
4090
|
+
{ envVar: "WEBHOOK_TIMEOUT_MS", required: false, description: "Per-request timeout (ms, default 10000)" }
|
|
4091
|
+
],
|
|
4092
|
+
push: [
|
|
4093
|
+
{ envVar: "VAPID_PUBLIC_KEY", required: true, description: "VAPID public key (web-push generate-vapid-keys)" },
|
|
4094
|
+
{ envVar: "VAPID_PRIVATE_KEY", required: true, description: "VAPID private key" },
|
|
4095
|
+
{ envVar: "VAPID_SUBJECT", required: true, description: "VAPID subject (mailto: or https: contact URI)" }
|
|
4096
|
+
],
|
|
4097
|
+
calendar: [
|
|
4098
|
+
{ envVar: "GOOGLE_CALENDAR_SA_KEY", required: true, description: "Google service-account key JSON (raw or base64) with calendar scope; store in Secret Manager, bind as env" },
|
|
4099
|
+
{ envVar: "GOOGLE_CALENDAR_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" },
|
|
4100
|
+
{ envVar: "GOOGLE_CALENDAR_ID", required: false, description: "Default calendar id (default: primary)" }
|
|
4101
|
+
],
|
|
4102
|
+
drive: [
|
|
4103
|
+
{ envVar: "GOOGLE_DRIVE_SA_KEY", required: true, description: "Google service-account key JSON (raw or base64) with drive scope; store in Secret Manager, bind as env" },
|
|
4104
|
+
{ envVar: "GOOGLE_DRIVE_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" }
|
|
4105
|
+
],
|
|
4106
|
+
metaAds: [
|
|
4107
|
+
{ envVar: "META_ACCESS_TOKEN", required: true, description: "Meta Graph API access token (Marketing API, ads_read)" },
|
|
4108
|
+
{ envVar: "META_AD_ACCOUNT_ID", required: false, description: "Default ad account id (act_\u2026); per-call accountId overrides" }
|
|
4109
|
+
],
|
|
4110
|
+
accounting: [],
|
|
4111
|
+
banking: [
|
|
4112
|
+
{ envVar: "GOCARDLESS_SECRET_ID", required: true, description: "GoCardless Bank Account Data secret id" },
|
|
4113
|
+
{ envVar: "GOCARDLESS_SECRET_KEY", required: true, description: "GoCardless Bank Account Data secret key" }
|
|
4114
|
+
],
|
|
4115
|
+
esign: [
|
|
4116
|
+
{ envVar: "DOCUSIGN_BASE_URL", required: true, description: "DocuSign REST base (e.g. https://demo.docusign.net/restapi/v2.1/accounts/<accountId>)" },
|
|
4117
|
+
{ envVar: "DOCUSIGN_ACCESS_TOKEN", required: true, description: "DocuSign OAuth access token (JWT grant rotation is the deployment concern)" }
|
|
4118
|
+
],
|
|
4119
|
+
llm: [
|
|
4120
|
+
{ envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key (use this OR OPENAI_API_KEY)" },
|
|
4121
|
+
{ envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key (use this OR ANTHROPIC_API_KEY)" }
|
|
4122
|
+
],
|
|
4123
|
+
"llm-integration": [
|
|
4124
|
+
{ envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key" },
|
|
4125
|
+
{ envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key" }
|
|
4126
|
+
],
|
|
4127
|
+
ml: [
|
|
4128
|
+
{ envVar: "MASAR_URL", required: true, description: "Base URL of the deployed model-serving orbital" },
|
|
4129
|
+
{ envVar: "MASAR_ML_TRAIT", required: true, description: "Kebab-case trait name the serving orbital exposes its /events route under" }
|
|
4130
|
+
],
|
|
4131
|
+
deepagent: [
|
|
4132
|
+
{ envVar: "DEEPAGENT_API_URL", required: true, description: "DeepAgent server URL" },
|
|
4133
|
+
{ envVar: "DEEPAGENT_API_KEY", required: false, description: "DeepAgent API key" }
|
|
4134
|
+
],
|
|
4135
|
+
github: [
|
|
4136
|
+
{ envVar: "GITHUB_TOKEN", required: true, description: "GitHub personal access token" },
|
|
4137
|
+
{ envVar: "GITHUB_OWNER", required: false, description: "Default repository owner" },
|
|
4138
|
+
{ envVar: "GITHUB_REPO", required: false, description: "Default repository name" }
|
|
4139
|
+
],
|
|
4140
|
+
docker: [
|
|
4141
|
+
{ envVar: "DOCKER_HOST", required: false, description: "Docker daemon host URL" }
|
|
4142
|
+
],
|
|
4143
|
+
storage: [
|
|
4144
|
+
{ envVar: "STORAGE_ACCESS_KEY_ID", required: true, description: "S3-compatible access key id" },
|
|
4145
|
+
{ envVar: "STORAGE_SECRET_ACCESS_KEY", required: true, description: "S3-compatible secret access key" },
|
|
4146
|
+
{ envVar: "STORAGE_BUCKET", required: true, description: "Default bucket name" },
|
|
4147
|
+
{ envVar: "STORAGE_REGION", required: false, description: "Region (default us-east-1)" },
|
|
4148
|
+
{ envVar: "STORAGE_ENDPOINT", required: false, description: "Custom S3-compatible endpoint (R2/MinIO/\u2026); empty = AWS S3" },
|
|
4149
|
+
{ envVar: "STORAGE_PUBLIC_URL_BASE", required: false, description: "Base URL for public-acl object links; empty = endpoint-derived" }
|
|
4150
|
+
],
|
|
4151
|
+
queue: [
|
|
4152
|
+
{ envVar: "QUEUE_URL", required: true, description: "Message queue connection URL" }
|
|
4153
|
+
],
|
|
4154
|
+
redis: [
|
|
4155
|
+
{ envVar: "REDIS_URL", required: true, description: "Redis connection URL" }
|
|
4156
|
+
],
|
|
4157
|
+
oauth: [
|
|
4158
|
+
{ envVar: "OAUTH_CLIENT_ID", required: true, description: "OIDC client ID" },
|
|
4159
|
+
{ envVar: "OAUTH_CLIENT_SECRET", required: true, description: "OIDC client secret" },
|
|
4160
|
+
{ envVar: "OAUTH_REDIRECT_URI", required: false, description: "OIDC redirect URI (default per-call param)" },
|
|
4161
|
+
{ envVar: "OIDC_ISSUER_URL", required: false, description: "OIDC issuer URL (default https://accounts.google.com)" }
|
|
4162
|
+
],
|
|
4163
|
+
credentials: [
|
|
4164
|
+
{ envVar: "ALMADAR_CREDENTIAL_MASTER_KEY", required: false, description: "AES-256 master key (64-char hex) enabling the hosted credential store \u2014 hold it alone in the platform secret store" }
|
|
4165
|
+
],
|
|
4166
|
+
otel: [
|
|
4167
|
+
{ envVar: "OTEL_EXPORTER_OTLP_ENDPOINT", required: true, description: "OpenTelemetry collector endpoint" },
|
|
4168
|
+
{ envVar: "OTEL_SERVICE_NAME", required: false, description: "Service name for traces" }
|
|
4169
|
+
],
|
|
4170
|
+
cli: [],
|
|
4171
|
+
// No fixed env vars — connection strings are resolved per-query from the
|
|
4172
|
+
// caller-supplied connectionRef, so credentials cannot be declared statically.
|
|
4173
|
+
database: [],
|
|
4174
|
+
wikimedia: [
|
|
4175
|
+
{ envVar: "WIKIMEDIA_USER_AGENT", required: false, description: "Descriptive User-Agent for the Wikipedia API" },
|
|
4176
|
+
{ envVar: "WIKIMEDIA_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
|
|
4177
|
+
],
|
|
4178
|
+
iconify: [
|
|
4179
|
+
{ envVar: "ICONIFY_USER_AGENT", required: false, description: "User-Agent for the Iconify API" },
|
|
4180
|
+
{ envVar: "ICONIFY_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
|
|
4181
|
+
],
|
|
4182
|
+
arxiv: [
|
|
4183
|
+
{ envVar: "ARXIV_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
|
|
4184
|
+
]
|
|
4185
|
+
};
|
|
4186
|
+
var serviceProbes = {
|
|
4187
|
+
calendar: { action: "listEvents", params: { maxResults: 1 } },
|
|
4188
|
+
drive: { action: "listFiles", params: { maxResults: 1 } },
|
|
4189
|
+
metaAds: { action: "listCampaigns", params: {} }
|
|
4190
|
+
};
|
|
4191
|
+
|
|
4192
|
+
// src/integrations/credentials/index.ts
|
|
4193
|
+
var ENV_VAR_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
4194
|
+
function isProbeService(service) {
|
|
4195
|
+
return service in serviceProbes;
|
|
4196
|
+
}
|
|
4197
|
+
var CredentialsIntegration = class extends BaseIntegration {
|
|
4198
|
+
constructor(config) {
|
|
4199
|
+
super(config);
|
|
4200
|
+
this.logger.info("Credentials integration initialized (tenant credential store surface)");
|
|
4201
|
+
}
|
|
4202
|
+
async execute(action, params) {
|
|
4203
|
+
const validation = this.validateParams(action, params);
|
|
4204
|
+
if (!validation.valid) {
|
|
4205
|
+
return {
|
|
4206
|
+
success: false,
|
|
4207
|
+
error: {
|
|
4208
|
+
name: "IntegrationError",
|
|
4209
|
+
message: "Validation failed",
|
|
4210
|
+
code: "VALIDATION_ERROR",
|
|
4211
|
+
details: validation.errors
|
|
4212
|
+
},
|
|
4213
|
+
metadata: this.createMetadata(action, 0)
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
const startTime = Date.now();
|
|
4217
|
+
try {
|
|
4218
|
+
let data;
|
|
4219
|
+
switch (action) {
|
|
4220
|
+
case "list":
|
|
4221
|
+
data = this.list(typeof params.service === "string" ? params.service : void 0);
|
|
4222
|
+
break;
|
|
4223
|
+
case "set":
|
|
4224
|
+
data = await this.set(params.service, params.envVar, params.value);
|
|
4225
|
+
break;
|
|
4226
|
+
case "remove":
|
|
4227
|
+
data = await this.remove(params.service, params.envVar);
|
|
4228
|
+
break;
|
|
4229
|
+
case "test":
|
|
4230
|
+
data = await this.test(params.service);
|
|
4231
|
+
break;
|
|
4232
|
+
default:
|
|
4233
|
+
throw new Error(`Unknown action: ${action}`);
|
|
4234
|
+
}
|
|
4235
|
+
return {
|
|
4236
|
+
success: true,
|
|
4237
|
+
data,
|
|
4238
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
4239
|
+
};
|
|
4240
|
+
} catch (error) {
|
|
4241
|
+
return this.handleError(action, error);
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
declaredFor(service) {
|
|
4245
|
+
return serviceCredentials[service] ?? [];
|
|
4246
|
+
}
|
|
4247
|
+
assertSettable(service, envVar) {
|
|
4248
|
+
if (service === "database") {
|
|
4249
|
+
if (!ENV_VAR_NAME.test(envVar)) {
|
|
4250
|
+
throw new Error(`"${envVar}" is not a well-formed connection reference (expected an env-var name)`);
|
|
4251
|
+
}
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
const declared = this.declaredFor(service);
|
|
4255
|
+
if (declared.length === 0) {
|
|
4256
|
+
throw new Error(`Service "${service}" declares no credentials`);
|
|
4257
|
+
}
|
|
4258
|
+
if (!declared.some((c) => c.envVar === envVar)) {
|
|
4259
|
+
const valid = declared.map((c) => c.envVar).join(", ");
|
|
4260
|
+
throw new Error(`"${envVar}" is not a declared credential of "${service}" (declared: ${valid})`);
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
list(serviceFilter) {
|
|
4264
|
+
const store = getInstalledCredentialStore();
|
|
4265
|
+
const stored = new Map((store?.entries() ?? []).map((e) => [`${e.service}\0${e.envVar}`, e]));
|
|
4266
|
+
const entries = [];
|
|
4267
|
+
for (const [service, declared] of Object.entries(serviceCredentials)) {
|
|
4268
|
+
if (serviceFilter && service !== serviceFilter) continue;
|
|
4269
|
+
for (const { envVar, required, description } of declared) {
|
|
4270
|
+
const fromStore = stored.get(`${service}\0${envVar}`);
|
|
4271
|
+
stored.delete(`${service}\0${envVar}`);
|
|
4272
|
+
const fromEnv = process.env[envVar];
|
|
4273
|
+
const source = fromStore ? "store" : fromEnv ? "env" : "none";
|
|
4274
|
+
entries.push({
|
|
4275
|
+
service,
|
|
4276
|
+
envVar,
|
|
4277
|
+
required,
|
|
4278
|
+
description,
|
|
4279
|
+
configured: source !== "none",
|
|
4280
|
+
source,
|
|
4281
|
+
last4: fromStore ? fromStore.last4 : fromEnv ? fromEnv.slice(-4) : ""
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
for (const e of stored.values()) {
|
|
4286
|
+
if (serviceFilter && e.service !== serviceFilter) continue;
|
|
4287
|
+
entries.push({
|
|
4288
|
+
service: e.service,
|
|
4289
|
+
envVar: e.envVar,
|
|
4290
|
+
required: false,
|
|
4291
|
+
description: "Stored connection reference",
|
|
4292
|
+
configured: true,
|
|
4293
|
+
source: "store",
|
|
4294
|
+
last4: e.last4
|
|
4295
|
+
});
|
|
4296
|
+
}
|
|
4297
|
+
return { enabled: store?.enabled ?? false, entries };
|
|
4298
|
+
}
|
|
4299
|
+
async set(service, envVar, value) {
|
|
4300
|
+
const store = getInstalledCredentialStore();
|
|
4301
|
+
if (!store) {
|
|
4302
|
+
throw new Error("No credential store is installed on this host \u2014 set credentials via the environment");
|
|
4303
|
+
}
|
|
4304
|
+
this.assertSettable(service, envVar);
|
|
4305
|
+
const entry = await store.set(service, envVar, value);
|
|
4306
|
+
return { saved: true, service, envVar, last4: entry.last4 };
|
|
4307
|
+
}
|
|
4308
|
+
async remove(service, envVar) {
|
|
4309
|
+
const store = getInstalledCredentialStore();
|
|
4310
|
+
if (!store) {
|
|
4311
|
+
throw new Error("No credential store is installed on this host");
|
|
4312
|
+
}
|
|
4313
|
+
this.assertSettable(service, envVar);
|
|
4314
|
+
return { removed: await store.remove(envVar) };
|
|
4315
|
+
}
|
|
4316
|
+
async test(service) {
|
|
4317
|
+
const declared = this.declaredFor(service);
|
|
4318
|
+
const missing = declared.filter((c) => c.required && !resolveCredentialRef(c.envVar)).map((c) => c.envVar);
|
|
4319
|
+
const configured = missing.length === 0;
|
|
4320
|
+
if (!configured) {
|
|
4321
|
+
return { service, configured, missing, probed: false, ok: false, message: `Missing required credentials: ${missing.join(", ")}` };
|
|
4322
|
+
}
|
|
4323
|
+
const factory = getActiveFactory();
|
|
4324
|
+
const probe = isProbeService(service) ? serviceProbes[service] : void 0;
|
|
4325
|
+
if (!factory || !probe) {
|
|
4326
|
+
return { service, configured, missing, probed: false, ok: true, message: "Credentials present (no live probe declared for this service)" };
|
|
4327
|
+
}
|
|
4328
|
+
if (!factory.isConfigured(service)) {
|
|
4329
|
+
return { service, configured, missing, probed: false, ok: false, message: "Credentials present but the service is not configured on this host \u2014 restart or re-save a credential" };
|
|
4330
|
+
}
|
|
4331
|
+
const result = await factory.execute(service, probe.action, probe.params);
|
|
4332
|
+
if (!result.success) {
|
|
4333
|
+
return { service, configured, missing, probed: true, ok: false, message: result.error?.message ?? `Probe ${probe.action} failed` };
|
|
4334
|
+
}
|
|
4335
|
+
const echoed = result.data !== null && typeof result.data === "object" && "_mock" in result.data;
|
|
4336
|
+
if (echoed) {
|
|
4337
|
+
return { service, configured, missing, probed: false, ok: false, message: "Probe was mock-echoed \u2014 the service is not actually configured" };
|
|
4338
|
+
}
|
|
4339
|
+
return { service, configured, missing, probed: true, ok: true, message: `Probe ${probe.action} succeeded` };
|
|
4340
|
+
}
|
|
4341
|
+
};
|
|
4342
|
+
registerIntegration("credentials", CredentialsIntegration);
|
|
4343
|
+
function isParamRecord2(value) {
|
|
4344
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
4345
|
+
}
|
|
4346
|
+
function toFilePayload(value) {
|
|
4347
|
+
if (!isParamRecord2(value)) return null;
|
|
4348
|
+
const { name, size, type, content } = value;
|
|
4349
|
+
if (typeof name !== "string") return null;
|
|
4350
|
+
return {
|
|
4351
|
+
name,
|
|
4352
|
+
size: typeof size === "number" ? size : 0,
|
|
4353
|
+
type: typeof type === "string" ? type : "application/octet-stream",
|
|
4354
|
+
content: typeof content === "string" ? content : void 0
|
|
4355
|
+
};
|
|
4356
|
+
}
|
|
4357
|
+
function decodeContent2(content) {
|
|
4358
|
+
const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
|
|
4359
|
+
if (dataUrlMatch) {
|
|
4360
|
+
const [, mime, isB64, body] = dataUrlMatch;
|
|
4361
|
+
const bytes = isB64 ? Buffer.from(body, "base64") : Buffer.from(decodeURIComponent(body), "utf8");
|
|
4362
|
+
return { bytes, contentType: mime || null };
|
|
4363
|
+
}
|
|
4364
|
+
return { bytes: Buffer.from(content, "utf8"), contentType: null };
|
|
4365
|
+
}
|
|
2839
4366
|
var StorageIntegration = class extends BaseIntegration {
|
|
2840
4367
|
constructor(config) {
|
|
2841
4368
|
super(config);
|
|
2842
4369
|
this.objects = /* @__PURE__ */ new Map();
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
4370
|
+
this.s3 = null;
|
|
4371
|
+
this.defaultBucket = config.env.STORAGE_BUCKET || "";
|
|
4372
|
+
this.publicUrlBase = config.env.STORAGE_PUBLIC_URL_BASE || "";
|
|
4373
|
+
const accessKeyId = config.env.STORAGE_ACCESS_KEY_ID || "";
|
|
4374
|
+
const secretAccessKey = config.env.STORAGE_SECRET_ACCESS_KEY || "";
|
|
4375
|
+
if (accessKeyId && secretAccessKey) {
|
|
4376
|
+
const endpoint = config.env.STORAGE_ENDPOINT || void 0;
|
|
4377
|
+
this.s3 = new S3Client({
|
|
4378
|
+
region: config.env.STORAGE_REGION || "us-east-1",
|
|
4379
|
+
endpoint,
|
|
4380
|
+
// Path-style is what MinIO/R2-style endpoints expect.
|
|
4381
|
+
forcePathStyle: Boolean(endpoint),
|
|
4382
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
4383
|
+
});
|
|
4384
|
+
this.logger.info("Storage integration initialized (S3 backend)", {
|
|
4385
|
+
endpoint: endpoint ?? "aws",
|
|
4386
|
+
bucket: this.defaultBucket
|
|
4387
|
+
});
|
|
4388
|
+
} else {
|
|
4389
|
+
if (process.env.NODE_ENV === "production") {
|
|
4390
|
+
throw new Error(
|
|
4391
|
+
"Storage credentials missing in production (STORAGE_ACCESS_KEY_ID / STORAGE_SECRET_ACCESS_KEY) \u2014 refusing the in-memory fallback. See SECRETS.md."
|
|
4392
|
+
);
|
|
4393
|
+
}
|
|
4394
|
+
this.logger.warn("Storage integration initialized (in-memory backend \u2014 dev only, nothing persists)");
|
|
2849
4395
|
}
|
|
2850
|
-
this.logger.info("Storage integration initialized (in-memory backend)");
|
|
2851
4396
|
}
|
|
2852
4397
|
async execute(action, params) {
|
|
2853
4398
|
const validation = this.validateParams(action, params);
|
|
@@ -2897,107 +4442,182 @@ var StorageIntegration = class extends BaseIntegration {
|
|
|
2897
4442
|
// ---------------------------------------------------------------------------
|
|
2898
4443
|
// Helpers
|
|
2899
4444
|
// ---------------------------------------------------------------------------
|
|
2900
|
-
|
|
4445
|
+
bucketOf(params) {
|
|
4446
|
+
return params.bucket || this.defaultBucket;
|
|
4447
|
+
}
|
|
2901
4448
|
compositeKey(bucket, key) {
|
|
2902
4449
|
return `${bucket}/${key}`;
|
|
2903
4450
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
4451
|
+
generateEtag(bytes) {
|
|
4452
|
+
return `"${createHash("md5").update(bytes).digest("hex")}"`;
|
|
4453
|
+
}
|
|
4454
|
+
/** Resolve the upload inputs from either admitted shape. */
|
|
4455
|
+
resolveUpload(params) {
|
|
4456
|
+
const file = toFilePayload(params.file);
|
|
4457
|
+
if (file) {
|
|
4458
|
+
const maxSize = typeof params.maxSize === "number" ? params.maxSize : 0;
|
|
4459
|
+
if (maxSize > 0 && file.size > maxSize) {
|
|
4460
|
+
throw new Error(`Upload rejected: ${file.name} is ${file.size} bytes (max ${maxSize})`);
|
|
4461
|
+
}
|
|
4462
|
+
if (!file.content) {
|
|
4463
|
+
throw new Error(
|
|
4464
|
+
`Upload rejected: file payload for '${file.name}' carries no content \u2014 the uploader must include the base64 data URL`
|
|
4465
|
+
);
|
|
4466
|
+
}
|
|
4467
|
+
const { bytes: bytes2, contentType: contentType2 } = decodeContent2(file.content);
|
|
4468
|
+
const safeName = file.name.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
4469
|
+
return {
|
|
4470
|
+
key: `${Date.now()}-${safeName}`,
|
|
4471
|
+
bytes: bytes2,
|
|
4472
|
+
contentType: contentType2 ?? file.type,
|
|
4473
|
+
acl: params.acl === "public" ? "public-read" : void 0
|
|
4474
|
+
};
|
|
4475
|
+
}
|
|
4476
|
+
const key = params.key;
|
|
4477
|
+
const content = params.content;
|
|
4478
|
+
if (!key || content === void 0 || content === null) {
|
|
4479
|
+
throw new Error("upload requires either `file` (with content) or the `key` + `content` pair");
|
|
2911
4480
|
}
|
|
2912
|
-
|
|
4481
|
+
const raw = typeof content === "string" ? content : JSON.stringify(content);
|
|
4482
|
+
const { bytes, contentType } = decodeContent2(raw);
|
|
4483
|
+
return {
|
|
4484
|
+
key,
|
|
4485
|
+
bytes,
|
|
4486
|
+
contentType: params.contentType || contentType || "application/octet-stream",
|
|
4487
|
+
acl: params.acl === "public" ? "public-read" : void 0
|
|
4488
|
+
};
|
|
2913
4489
|
}
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
4490
|
+
publicUrl(bucket, key) {
|
|
4491
|
+
if (this.publicUrlBase) {
|
|
4492
|
+
return `${this.publicUrlBase.replace(/\/$/, "")}/${key}`;
|
|
4493
|
+
}
|
|
4494
|
+
const endpoint = this.config.env.STORAGE_ENDPOINT;
|
|
4495
|
+
if (endpoint) {
|
|
4496
|
+
return `${endpoint.replace(/\/$/, "")}/${bucket}/${key}`;
|
|
2918
4497
|
}
|
|
2919
|
-
|
|
4498
|
+
const region = this.config.env.STORAGE_REGION || "us-east-1";
|
|
4499
|
+
return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
2920
4500
|
}
|
|
2921
4501
|
// ---------------------------------------------------------------------------
|
|
2922
4502
|
// Actions
|
|
2923
4503
|
// ---------------------------------------------------------------------------
|
|
2924
4504
|
async upload(params) {
|
|
2925
|
-
const bucket = params
|
|
2926
|
-
const key = params
|
|
2927
|
-
const
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
4505
|
+
const bucket = this.bucketOf(params);
|
|
4506
|
+
const { key, bytes, contentType, acl } = this.resolveUpload(params);
|
|
4507
|
+
const etag = this.generateEtag(bytes);
|
|
4508
|
+
this.logger.debug("Storage UPLOAD", { bucket, key, contentType, size: bytes.length });
|
|
4509
|
+
if (this.s3) {
|
|
4510
|
+
await this.s3.send(
|
|
4511
|
+
new PutObjectCommand({
|
|
4512
|
+
Bucket: bucket,
|
|
4513
|
+
Key: key,
|
|
4514
|
+
Body: bytes,
|
|
4515
|
+
ContentType: contentType,
|
|
4516
|
+
ACL: acl
|
|
4517
|
+
})
|
|
4518
|
+
);
|
|
4519
|
+
} else {
|
|
4520
|
+
this.objects.set(this.compositeKey(bucket, key), {
|
|
4521
|
+
content: bytes.toString("base64"),
|
|
4522
|
+
contentType,
|
|
4523
|
+
size: bytes.length,
|
|
4524
|
+
metadata: params.metadata ?? {},
|
|
4525
|
+
lastModified: Date.now(),
|
|
4526
|
+
etag
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
const url = acl === "public-read" ? this.publicUrl(bucket, key) : (await this.signUrl(bucket, key, "get", 3600)).url;
|
|
4530
|
+
return { key, bucket, size: bytes.length, etag, id: key, url };
|
|
2943
4531
|
}
|
|
2944
4532
|
async download(params) {
|
|
2945
|
-
const bucket = params
|
|
4533
|
+
const bucket = this.bucketOf(params);
|
|
2946
4534
|
const key = params.key;
|
|
2947
4535
|
this.logger.debug("Storage DOWNLOAD", { bucket, key });
|
|
4536
|
+
if (this.s3) {
|
|
4537
|
+
const response = await this.s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
4538
|
+
const bytes = Buffer.from(await response.Body.transformToByteArray());
|
|
4539
|
+
return {
|
|
4540
|
+
content: bytes.toString("base64"),
|
|
4541
|
+
contentType: response.ContentType ?? "application/octet-stream",
|
|
4542
|
+
size: bytes.length,
|
|
4543
|
+
metadata: {}
|
|
4544
|
+
};
|
|
4545
|
+
}
|
|
2948
4546
|
const obj = this.objects.get(this.compositeKey(bucket, key));
|
|
2949
4547
|
if (!obj) {
|
|
2950
4548
|
throw new Error(`Object not found: ${bucket}/${key}`);
|
|
2951
4549
|
}
|
|
2952
4550
|
return {
|
|
2953
|
-
content: obj.content,
|
|
4551
|
+
content: String(obj.content),
|
|
2954
4552
|
contentType: obj.contentType,
|
|
2955
4553
|
size: obj.size,
|
|
2956
4554
|
metadata: obj.metadata
|
|
2957
4555
|
};
|
|
2958
4556
|
}
|
|
2959
4557
|
async list(params) {
|
|
2960
|
-
const bucket = params
|
|
4558
|
+
const bucket = this.bucketOf(params);
|
|
2961
4559
|
const prefix = params.prefix ?? "";
|
|
2962
4560
|
const maxKeys = params.maxKeys ?? 1e3;
|
|
2963
4561
|
this.logger.debug("Storage LIST", { bucket, prefix, maxKeys });
|
|
4562
|
+
if (this.s3) {
|
|
4563
|
+
const response = await this.s3.send(
|
|
4564
|
+
new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix || void 0, MaxKeys: maxKeys })
|
|
4565
|
+
);
|
|
4566
|
+
return {
|
|
4567
|
+
keys: (response.Contents ?? []).map((entry) => ({
|
|
4568
|
+
key: entry.Key ?? "",
|
|
4569
|
+
size: entry.Size ?? 0,
|
|
4570
|
+
lastModified: entry.LastModified?.getTime() ?? 0
|
|
4571
|
+
})),
|
|
4572
|
+
// Continuation tokens are unrepresentable in the result type (ledger
|
|
4573
|
+
// I-11) — surface the clamp honestly.
|
|
4574
|
+
truncated: Boolean(response.IsTruncated)
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
2964
4577
|
const bucketPrefix = `${bucket}/`;
|
|
2965
4578
|
const fullPrefix = `${bucket}/${prefix}`;
|
|
2966
4579
|
const results = [];
|
|
2967
4580
|
for (const [compositeKey, obj] of this.objects) {
|
|
2968
4581
|
if (!compositeKey.startsWith(fullPrefix)) continue;
|
|
2969
|
-
const objectKey = compositeKey.slice(bucketPrefix.length);
|
|
2970
4582
|
results.push({
|
|
2971
|
-
key:
|
|
4583
|
+
key: compositeKey.slice(bucketPrefix.length),
|
|
2972
4584
|
size: obj.size,
|
|
2973
4585
|
lastModified: obj.lastModified
|
|
2974
4586
|
});
|
|
2975
4587
|
}
|
|
2976
4588
|
results.sort((a, b) => a.key.localeCompare(b.key));
|
|
2977
|
-
|
|
2978
|
-
return {
|
|
2979
|
-
keys: results.slice(0, maxKeys),
|
|
2980
|
-
truncated
|
|
2981
|
-
};
|
|
4589
|
+
return { keys: results.slice(0, maxKeys), truncated: results.length > maxKeys };
|
|
2982
4590
|
}
|
|
2983
4591
|
async deleteObject(params) {
|
|
2984
|
-
const bucket = params
|
|
4592
|
+
const bucket = this.bucketOf(params);
|
|
2985
4593
|
const key = params.key;
|
|
2986
4594
|
this.logger.debug("Storage DELETE", { bucket, key });
|
|
4595
|
+
if (this.s3) {
|
|
4596
|
+
await this.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
4597
|
+
return { deleted: true };
|
|
4598
|
+
}
|
|
2987
4599
|
const existed = this.objects.has(this.compositeKey(bucket, key));
|
|
2988
4600
|
this.objects.delete(this.compositeKey(bucket, key));
|
|
2989
4601
|
return { deleted: existed };
|
|
2990
4602
|
}
|
|
4603
|
+
async signUrl(bucket, key, operation, expiresIn) {
|
|
4604
|
+
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
4605
|
+
if (this.s3) {
|
|
4606
|
+
const command = operation === "put" ? new PutObjectCommand({ Bucket: bucket, Key: key }) : new GetObjectCommand({ Bucket: bucket, Key: key });
|
|
4607
|
+
const url2 = await getSignedUrl(this.s3, command, { expiresIn });
|
|
4608
|
+
return { url: url2, expiresAt };
|
|
4609
|
+
}
|
|
4610
|
+
const token = createHash("sha256").update(`${bucket}/${key}/${expiresAt}`).digest("hex").slice(0, 16);
|
|
4611
|
+
const url = `https://storage.mock.local/${bucket}/${key}?X-Amz-Algorithm=MOCK-HMAC-SHA256&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host&X-Amz-Signature=${token}&operation=${operation}`;
|
|
4612
|
+
return { url, expiresAt };
|
|
4613
|
+
}
|
|
2991
4614
|
async getSignedUrl(params) {
|
|
2992
|
-
const bucket = params
|
|
4615
|
+
const bucket = this.bucketOf(params);
|
|
2993
4616
|
const key = params.key;
|
|
2994
4617
|
const expiresIn = params.expiresIn ?? 3600;
|
|
2995
4618
|
const operation = params.operation ?? "get";
|
|
2996
4619
|
this.logger.debug("Storage GET_SIGNED_URL", { bucket, key, expiresIn, operation });
|
|
2997
|
-
|
|
2998
|
-
const token = Math.random().toString(36).slice(2, 18);
|
|
2999
|
-
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}`;
|
|
3000
|
-
return { url, expiresAt };
|
|
4620
|
+
return this.signUrl(bucket, key, operation, expiresIn);
|
|
3001
4621
|
}
|
|
3002
4622
|
};
|
|
3003
4623
|
registerIntegration("storage", StorageIntegration);
|
|
@@ -3504,10 +5124,10 @@ var DatabaseIntegration = class extends BaseIntegration {
|
|
|
3504
5124
|
}
|
|
3505
5125
|
/** Resolve (and cache) the driver for a connection reference. */
|
|
3506
5126
|
driverFor(connectionRef) {
|
|
3507
|
-
const connectionString =
|
|
5127
|
+
const connectionString = resolveCredentialRef(connectionRef);
|
|
3508
5128
|
if (!connectionString) {
|
|
3509
5129
|
throw new IntegrationError(
|
|
3510
|
-
`Connection reference "${connectionRef}" is not set in the environment`,
|
|
5130
|
+
`Connection reference "${connectionRef}" is not set in the credential store or environment`,
|
|
3511
5131
|
"AUTH_ERROR"
|
|
3512
5132
|
);
|
|
3513
5133
|
}
|
|
@@ -3759,6 +5379,6 @@ var ArxivIntegration = class extends BaseIntegration {
|
|
|
3759
5379
|
};
|
|
3760
5380
|
registerIntegration("arxiv", ArxivIntegration);
|
|
3761
5381
|
|
|
3762
|
-
export { ArxivIntegration, BaseIntegration, CLIIntegration, ConsoleLogger, DatabaseIntegration, DeepAgentIntegration, DockerIntegration, EmailIntegration, GitHubIntegration, IconifyIntegration, IntegrationError, IntegrationFactory, LLMIntegration, MLIntegration, OAuthIntegration, OtelIntegration, QueueIntegration, RedisIntegration, StorageIntegration, StripeIntegration, TwilioIntegration, WebhookIntegration, WikimediaIntegration, YouTubeIntegration, assertReadOnlySelect, getIntegration, getIntegrationFactory, getRegisteredIntegrations, isKnownIntegration, registerIntegration, resetIntegrationFactory, validateParams, verifyAndParseStripeEvent, withRetry };
|
|
5382
|
+
export { AccountingIntegration, ArxivIntegration, BankingIntegration, BaseIntegration, CLIIntegration, CREDENTIAL_ENTITY_TYPE, CREDENTIAL_MASTER_KEY_ENV, CalendarIntegration, ConsoleLogger, CredentialStore, CredentialsIntegration, DatabaseIntegration, DeepAgentIntegration, DockerIntegration, DriveIntegration, EmailIntegration, EsignIntegration, GitHubIntegration, IconifyIntegration, IntegrationError, IntegrationFactory, LLMIntegration, MLIntegration, MetaAdsIntegration, OAuthIntegration, OtelIntegration, PushIntegration, QueueIntegration, RedisIntegration, StorageIntegration, StripeIntegration, TwilioIntegration, WebhookIntegration, WikimediaIntegration, YouTubeIntegration, assertReadOnlySelect, getActiveFactory, getInstalledCredentialStore, getIntegration, getIntegrationFactory, getRegisteredIntegrations, googleCalendarHookProvider, installActiveFactory, installCredentialStore, isKnownIntegration, parseCalendarPushNotification, registerIntegration, resetIntegrationFactory, resolveCredentialRef, uninstallCredentialStore, validateParams, verifyAndParseStripeEvent, withRetry };
|
|
3763
5383
|
//# sourceMappingURL=index.js.map
|
|
3764
5384
|
//# sourceMappingURL=index.js.map
|