@almadar/integrations 2.24.0 → 2.26.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/{BaseIntegration-MA-b4fh8.d.ts → BaseIntegration-C_5q54DM.d.ts} +64 -3
- package/dist/index.d.ts +377 -22
- package/dist/index.js +2091 -164
- package/dist/index.js.map +1 -1
- package/dist/integrations/github/index.d.ts +1 -1
- package/dist/integrations/github/index.js +11 -0
- package/dist/integrations/github/index.js.map +1 -1
- package/dist/mocks/index.d.ts +1 -2
- package/dist/mocks/index.js +51 -16
- package/dist/mocks/index.js.map +1 -1
- package/dist/runtime/index.d.ts +35 -6
- package/dist/runtime/index.js +1877 -135
- package/dist/runtime/index.js.map +1 -1
- package/dist/{contracts-Dv9PM_Cz.d.ts → store-tehIm2rt.d.ts} +541 -6
- package/package.json +12 -7
- package/dist/factory-DTdVeyAi.d.ts +0 -41
package/dist/runtime/index.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { createLogger } from '@almadar/logger';
|
|
2
2
|
import { integratorsRegistry } from '@almadar/core/patterns';
|
|
3
|
+
import { createHmac, createHash } from 'crypto';
|
|
4
|
+
import { mkdtempSync, writeFileSync, rmSync, promises } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
3
6
|
import Stripe2 from 'stripe';
|
|
4
7
|
import { google } from 'googleapis';
|
|
5
8
|
import twilio from 'twilio';
|
|
6
9
|
import sgMail from '@sendgrid/mail';
|
|
7
10
|
import { Resend } from 'resend';
|
|
8
|
-
import
|
|
11
|
+
import webpush from 'web-push';
|
|
12
|
+
import { Readable } from 'stream';
|
|
9
13
|
import { getAvailableProvider, LLMClient, EmbeddingClient } from '@almadar/llm';
|
|
10
14
|
import { z } from 'zod';
|
|
11
15
|
import { execSync, spawn } from 'child_process';
|
|
12
|
-
import { mkdtempSync, writeFileSync, rmSync, promises } from 'fs';
|
|
13
|
-
import { join } from 'path';
|
|
14
16
|
import { tmpdir } from 'os';
|
|
17
|
+
import * as oidc from 'openid-client';
|
|
18
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
|
19
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
15
20
|
import { Pool } from 'pg';
|
|
16
21
|
import { createRequire } from 'module';
|
|
17
22
|
|
|
@@ -34,6 +39,133 @@ var IntegrationError = class extends Error {
|
|
|
34
39
|
};
|
|
35
40
|
}
|
|
36
41
|
};
|
|
42
|
+
|
|
43
|
+
// src/contracts.ts
|
|
44
|
+
var serviceCredentials = {
|
|
45
|
+
stripe: [
|
|
46
|
+
{ envVar: "STRIPE_SECRET_KEY", required: true, description: "Stripe secret API key" },
|
|
47
|
+
{ envVar: "STRIPE_WEBHOOK_SECRET", required: false, description: "Webhook endpoint signing secret" }
|
|
48
|
+
],
|
|
49
|
+
youtube: [
|
|
50
|
+
{ envVar: "YOUTUBE_API_KEY", required: true, description: "YouTube Data API key" }
|
|
51
|
+
],
|
|
52
|
+
twilio: [
|
|
53
|
+
{ envVar: "TWILIO_ACCOUNT_SID", required: true, description: "Twilio account SID" },
|
|
54
|
+
{ envVar: "TWILIO_AUTH_TOKEN", required: true, description: "Twilio auth token" },
|
|
55
|
+
{ envVar: "TWILIO_PHONE_NUMBER", required: false, description: "Default sender phone number" }
|
|
56
|
+
],
|
|
57
|
+
email: [
|
|
58
|
+
{ envVar: "SENDGRID_API_KEY", required: false, description: "SendGrid API key (use this OR RESEND_API_KEY)" },
|
|
59
|
+
{ envVar: "RESEND_API_KEY", required: false, description: "Resend API key (use this OR SENDGRID_API_KEY)" },
|
|
60
|
+
{ envVar: "FROM_EMAIL", required: false, description: "Default sender email address" }
|
|
61
|
+
],
|
|
62
|
+
webhook: [
|
|
63
|
+
{ envVar: "WEBHOOK_SIGNING_SECRET", required: false, description: "Default HMAC-SHA256 signing secret (per-call secret overrides)" },
|
|
64
|
+
{ envVar: "WEBHOOK_TIMEOUT_MS", required: false, description: "Per-request timeout (ms, default 10000)" }
|
|
65
|
+
],
|
|
66
|
+
push: [
|
|
67
|
+
{ envVar: "VAPID_PUBLIC_KEY", required: true, description: "VAPID public key (web-push generate-vapid-keys)" },
|
|
68
|
+
{ envVar: "VAPID_PRIVATE_KEY", required: true, description: "VAPID private key" },
|
|
69
|
+
{ envVar: "VAPID_SUBJECT", required: true, description: "VAPID subject (mailto: or https: contact URI)" }
|
|
70
|
+
],
|
|
71
|
+
calendar: [
|
|
72
|
+
{ 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" },
|
|
73
|
+
{ envVar: "GOOGLE_CALENDAR_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" },
|
|
74
|
+
{ envVar: "GOOGLE_CALENDAR_ID", required: false, description: "Default calendar id (default: primary)" },
|
|
75
|
+
{ envVar: "GOOGLE_CALENDAR_CHANNEL_TOKEN", required: false, description: "Watch-channel verification token \u2014 required to receive inbound calendar hooks (two-way sync)" }
|
|
76
|
+
],
|
|
77
|
+
drive: [
|
|
78
|
+
{ envVar: "GOOGLE_DRIVE_SA_KEY", required: false, description: "Google service-account key JSON (raw or base64) with drive scope \u2014 serves reads; store in Secret Manager, bind as env" },
|
|
79
|
+
{ envVar: "GOOGLE_DRIVE_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" },
|
|
80
|
+
{ envVar: "GOOGLE_DRIVE_REFRESH_TOKEN", required: false, description: "User OAuth refresh token (drive-consent.mjs) \u2014 serves writes; SA uploads are impossible on personal accounts (no SA storage quota)" },
|
|
81
|
+
{ envVar: "GOOGLE_DRIVE_FOLDER_ID", required: false, description: "Default parent folder for uploads/new folders when the call names none" }
|
|
82
|
+
],
|
|
83
|
+
metaAds: [
|
|
84
|
+
{ envVar: "META_ACCESS_TOKEN", required: true, description: "Meta Graph API access token (Marketing API, ads_read)" },
|
|
85
|
+
{ envVar: "META_AD_ACCOUNT_ID", required: false, description: "Default ad account id (act_\u2026); per-call accountId overrides" }
|
|
86
|
+
],
|
|
87
|
+
accounting: [],
|
|
88
|
+
banking: [
|
|
89
|
+
{ envVar: "GOCARDLESS_SECRET_ID", required: true, description: "GoCardless Bank Account Data secret id" },
|
|
90
|
+
{ envVar: "GOCARDLESS_SECRET_KEY", required: true, description: "GoCardless Bank Account Data secret key" }
|
|
91
|
+
],
|
|
92
|
+
esign: [
|
|
93
|
+
{ envVar: "DOCUSIGN_BASE_URL", required: true, description: "DocuSign REST base (e.g. https://demo.docusign.net/restapi/v2.1/accounts/<accountId>)" },
|
|
94
|
+
{ envVar: "DOCUSIGN_ACCESS_TOKEN", required: true, description: "DocuSign OAuth access token (JWT grant rotation is the deployment concern)" }
|
|
95
|
+
],
|
|
96
|
+
llm: [
|
|
97
|
+
{ envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key (use this OR OPENAI_API_KEY)" },
|
|
98
|
+
{ envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key (use this OR ANTHROPIC_API_KEY)" }
|
|
99
|
+
],
|
|
100
|
+
"llm-integration": [
|
|
101
|
+
{ envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key" },
|
|
102
|
+
{ envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key" }
|
|
103
|
+
],
|
|
104
|
+
ml: [
|
|
105
|
+
{ envVar: "MASAR_URL", required: true, description: "Base URL of the deployed model-serving orbital" },
|
|
106
|
+
{ envVar: "MASAR_ML_TRAIT", required: true, description: "Kebab-case trait name the serving orbital exposes its /events route under" }
|
|
107
|
+
],
|
|
108
|
+
deepagent: [
|
|
109
|
+
{ envVar: "DEEPAGENT_API_URL", required: true, description: "DeepAgent server URL" },
|
|
110
|
+
{ envVar: "DEEPAGENT_API_KEY", required: false, description: "DeepAgent API key" }
|
|
111
|
+
],
|
|
112
|
+
github: [
|
|
113
|
+
{ envVar: "GITHUB_TOKEN", required: true, description: "GitHub personal access token" },
|
|
114
|
+
{ envVar: "GITHUB_OWNER", required: false, description: "Default repository owner" },
|
|
115
|
+
{ envVar: "GITHUB_REPO", required: false, description: "Default repository name" }
|
|
116
|
+
],
|
|
117
|
+
docker: [
|
|
118
|
+
{ envVar: "DOCKER_HOST", required: false, description: "Docker daemon host URL" }
|
|
119
|
+
],
|
|
120
|
+
storage: [
|
|
121
|
+
{ envVar: "STORAGE_ACCESS_KEY_ID", required: true, description: "S3-compatible access key id" },
|
|
122
|
+
{ envVar: "STORAGE_SECRET_ACCESS_KEY", required: true, description: "S3-compatible secret access key" },
|
|
123
|
+
{ envVar: "STORAGE_BUCKET", required: true, description: "Default bucket name" },
|
|
124
|
+
{ envVar: "STORAGE_REGION", required: false, description: "Region (default us-east-1)" },
|
|
125
|
+
{ envVar: "STORAGE_ENDPOINT", required: false, description: "Custom S3-compatible endpoint (R2/MinIO/\u2026); empty = AWS S3" },
|
|
126
|
+
{ envVar: "STORAGE_PUBLIC_URL_BASE", required: false, description: "Base URL for public-acl object links; empty = endpoint-derived" }
|
|
127
|
+
],
|
|
128
|
+
queue: [
|
|
129
|
+
{ envVar: "QUEUE_URL", required: true, description: "Message queue connection URL" }
|
|
130
|
+
],
|
|
131
|
+
redis: [
|
|
132
|
+
{ envVar: "REDIS_URL", required: true, description: "Redis connection URL" }
|
|
133
|
+
],
|
|
134
|
+
oauth: [
|
|
135
|
+
{ envVar: "OAUTH_CLIENT_ID", required: true, description: "OIDC client ID" },
|
|
136
|
+
{ envVar: "OAUTH_CLIENT_SECRET", required: true, description: "OIDC client secret" },
|
|
137
|
+
{ envVar: "OAUTH_REDIRECT_URI", required: false, description: "OIDC redirect URI (default per-call param)" },
|
|
138
|
+
{ envVar: "OIDC_ISSUER_URL", required: false, description: "OIDC issuer URL (default https://accounts.google.com)" }
|
|
139
|
+
],
|
|
140
|
+
credentials: [
|
|
141
|
+
{ 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" },
|
|
142
|
+
{ envVar: "ALMADAR_CREDENTIAL_MASTER_KEY_PREVIOUS", required: false, description: "Previous master key, present only during a rotation window \u2014 decrypts old rows until `credentials.rotate` re-encrypts them" }
|
|
143
|
+
],
|
|
144
|
+
otel: [
|
|
145
|
+
{ envVar: "OTEL_EXPORTER_OTLP_ENDPOINT", required: true, description: "OpenTelemetry collector endpoint" },
|
|
146
|
+
{ envVar: "OTEL_SERVICE_NAME", required: false, description: "Service name for traces" }
|
|
147
|
+
],
|
|
148
|
+
cli: [],
|
|
149
|
+
// No fixed env vars — connection strings are resolved per-query from the
|
|
150
|
+
// caller-supplied connectionRef, so credentials cannot be declared statically.
|
|
151
|
+
database: [],
|
|
152
|
+
wikimedia: [
|
|
153
|
+
{ envVar: "WIKIMEDIA_USER_AGENT", required: false, description: "Descriptive User-Agent for the Wikipedia API" },
|
|
154
|
+
{ envVar: "WIKIMEDIA_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
|
|
155
|
+
],
|
|
156
|
+
iconify: [
|
|
157
|
+
{ envVar: "ICONIFY_USER_AGENT", required: false, description: "User-Agent for the Iconify API" },
|
|
158
|
+
{ envVar: "ICONIFY_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
|
|
159
|
+
],
|
|
160
|
+
arxiv: [
|
|
161
|
+
{ envVar: "ARXIV_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
|
|
162
|
+
]
|
|
163
|
+
};
|
|
164
|
+
var serviceProbes = {
|
|
165
|
+
calendar: { action: "listEvents", params: { maxResults: 1 } },
|
|
166
|
+
drive: { action: "listFiles", params: { maxResults: 1 } },
|
|
167
|
+
metaAds: { action: "listCampaigns", params: {} }
|
|
168
|
+
};
|
|
37
169
|
var ConsoleLogger = class {
|
|
38
170
|
constructor(_level = "info") {
|
|
39
171
|
this.log = createLogger("almadar:integrations");
|
|
@@ -51,6 +183,7 @@ var ConsoleLogger = class {
|
|
|
51
183
|
this.log.error(message, meta);
|
|
52
184
|
}
|
|
53
185
|
};
|
|
186
|
+
var RESERVED_ENVELOPE_KEYS = /* @__PURE__ */ new Set(["emit", "onSuccess", "onError", "timeout"]);
|
|
54
187
|
function validateParams(integration, action, params) {
|
|
55
188
|
const typedRegistry = integratorsRegistry;
|
|
56
189
|
const registry = typedRegistry.integrators[integration];
|
|
@@ -73,6 +206,16 @@ function validateParams(integration, action, params) {
|
|
|
73
206
|
};
|
|
74
207
|
}
|
|
75
208
|
const errors = [];
|
|
209
|
+
const declaredNames = new Set(actionDef.params.map((p) => p.name));
|
|
210
|
+
for (const key of Object.keys(params)) {
|
|
211
|
+
if (RESERVED_ENVELOPE_KEYS.has(key)) continue;
|
|
212
|
+
if (!declaredNames.has(key)) {
|
|
213
|
+
errors.push({
|
|
214
|
+
param: key,
|
|
215
|
+
message: `Unknown parameter: ${key} (declared: ${[...declaredNames].sort().join(", ")})`
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
76
219
|
for (const paramDef of actionDef.params) {
|
|
77
220
|
if (paramDef.required && !(paramDef.name in params)) {
|
|
78
221
|
errors.push({
|
|
@@ -217,56 +360,80 @@ function getIntegration(name) {
|
|
|
217
360
|
}
|
|
218
361
|
|
|
219
362
|
// src/factory.ts
|
|
363
|
+
function instanceKey(name, principal) {
|
|
364
|
+
return principal ? `${name}\0${principal}` : name;
|
|
365
|
+
}
|
|
220
366
|
var IntegrationFactory = class {
|
|
221
367
|
constructor() {
|
|
222
368
|
this.instances = /* @__PURE__ */ new Map();
|
|
223
369
|
this.configs = /* @__PURE__ */ new Map();
|
|
224
370
|
}
|
|
225
371
|
/**
|
|
226
|
-
* Configure an integration (doesn't instantiate yet)
|
|
372
|
+
* Configure an integration (doesn't instantiate yet). A `principal` scopes
|
|
373
|
+
* the config to that principal; the app-wide config (no principal) is the
|
|
374
|
+
* fallback for every principal.
|
|
227
375
|
*/
|
|
228
|
-
configure(name, config) {
|
|
229
|
-
this.configs.set(name, { name, ...config });
|
|
376
|
+
configure(name, config, principal) {
|
|
377
|
+
this.configs.set(instanceKey(name, principal), { name, ...config });
|
|
230
378
|
}
|
|
231
379
|
/**
|
|
232
|
-
* Get or create an integration instance
|
|
380
|
+
* Get or create an integration instance. Principal-scoped lookups fall
|
|
381
|
+
* back to the app-wide config when no per-principal config exists.
|
|
233
382
|
*/
|
|
234
|
-
get(name) {
|
|
235
|
-
|
|
236
|
-
|
|
383
|
+
get(name, principal) {
|
|
384
|
+
const key = instanceKey(name, principal);
|
|
385
|
+
const cached = this.instances.get(key);
|
|
386
|
+
if (cached) {
|
|
387
|
+
return cached;
|
|
237
388
|
}
|
|
238
389
|
const Constructor = getIntegration(name);
|
|
239
390
|
if (!Constructor) {
|
|
240
391
|
throw new Error(`Unknown integration: ${name}. Make sure it's imported.`);
|
|
241
392
|
}
|
|
242
|
-
const config = this.configs.get(name);
|
|
393
|
+
const config = this.configs.get(key) ?? this.configs.get(name);
|
|
243
394
|
if (!config) {
|
|
244
395
|
throw new Error(
|
|
245
396
|
`Integration not configured: ${name}. Call configure() first.`
|
|
246
397
|
);
|
|
247
398
|
}
|
|
248
399
|
const instance = new Constructor(config);
|
|
249
|
-
this.instances.set(
|
|
400
|
+
this.instances.set(key, instance);
|
|
250
401
|
return instance;
|
|
251
402
|
}
|
|
252
403
|
/**
|
|
253
404
|
* Execute an action on an integration
|
|
254
405
|
*/
|
|
255
|
-
async execute(integration, action, params) {
|
|
256
|
-
const instance = this.get(integration);
|
|
257
|
-
return await instance.execute(action, params);
|
|
406
|
+
async execute(integration, action, params, context) {
|
|
407
|
+
const instance = this.get(integration, context?.principal);
|
|
408
|
+
return await instance.execute(action, params, context);
|
|
258
409
|
}
|
|
259
410
|
/**
|
|
260
411
|
* Check if integration is configured
|
|
261
412
|
*/
|
|
262
|
-
isConfigured(name) {
|
|
263
|
-
return this.configs.has(name);
|
|
413
|
+
isConfigured(name, principal) {
|
|
414
|
+
return this.configs.has(instanceKey(name, principal)) || this.configs.has(name);
|
|
264
415
|
}
|
|
265
416
|
/**
|
|
266
417
|
* Register an integration instance directly (used by mock infrastructure)
|
|
267
418
|
*/
|
|
268
|
-
registerInstance(name, instance) {
|
|
269
|
-
this.instances.set(name, instance);
|
|
419
|
+
registerInstance(name, instance, principal) {
|
|
420
|
+
this.instances.set(instanceKey(name, principal), instance);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Drop the cached instance(s) for a name so the next `get` rebuilds from
|
|
424
|
+
* the current config — how a credential change goes live without restart.
|
|
425
|
+
* Configs are kept; without a name, every instance is dropped.
|
|
426
|
+
*/
|
|
427
|
+
invalidate(name) {
|
|
428
|
+
if (name === void 0) {
|
|
429
|
+
this.instances.clear();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
for (const key of this.instances.keys()) {
|
|
433
|
+
if (key === name || key.startsWith(`${name}\0`)) {
|
|
434
|
+
this.instances.delete(key);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
270
437
|
}
|
|
271
438
|
/**
|
|
272
439
|
* Clear all instances (useful for testing)
|
|
@@ -283,6 +450,26 @@ var IntegrationFactory = class {
|
|
|
283
450
|
}
|
|
284
451
|
};
|
|
285
452
|
|
|
453
|
+
// src/credentials/resolver.ts
|
|
454
|
+
var installedStore = null;
|
|
455
|
+
var activeFactory = null;
|
|
456
|
+
function installActiveFactory(factory) {
|
|
457
|
+
activeFactory = factory;
|
|
458
|
+
}
|
|
459
|
+
function getActiveFactory() {
|
|
460
|
+
return activeFactory;
|
|
461
|
+
}
|
|
462
|
+
function installCredentialStore(store) {
|
|
463
|
+
installedStore = store;
|
|
464
|
+
}
|
|
465
|
+
function getInstalledCredentialStore() {
|
|
466
|
+
return installedStore;
|
|
467
|
+
}
|
|
468
|
+
function resolveCredentialRef(ref, env = process.env) {
|
|
469
|
+
return installedStore?.resolve(ref) ?? env[ref];
|
|
470
|
+
}
|
|
471
|
+
join(".almadar", "dev-credentials.json");
|
|
472
|
+
|
|
286
473
|
// src/integrations/stripe/index.ts
|
|
287
474
|
var STRIPE_API_VERSION = "2025-02-24.acacia";
|
|
288
475
|
function isoFromUnix(seconds) {
|
|
@@ -726,10 +913,10 @@ var TwilioIntegration = class extends BaseIntegration {
|
|
|
726
913
|
}
|
|
727
914
|
}
|
|
728
915
|
async sendSMS(params) {
|
|
729
|
-
const { to, body } = params;
|
|
916
|
+
const { to, body, from } = params;
|
|
730
917
|
this.logger.debug("Sending SMS", { to: String(to ?? "") });
|
|
731
918
|
const message = await this.client.messages.create({
|
|
732
|
-
from: this.phoneNumber,
|
|
919
|
+
from: from || this.phoneNumber,
|
|
733
920
|
to,
|
|
734
921
|
body
|
|
735
922
|
});
|
|
@@ -808,31 +995,35 @@ var EmailIntegration = class extends BaseIntegration {
|
|
|
808
995
|
}
|
|
809
996
|
}
|
|
810
997
|
async send(params) {
|
|
811
|
-
const { to, subject, body, from } = params;
|
|
998
|
+
const { to, subject, body, from, htmlBody, replyTo, templateId } = params;
|
|
812
999
|
this.logger.debug("Sending email", { to: String(to), subject: String(subject), provider: this.provider });
|
|
1000
|
+
const message = {
|
|
1001
|
+
to,
|
|
1002
|
+
subject,
|
|
1003
|
+
body,
|
|
1004
|
+
from: from || this.fromEmail,
|
|
1005
|
+
htmlBody: htmlBody || void 0,
|
|
1006
|
+
replyTo: replyTo || void 0,
|
|
1007
|
+
templateId: templateId || void 0
|
|
1008
|
+
};
|
|
813
1009
|
if (this.provider === "sendgrid") {
|
|
814
|
-
return await this.sendViaSendGrid(
|
|
815
|
-
to,
|
|
816
|
-
subject,
|
|
817
|
-
body,
|
|
818
|
-
from || this.fromEmail
|
|
819
|
-
);
|
|
1010
|
+
return await this.sendViaSendGrid(message);
|
|
820
1011
|
} else if (this.provider === "resend") {
|
|
821
|
-
return await this.sendViaResend(
|
|
822
|
-
to,
|
|
823
|
-
subject,
|
|
824
|
-
body,
|
|
825
|
-
from || this.fromEmail
|
|
826
|
-
);
|
|
1012
|
+
return await this.sendViaResend(message);
|
|
827
1013
|
}
|
|
828
1014
|
throw new Error(`Unknown email provider: ${this.provider}`);
|
|
829
1015
|
}
|
|
830
|
-
async sendViaSendGrid(
|
|
1016
|
+
async sendViaSendGrid(message) {
|
|
831
1017
|
const msg = {
|
|
832
|
-
to,
|
|
833
|
-
from,
|
|
834
|
-
subject,
|
|
835
|
-
|
|
1018
|
+
to: message.to,
|
|
1019
|
+
from: message.from,
|
|
1020
|
+
subject: message.subject,
|
|
1021
|
+
// htmlBody present → it carries the HTML and body becomes the
|
|
1022
|
+
// plain-text alternative; absent → body renders as HTML (legacy).
|
|
1023
|
+
html: message.htmlBody ?? message.body,
|
|
1024
|
+
...message.htmlBody ? { text: message.body } : {},
|
|
1025
|
+
...message.replyTo ? { replyTo: message.replyTo } : {},
|
|
1026
|
+
...message.templateId ? { templateId: message.templateId } : {}
|
|
836
1027
|
};
|
|
837
1028
|
const response = await sgMail.send(msg);
|
|
838
1029
|
return {
|
|
@@ -840,15 +1031,20 @@ var EmailIntegration = class extends BaseIntegration {
|
|
|
840
1031
|
status: "sent"
|
|
841
1032
|
};
|
|
842
1033
|
}
|
|
843
|
-
async sendViaResend(
|
|
1034
|
+
async sendViaResend(message) {
|
|
844
1035
|
if (!this.resendClient) {
|
|
845
1036
|
throw new Error("Resend client not initialized");
|
|
846
1037
|
}
|
|
1038
|
+
if (message.templateId) {
|
|
1039
|
+
throw new Error("templateId is not supported by the resend provider \u2014 use sendgrid or drop templateId");
|
|
1040
|
+
}
|
|
847
1041
|
const response = await this.resendClient.emails.send({
|
|
848
|
-
from,
|
|
849
|
-
to,
|
|
850
|
-
subject,
|
|
851
|
-
html: body
|
|
1042
|
+
from: message.from,
|
|
1043
|
+
to: message.to,
|
|
1044
|
+
subject: message.subject,
|
|
1045
|
+
html: message.htmlBody ?? message.body,
|
|
1046
|
+
...message.htmlBody ? { text: message.body } : {},
|
|
1047
|
+
...message.replyTo ? { replyTo: message.replyTo } : {}
|
|
852
1048
|
});
|
|
853
1049
|
return {
|
|
854
1050
|
id: response.data?.id,
|
|
@@ -913,25 +1109,948 @@ var WebhookIntegration = class extends BaseIntegration {
|
|
|
913
1109
|
if (signingSecret) {
|
|
914
1110
|
headers["X-Almadar-Signature"] = "sha256=" + createHmac("sha256", signingSecret).update(body).digest("hex");
|
|
915
1111
|
}
|
|
916
|
-
this.logger.debug("Sending webhook", { url: String(url), event: String(event) });
|
|
917
|
-
const startTime = Date.now();
|
|
918
|
-
const response = await fetch(url, {
|
|
919
|
-
method: "POST",
|
|
920
|
-
headers,
|
|
921
|
-
body,
|
|
922
|
-
signal: AbortSignal.timeout(this.timeoutMs)
|
|
1112
|
+
this.logger.debug("Sending webhook", { url: String(url), event: String(event) });
|
|
1113
|
+
const startTime = Date.now();
|
|
1114
|
+
const response = await fetch(url, {
|
|
1115
|
+
method: "POST",
|
|
1116
|
+
headers,
|
|
1117
|
+
body,
|
|
1118
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
1119
|
+
});
|
|
1120
|
+
if (response.status >= 500) {
|
|
1121
|
+
throw new Error(`Webhook endpoint returned ${response.status}`);
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
status: response.status,
|
|
1125
|
+
ok: response.ok,
|
|
1126
|
+
durationMs: Date.now() - startTime
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
registerIntegration("webhook", WebhookIntegration);
|
|
1131
|
+
function isParamRecord(value) {
|
|
1132
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
1133
|
+
}
|
|
1134
|
+
function toSubscription(value) {
|
|
1135
|
+
if (isParamRecord(value)) {
|
|
1136
|
+
const { endpoint, keys } = value;
|
|
1137
|
+
if (typeof endpoint === "string" && isParamRecord(keys)) {
|
|
1138
|
+
const { p256dh, auth } = keys;
|
|
1139
|
+
if (typeof p256dh === "string" && typeof auth === "string") {
|
|
1140
|
+
return { endpoint, keys: { p256dh, auth } };
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
throw new Error("push.send: subscription must be { endpoint, keys: { p256dh, auth } }");
|
|
1145
|
+
}
|
|
1146
|
+
var PushIntegration = class extends BaseIntegration {
|
|
1147
|
+
constructor(config) {
|
|
1148
|
+
super(config);
|
|
1149
|
+
this.vapidPublicKey = config.env.VAPID_PUBLIC_KEY || "";
|
|
1150
|
+
this.vapidPrivateKey = config.env.VAPID_PRIVATE_KEY || "";
|
|
1151
|
+
this.vapidSubject = config.env.VAPID_SUBJECT || "";
|
|
1152
|
+
this.logger.info("Push integration initialized");
|
|
1153
|
+
}
|
|
1154
|
+
async execute(action, params) {
|
|
1155
|
+
const validation = this.validateParams(action, params);
|
|
1156
|
+
if (!validation.valid) {
|
|
1157
|
+
return {
|
|
1158
|
+
success: false,
|
|
1159
|
+
error: {
|
|
1160
|
+
name: "IntegrationError",
|
|
1161
|
+
message: "Validation failed",
|
|
1162
|
+
code: "VALIDATION_ERROR",
|
|
1163
|
+
details: validation.errors
|
|
1164
|
+
},
|
|
1165
|
+
metadata: this.createMetadata(action, 0)
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
const startTime = Date.now();
|
|
1169
|
+
let retries = 0;
|
|
1170
|
+
try {
|
|
1171
|
+
let data;
|
|
1172
|
+
switch (action) {
|
|
1173
|
+
case "send":
|
|
1174
|
+
data = await this.executeWithRetry(() => this.send(params));
|
|
1175
|
+
break;
|
|
1176
|
+
default:
|
|
1177
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
success: true,
|
|
1181
|
+
data,
|
|
1182
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1183
|
+
};
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
return this.handleError(action, error);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async send(params) {
|
|
1189
|
+
const { subscription, title, body, url, icon } = params;
|
|
1190
|
+
const sub = toSubscription(subscription);
|
|
1191
|
+
const payload = JSON.stringify({
|
|
1192
|
+
title,
|
|
1193
|
+
body,
|
|
1194
|
+
url: url || void 0,
|
|
1195
|
+
icon: icon || void 0
|
|
1196
|
+
});
|
|
1197
|
+
this.logger.debug("Sending push notification", { endpoint: sub.endpoint });
|
|
1198
|
+
try {
|
|
1199
|
+
const response = await webpush.sendNotification(sub, payload, {
|
|
1200
|
+
vapidDetails: {
|
|
1201
|
+
subject: this.vapidSubject,
|
|
1202
|
+
publicKey: this.vapidPublicKey,
|
|
1203
|
+
privateKey: this.vapidPrivateKey
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
return { statusCode: response.statusCode, ok: true, expired: false };
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
if (error instanceof webpush.WebPushError) {
|
|
1209
|
+
if (error.statusCode >= 500) {
|
|
1210
|
+
throw new Error(`Push endpoint returned ${error.statusCode}`);
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
statusCode: error.statusCode,
|
|
1214
|
+
ok: false,
|
|
1215
|
+
expired: error.statusCode === 404 || error.statusCode === 410
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
throw error;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
registerIntegration("push", PushIntegration);
|
|
1223
|
+
var CalendarIntegration = class extends BaseIntegration {
|
|
1224
|
+
constructor(config) {
|
|
1225
|
+
super(config);
|
|
1226
|
+
const rawKey = config.env.GOOGLE_CALENDAR_SA_KEY;
|
|
1227
|
+
if (!rawKey) {
|
|
1228
|
+
throw new Error("GOOGLE_CALENDAR_SA_KEY not configured");
|
|
1229
|
+
}
|
|
1230
|
+
const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
|
|
1231
|
+
const key = JSON.parse(keyJson);
|
|
1232
|
+
const subject = config.env.GOOGLE_CALENDAR_SUBJECT || void 0;
|
|
1233
|
+
const auth = new google.auth.JWT({
|
|
1234
|
+
email: key.client_email,
|
|
1235
|
+
key: key.private_key,
|
|
1236
|
+
scopes: ["https://www.googleapis.com/auth/calendar"],
|
|
1237
|
+
subject
|
|
1238
|
+
});
|
|
1239
|
+
this.client = google.calendar({ version: "v3", auth });
|
|
1240
|
+
this.defaultCalendarId = config.env.GOOGLE_CALENDAR_ID || "primary";
|
|
1241
|
+
this.logger.info("Calendar integration initialized", {
|
|
1242
|
+
delegated: Boolean(subject)
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
async execute(action, params) {
|
|
1246
|
+
const validation = this.validateParams(action, params);
|
|
1247
|
+
if (!validation.valid) {
|
|
1248
|
+
return {
|
|
1249
|
+
success: false,
|
|
1250
|
+
error: {
|
|
1251
|
+
name: "IntegrationError",
|
|
1252
|
+
message: "Validation failed",
|
|
1253
|
+
code: "VALIDATION_ERROR",
|
|
1254
|
+
details: validation.errors
|
|
1255
|
+
},
|
|
1256
|
+
metadata: this.createMetadata(action, 0)
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
const startTime = Date.now();
|
|
1260
|
+
let retries = 0;
|
|
1261
|
+
try {
|
|
1262
|
+
let data;
|
|
1263
|
+
switch (action) {
|
|
1264
|
+
case "listEvents":
|
|
1265
|
+
data = await this.executeWithRetry(() => this.listEvents(params));
|
|
1266
|
+
break;
|
|
1267
|
+
case "createEvent":
|
|
1268
|
+
data = await this.executeWithRetry(() => this.createEvent(params));
|
|
1269
|
+
break;
|
|
1270
|
+
case "updateEvent":
|
|
1271
|
+
data = await this.executeWithRetry(() => this.updateEvent(params));
|
|
1272
|
+
break;
|
|
1273
|
+
case "deleteEvent":
|
|
1274
|
+
data = await this.executeWithRetry(() => this.deleteEvent(params));
|
|
1275
|
+
break;
|
|
1276
|
+
case "freeBusy":
|
|
1277
|
+
data = await this.executeWithRetry(() => this.freeBusy(params));
|
|
1278
|
+
break;
|
|
1279
|
+
case "watch":
|
|
1280
|
+
data = await this.executeWithRetry(() => this.watch(params));
|
|
1281
|
+
break;
|
|
1282
|
+
case "stopWatch":
|
|
1283
|
+
data = await this.executeWithRetry(() => this.stopWatch(params));
|
|
1284
|
+
break;
|
|
1285
|
+
default:
|
|
1286
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
success: true,
|
|
1290
|
+
data,
|
|
1291
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
1292
|
+
};
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
return this.handleError(action, error);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
calendarId(params) {
|
|
1298
|
+
return params.calendarId || this.defaultCalendarId;
|
|
1299
|
+
}
|
|
1300
|
+
toEventTime(value) {
|
|
1301
|
+
return value.length === 10 ? { date: value } : { dateTime: value };
|
|
1302
|
+
}
|
|
1303
|
+
fromEventTime(time) {
|
|
1304
|
+
return time?.dateTime ?? time?.date ?? "";
|
|
1305
|
+
}
|
|
1306
|
+
async listEvents(params) {
|
|
1307
|
+
const { timeMin, timeMax, syncToken, maxResults } = params;
|
|
1308
|
+
const response = await this.client.events.list({
|
|
1309
|
+
calendarId: this.calendarId(params),
|
|
1310
|
+
// Incremental sync: a syncToken supersedes the window params (the API
|
|
1311
|
+
// rejects combining them).
|
|
1312
|
+
...syncToken ? { syncToken } : {
|
|
1313
|
+
timeMin: timeMin || void 0,
|
|
1314
|
+
timeMax: timeMax || void 0,
|
|
1315
|
+
singleEvents: true,
|
|
1316
|
+
orderBy: "startTime"
|
|
1317
|
+
},
|
|
1318
|
+
maxResults: maxResults || 250
|
|
1319
|
+
});
|
|
1320
|
+
const items = response.data.items ?? [];
|
|
1321
|
+
return {
|
|
1322
|
+
events: items.map((event) => ({
|
|
1323
|
+
id: event.id ?? "",
|
|
1324
|
+
summary: event.summary ?? "",
|
|
1325
|
+
description: event.description ?? "",
|
|
1326
|
+
location: event.location ?? "",
|
|
1327
|
+
start: this.fromEventTime(event.start ?? void 0),
|
|
1328
|
+
end: this.fromEventTime(event.end ?? void 0),
|
|
1329
|
+
status: event.status ?? "",
|
|
1330
|
+
updated: event.updated ?? ""
|
|
1331
|
+
})),
|
|
1332
|
+
nextSyncToken: response.data.nextSyncToken ?? null
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
resolveEnd(start, end, durationMinutes) {
|
|
1336
|
+
if (end) return end;
|
|
1337
|
+
if (start.length === 10) {
|
|
1338
|
+
const next = new Date(Date.parse(start) + 24 * 60 * 6e4);
|
|
1339
|
+
return next.toISOString().slice(0, 10);
|
|
1340
|
+
}
|
|
1341
|
+
if (durationMinutes && durationMinutes > 0) {
|
|
1342
|
+
return new Date(Date.parse(start) + durationMinutes * 6e4).toISOString();
|
|
1343
|
+
}
|
|
1344
|
+
throw new Error("createEvent requires `end` or a positive `durationMinutes`");
|
|
1345
|
+
}
|
|
1346
|
+
async createEvent(params) {
|
|
1347
|
+
const { summary, description, location, start, end, durationMinutes } = params;
|
|
1348
|
+
const response = await this.client.events.insert({
|
|
1349
|
+
calendarId: this.calendarId(params),
|
|
1350
|
+
requestBody: {
|
|
1351
|
+
summary,
|
|
1352
|
+
description: description || void 0,
|
|
1353
|
+
location: location || void 0,
|
|
1354
|
+
start: this.toEventTime(start),
|
|
1355
|
+
end: this.toEventTime(
|
|
1356
|
+
this.resolveEnd(start, end || void 0, durationMinutes || void 0)
|
|
1357
|
+
)
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
return {
|
|
1361
|
+
id: response.data.id ?? "",
|
|
1362
|
+
status: response.data.status ?? "",
|
|
1363
|
+
htmlLink: response.data.htmlLink ?? ""
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
async updateEvent(params) {
|
|
1367
|
+
const { eventId, summary, description, location, start, end } = params;
|
|
1368
|
+
const requestBody = {};
|
|
1369
|
+
if (typeof summary === "string") requestBody.summary = summary;
|
|
1370
|
+
if (typeof description === "string") requestBody.description = description;
|
|
1371
|
+
if (typeof location === "string") requestBody.location = location;
|
|
1372
|
+
if (typeof start === "string" && start) requestBody.start = this.toEventTime(start);
|
|
1373
|
+
if (typeof end === "string" && end) requestBody.end = this.toEventTime(end);
|
|
1374
|
+
const response = await this.client.events.patch({
|
|
1375
|
+
calendarId: this.calendarId(params),
|
|
1376
|
+
eventId,
|
|
1377
|
+
requestBody
|
|
1378
|
+
});
|
|
1379
|
+
return {
|
|
1380
|
+
id: response.data.id ?? "",
|
|
1381
|
+
status: response.data.status ?? ""
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
async deleteEvent(params) {
|
|
1385
|
+
const { eventId } = params;
|
|
1386
|
+
await this.client.events.delete({
|
|
1387
|
+
calendarId: this.calendarId(params),
|
|
1388
|
+
eventId
|
|
1389
|
+
});
|
|
1390
|
+
return { id: eventId, deleted: true };
|
|
1391
|
+
}
|
|
1392
|
+
async freeBusy(params) {
|
|
1393
|
+
const { timeMin, timeMax } = params;
|
|
1394
|
+
const id = this.calendarId(params);
|
|
1395
|
+
const response = await this.client.freebusy.query({
|
|
1396
|
+
requestBody: {
|
|
1397
|
+
timeMin,
|
|
1398
|
+
timeMax,
|
|
1399
|
+
items: [{ id }]
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
const busy = response.data.calendars?.[id]?.busy ?? [];
|
|
1403
|
+
return {
|
|
1404
|
+
busy: busy.map((slot) => ({ start: slot.start ?? "", end: slot.end ?? "" }))
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
async watch(params) {
|
|
1408
|
+
const { channelId, address, ttlSeconds } = params;
|
|
1409
|
+
const response = await this.client.events.watch({
|
|
1410
|
+
calendarId: this.calendarId(params),
|
|
1411
|
+
requestBody: {
|
|
1412
|
+
id: channelId,
|
|
1413
|
+
type: "web_hook",
|
|
1414
|
+
address,
|
|
1415
|
+
params: ttlSeconds ? { ttl: String(ttlSeconds) } : void 0
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
return {
|
|
1419
|
+
channelId: response.data.id ?? channelId,
|
|
1420
|
+
resourceId: response.data.resourceId ?? "",
|
|
1421
|
+
expiration: response.data.expiration ?? ""
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
async stopWatch(params) {
|
|
1425
|
+
const { channelId, resourceId } = params;
|
|
1426
|
+
await this.client.channels.stop({
|
|
1427
|
+
requestBody: {
|
|
1428
|
+
id: channelId,
|
|
1429
|
+
resourceId
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
return { stopped: true };
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
registerIntegration("calendar", CalendarIntegration);
|
|
1436
|
+
function decodeContent(content) {
|
|
1437
|
+
const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
|
|
1438
|
+
if (dataUrlMatch) {
|
|
1439
|
+
const [, mime, isB64, body] = dataUrlMatch;
|
|
1440
|
+
const bytes = isB64 ? Buffer.from(body, "base64") : Buffer.from(decodeURIComponent(body), "utf8");
|
|
1441
|
+
return { bytes, contentType: mime || null };
|
|
1442
|
+
}
|
|
1443
|
+
return { bytes: Buffer.from(content, "utf8"), contentType: null };
|
|
1444
|
+
}
|
|
1445
|
+
var DriveIntegration = class extends BaseIntegration {
|
|
1446
|
+
constructor(config) {
|
|
1447
|
+
super(config);
|
|
1448
|
+
const rawKey = config.env.GOOGLE_DRIVE_SA_KEY;
|
|
1449
|
+
if (rawKey) {
|
|
1450
|
+
const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
|
|
1451
|
+
const key = JSON.parse(keyJson);
|
|
1452
|
+
const subject = config.env.GOOGLE_DRIVE_SUBJECT || void 0;
|
|
1453
|
+
const auth = new google.auth.JWT({
|
|
1454
|
+
email: key.client_email,
|
|
1455
|
+
key: key.private_key,
|
|
1456
|
+
scopes: ["https://www.googleapis.com/auth/drive"],
|
|
1457
|
+
subject
|
|
1458
|
+
});
|
|
1459
|
+
this.saClient = google.drive({ version: "v3", auth });
|
|
1460
|
+
} else {
|
|
1461
|
+
this.saClient = null;
|
|
1462
|
+
}
|
|
1463
|
+
const refreshToken = config.env.GOOGLE_DRIVE_REFRESH_TOKEN;
|
|
1464
|
+
const clientId = config.env.OAUTH_CLIENT_ID;
|
|
1465
|
+
const clientSecret = config.env.OAUTH_CLIENT_SECRET;
|
|
1466
|
+
if (refreshToken && clientId && clientSecret) {
|
|
1467
|
+
const oauth2 = new google.auth.OAuth2(clientId, clientSecret);
|
|
1468
|
+
oauth2.setCredentials({ refresh_token: refreshToken });
|
|
1469
|
+
this.userClient = google.drive({ version: "v3", auth: oauth2 });
|
|
1470
|
+
} else {
|
|
1471
|
+
this.userClient = null;
|
|
1472
|
+
}
|
|
1473
|
+
if (!this.saClient && !this.userClient) {
|
|
1474
|
+
throw new Error(
|
|
1475
|
+
"Drive not configured \u2014 set GOOGLE_DRIVE_SA_KEY (reads) and/or GOOGLE_DRIVE_REFRESH_TOKEN + OAUTH_CLIENT_ID/SECRET (writes)"
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
this.defaultFolderId = config.env.GOOGLE_DRIVE_FOLDER_ID || void 0;
|
|
1479
|
+
this.logger.info("Drive integration initialized", {
|
|
1480
|
+
serviceAccount: this.saClient !== null,
|
|
1481
|
+
userToken: this.userClient !== null,
|
|
1482
|
+
delegated: Boolean(config.env.GOOGLE_DRIVE_SUBJECT)
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
/** Reads prefer the SA client (delegation-aware); user client covers its absence. */
|
|
1486
|
+
readClient() {
|
|
1487
|
+
const client = this.saClient ?? this.userClient;
|
|
1488
|
+
if (!client) throw new Error("Drive not configured");
|
|
1489
|
+
return client;
|
|
1490
|
+
}
|
|
1491
|
+
/** Writes REQUIRE the user client on personal accounts (SA has no storage quota); SA only as a Workspace fallback. */
|
|
1492
|
+
writeClient() {
|
|
1493
|
+
const client = this.userClient ?? this.saClient;
|
|
1494
|
+
if (!client) throw new Error("Drive not configured");
|
|
1495
|
+
return client;
|
|
1496
|
+
}
|
|
1497
|
+
async execute(action, params) {
|
|
1498
|
+
const validation = this.validateParams(action, params);
|
|
1499
|
+
if (!validation.valid) {
|
|
1500
|
+
return {
|
|
1501
|
+
success: false,
|
|
1502
|
+
error: {
|
|
1503
|
+
name: "IntegrationError",
|
|
1504
|
+
message: "Validation failed",
|
|
1505
|
+
code: "VALIDATION_ERROR",
|
|
1506
|
+
details: validation.errors
|
|
1507
|
+
},
|
|
1508
|
+
metadata: this.createMetadata(action, 0)
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
const startTime = Date.now();
|
|
1512
|
+
try {
|
|
1513
|
+
let data;
|
|
1514
|
+
switch (action) {
|
|
1515
|
+
case "listFiles":
|
|
1516
|
+
data = await this.executeWithRetry(() => this.listFiles(params));
|
|
1517
|
+
break;
|
|
1518
|
+
case "getFile":
|
|
1519
|
+
data = await this.executeWithRetry(() => this.getFile(params));
|
|
1520
|
+
break;
|
|
1521
|
+
case "uploadFile":
|
|
1522
|
+
data = await this.executeWithRetry(() => this.uploadFile(params));
|
|
1523
|
+
break;
|
|
1524
|
+
case "createFolder":
|
|
1525
|
+
data = await this.executeWithRetry(() => this.createFolder(params));
|
|
1526
|
+
break;
|
|
1527
|
+
case "shareFile":
|
|
1528
|
+
data = await this.executeWithRetry(() => this.shareFile(params));
|
|
1529
|
+
break;
|
|
1530
|
+
default:
|
|
1531
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1532
|
+
}
|
|
1533
|
+
return {
|
|
1534
|
+
success: true,
|
|
1535
|
+
data,
|
|
1536
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1537
|
+
};
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
return this.handleError(action, error);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
async listFiles(params) {
|
|
1543
|
+
const { folderId, query, maxResults } = params;
|
|
1544
|
+
const clauses = ["trashed = false"];
|
|
1545
|
+
if (folderId) clauses.push(`'${String(folderId).replace(/'/g, "\\'")}' in parents`);
|
|
1546
|
+
if (query) clauses.push(String(query));
|
|
1547
|
+
const response = await this.readClient().files.list({
|
|
1548
|
+
q: clauses.join(" and "),
|
|
1549
|
+
pageSize: maxResults || 100,
|
|
1550
|
+
fields: "files(id, name, mimeType, size, modifiedTime, webViewLink)"
|
|
1551
|
+
});
|
|
1552
|
+
return {
|
|
1553
|
+
files: (response.data.files ?? []).map((file) => ({
|
|
1554
|
+
id: file.id ?? "",
|
|
1555
|
+
name: file.name ?? "",
|
|
1556
|
+
mimeType: file.mimeType ?? "",
|
|
1557
|
+
size: Number(file.size ?? 0),
|
|
1558
|
+
modifiedTime: file.modifiedTime ?? "",
|
|
1559
|
+
webViewLink: file.webViewLink ?? ""
|
|
1560
|
+
}))
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
async getFile(params) {
|
|
1564
|
+
const fileId = params.fileId;
|
|
1565
|
+
const meta = await this.readClient().files.get({
|
|
1566
|
+
fileId,
|
|
1567
|
+
fields: "id, name, mimeType, size"
|
|
1568
|
+
});
|
|
1569
|
+
const content = await this.readClient().files.get(
|
|
1570
|
+
{ fileId, alt: "media" },
|
|
1571
|
+
{ responseType: "arraybuffer" }
|
|
1572
|
+
);
|
|
1573
|
+
const bytes = Buffer.from(content.data);
|
|
1574
|
+
return {
|
|
1575
|
+
id: meta.data.id ?? fileId,
|
|
1576
|
+
name: meta.data.name ?? "",
|
|
1577
|
+
mimeType: meta.data.mimeType ?? "application/octet-stream",
|
|
1578
|
+
content: bytes.toString("base64"),
|
|
1579
|
+
size: bytes.length
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
async uploadFile(params) {
|
|
1583
|
+
const { name, content, mimeType, folderId } = params;
|
|
1584
|
+
const { bytes, contentType } = decodeContent(content);
|
|
1585
|
+
const parent = folderId || this.defaultFolderId;
|
|
1586
|
+
const response = await this.writeClient().files.create({
|
|
1587
|
+
requestBody: {
|
|
1588
|
+
name,
|
|
1589
|
+
parents: parent ? [parent] : void 0
|
|
1590
|
+
},
|
|
1591
|
+
media: {
|
|
1592
|
+
mimeType: mimeType || contentType || "application/octet-stream",
|
|
1593
|
+
body: Readable.from(bytes)
|
|
1594
|
+
},
|
|
1595
|
+
fields: "id, name, webViewLink"
|
|
1596
|
+
});
|
|
1597
|
+
return {
|
|
1598
|
+
id: response.data.id ?? "",
|
|
1599
|
+
name: response.data.name ?? name,
|
|
1600
|
+
webViewLink: response.data.webViewLink ?? ""
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
async createFolder(params) {
|
|
1604
|
+
const { name, parentId } = params;
|
|
1605
|
+
const parent = parentId || this.defaultFolderId;
|
|
1606
|
+
const response = await this.writeClient().files.create({
|
|
1607
|
+
requestBody: {
|
|
1608
|
+
name,
|
|
1609
|
+
mimeType: "application/vnd.google-apps.folder",
|
|
1610
|
+
parents: parent ? [parent] : void 0
|
|
1611
|
+
},
|
|
1612
|
+
fields: "id, name"
|
|
1613
|
+
});
|
|
1614
|
+
return { id: response.data.id ?? "", name: response.data.name ?? name };
|
|
1615
|
+
}
|
|
1616
|
+
async shareFile(params) {
|
|
1617
|
+
const { fileId, email, role } = params;
|
|
1618
|
+
const response = await this.readClient().permissions.create({
|
|
1619
|
+
fileId,
|
|
1620
|
+
requestBody: {
|
|
1621
|
+
type: "user",
|
|
1622
|
+
role: role || "reader",
|
|
1623
|
+
emailAddress: email
|
|
1624
|
+
},
|
|
1625
|
+
fields: "id"
|
|
1626
|
+
});
|
|
1627
|
+
return { shared: true, permissionId: response.data.id ?? "" };
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
registerIntegration("drive", DriveIntegration);
|
|
1631
|
+
|
|
1632
|
+
// src/integrations/metaAds/index.ts
|
|
1633
|
+
var GRAPH_BASE = "https://graph.facebook.com/v21.0";
|
|
1634
|
+
var MetaAdsIntegration = class extends BaseIntegration {
|
|
1635
|
+
constructor(config) {
|
|
1636
|
+
super(config);
|
|
1637
|
+
this.accessToken = config.env.META_ACCESS_TOKEN || "";
|
|
1638
|
+
if (!this.accessToken) {
|
|
1639
|
+
throw new Error("META_ACCESS_TOKEN not configured");
|
|
1640
|
+
}
|
|
1641
|
+
this.defaultAccountId = config.env.META_AD_ACCOUNT_ID || "";
|
|
1642
|
+
this.logger.info("Meta Ads integration initialized");
|
|
1643
|
+
}
|
|
1644
|
+
async execute(action, params) {
|
|
1645
|
+
const validation = this.validateParams(action, params);
|
|
1646
|
+
if (!validation.valid) {
|
|
1647
|
+
return {
|
|
1648
|
+
success: false,
|
|
1649
|
+
error: {
|
|
1650
|
+
name: "IntegrationError",
|
|
1651
|
+
message: "Validation failed",
|
|
1652
|
+
code: "VALIDATION_ERROR",
|
|
1653
|
+
details: validation.errors
|
|
1654
|
+
},
|
|
1655
|
+
metadata: this.createMetadata(action, 0)
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
const startTime = Date.now();
|
|
1659
|
+
try {
|
|
1660
|
+
let data;
|
|
1661
|
+
switch (action) {
|
|
1662
|
+
case "getSpend":
|
|
1663
|
+
data = await this.executeWithRetry(() => this.getSpend(params));
|
|
1664
|
+
break;
|
|
1665
|
+
case "listCampaigns":
|
|
1666
|
+
data = await this.executeWithRetry(() => this.listCampaigns(params));
|
|
1667
|
+
break;
|
|
1668
|
+
default:
|
|
1669
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1670
|
+
}
|
|
1671
|
+
return {
|
|
1672
|
+
success: true,
|
|
1673
|
+
data,
|
|
1674
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1675
|
+
};
|
|
1676
|
+
} catch (error) {
|
|
1677
|
+
return this.handleError(action, error);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
accountId(params) {
|
|
1681
|
+
const id = params.accountId || this.defaultAccountId;
|
|
1682
|
+
if (!id) {
|
|
1683
|
+
throw new Error("No ad account: pass `accountId` or set META_AD_ACCOUNT_ID");
|
|
1684
|
+
}
|
|
1685
|
+
return id.startsWith("act_") ? id : `act_${id}`;
|
|
1686
|
+
}
|
|
1687
|
+
async graphGet(path, query) {
|
|
1688
|
+
const url = new URL(`${GRAPH_BASE}/${path}`);
|
|
1689
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1690
|
+
url.searchParams.set(key, value);
|
|
1691
|
+
}
|
|
1692
|
+
url.searchParams.set("access_token", this.accessToken);
|
|
1693
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
1694
|
+
if (response.status >= 500) {
|
|
1695
|
+
throw new Error(`Meta Graph API returned ${response.status}`);
|
|
1696
|
+
}
|
|
1697
|
+
const body = await response.json();
|
|
1698
|
+
if (!response.ok) {
|
|
1699
|
+
throw new Error(`Meta Graph API error: ${body.error?.message ?? response.status}`);
|
|
1700
|
+
}
|
|
1701
|
+
return body.data ?? body;
|
|
1702
|
+
}
|
|
1703
|
+
async getSpend(params) {
|
|
1704
|
+
const { since, until } = params;
|
|
1705
|
+
const rows = await this.graphGet(`${this.accountId(params)}/insights`, {
|
|
1706
|
+
fields: "spend,impressions,clicks,account_currency",
|
|
1707
|
+
time_range: JSON.stringify({ since, until }),
|
|
1708
|
+
level: "account"
|
|
1709
|
+
});
|
|
1710
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
1711
|
+
return {
|
|
1712
|
+
spend: Number(row?.spend ?? 0),
|
|
1713
|
+
currency: row?.account_currency ?? "",
|
|
1714
|
+
impressions: Number(row?.impressions ?? 0),
|
|
1715
|
+
clicks: Number(row?.clicks ?? 0)
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
async listCampaigns(params) {
|
|
1719
|
+
const { status } = params;
|
|
1720
|
+
const rows = await this.graphGet(`${this.accountId(params)}/campaigns`, {
|
|
1721
|
+
fields: "id,name,status,daily_budget",
|
|
1722
|
+
...status ? { effective_status: JSON.stringify([status]) } : {}
|
|
1723
|
+
});
|
|
1724
|
+
return {
|
|
1725
|
+
campaigns: (Array.isArray(rows) ? rows : []).map((row) => ({
|
|
1726
|
+
id: row.id ?? "",
|
|
1727
|
+
name: row.name ?? "",
|
|
1728
|
+
status: row.status ?? "",
|
|
1729
|
+
// Meta reports budgets in minor units (cents).
|
|
1730
|
+
dailyBudget: Number(row.daily_budget ?? 0) / 100
|
|
1731
|
+
}))
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
registerIntegration("metaAds", MetaAdsIntegration);
|
|
1736
|
+
|
|
1737
|
+
// src/integrations/accounting/index.ts
|
|
1738
|
+
var AccountingIntegration = class extends BaseIntegration {
|
|
1739
|
+
constructor(config) {
|
|
1740
|
+
super(config);
|
|
1741
|
+
this.logger.info("Accounting integration initialized (generic CSV export)");
|
|
1742
|
+
}
|
|
1743
|
+
async execute(action, params) {
|
|
1744
|
+
const validation = this.validateParams(action, params);
|
|
1745
|
+
if (!validation.valid) {
|
|
1746
|
+
return {
|
|
1747
|
+
success: false,
|
|
1748
|
+
error: {
|
|
1749
|
+
name: "IntegrationError",
|
|
1750
|
+
message: "Validation failed",
|
|
1751
|
+
code: "VALIDATION_ERROR",
|
|
1752
|
+
details: validation.errors
|
|
1753
|
+
},
|
|
1754
|
+
metadata: this.createMetadata(action, 0)
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
const startTime = Date.now();
|
|
1758
|
+
try {
|
|
1759
|
+
let data;
|
|
1760
|
+
switch (action) {
|
|
1761
|
+
case "exportInvoices":
|
|
1762
|
+
data = this.exportRows(params.invoices, INVOICE_COLUMNS, "invoices");
|
|
1763
|
+
break;
|
|
1764
|
+
case "exportJournal":
|
|
1765
|
+
data = this.exportRows(params.entries, JOURNAL_COLUMNS, "journal");
|
|
1766
|
+
break;
|
|
1767
|
+
default:
|
|
1768
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1769
|
+
}
|
|
1770
|
+
return {
|
|
1771
|
+
success: true,
|
|
1772
|
+
data,
|
|
1773
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1774
|
+
};
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
return this.handleError(action, error);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
exportRows(rowsValue, columns, kind) {
|
|
1780
|
+
if (!Array.isArray(rowsValue)) {
|
|
1781
|
+
throw new Error(`${kind} export requires an array of rows`);
|
|
1782
|
+
}
|
|
1783
|
+
const lines = [columns.join(",")];
|
|
1784
|
+
for (const row of rowsValue) {
|
|
1785
|
+
if (row === null || typeof row !== "object" || Array.isArray(row) || row instanceof Date) {
|
|
1786
|
+
throw new Error(`${kind} export: every row must be an object`);
|
|
1787
|
+
}
|
|
1788
|
+
const record = row;
|
|
1789
|
+
lines.push(columns.map((column) => csvCell(record[column])).join(","));
|
|
1790
|
+
}
|
|
1791
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1792
|
+
return {
|
|
1793
|
+
content: lines.join("\r\n") + "\r\n",
|
|
1794
|
+
filename: `${kind}-export-${stamp}.csv`,
|
|
1795
|
+
count: rowsValue.length
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
var INVOICE_COLUMNS = ["id", "number", "customer", "issuedAt", "dueAt", "currency", "net", "tax", "gross", "status"];
|
|
1800
|
+
var JOURNAL_COLUMNS = ["date", "account", "description", "debit", "credit", "reference"];
|
|
1801
|
+
function csvCell(value) {
|
|
1802
|
+
if (value === void 0 || value === null) return "";
|
|
1803
|
+
const raw = value instanceof Date ? value.toISOString() : String(value);
|
|
1804
|
+
return /[",\r\n]/.test(raw) ? `"${raw.replace(/"/g, '""')}"` : raw;
|
|
1805
|
+
}
|
|
1806
|
+
registerIntegration("accounting", AccountingIntegration);
|
|
1807
|
+
|
|
1808
|
+
// src/integrations/banking/index.ts
|
|
1809
|
+
var GC_BASE = "https://bankaccountdata.gocardless.com/api/v2";
|
|
1810
|
+
var BankingIntegration = class extends BaseIntegration {
|
|
1811
|
+
constructor(config) {
|
|
1812
|
+
super(config);
|
|
1813
|
+
this.accessToken = null;
|
|
1814
|
+
this.accessTokenExpiresAt = 0;
|
|
1815
|
+
this.secretId = config.env.GOCARDLESS_SECRET_ID || "";
|
|
1816
|
+
this.secretKey = config.env.GOCARDLESS_SECRET_KEY || "";
|
|
1817
|
+
if (!this.secretId || !this.secretKey) {
|
|
1818
|
+
throw new Error("GOCARDLESS_SECRET_ID / GOCARDLESS_SECRET_KEY not configured");
|
|
1819
|
+
}
|
|
1820
|
+
this.logger.info("Banking integration initialized (GoCardless Bank Account Data)");
|
|
1821
|
+
}
|
|
1822
|
+
async execute(action, params) {
|
|
1823
|
+
const validation = this.validateParams(action, params);
|
|
1824
|
+
if (!validation.valid) {
|
|
1825
|
+
return {
|
|
1826
|
+
success: false,
|
|
1827
|
+
error: {
|
|
1828
|
+
name: "IntegrationError",
|
|
1829
|
+
message: "Validation failed",
|
|
1830
|
+
code: "VALIDATION_ERROR",
|
|
1831
|
+
details: validation.errors
|
|
1832
|
+
},
|
|
1833
|
+
metadata: this.createMetadata(action, 0)
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
const startTime = Date.now();
|
|
1837
|
+
try {
|
|
1838
|
+
let data;
|
|
1839
|
+
switch (action) {
|
|
1840
|
+
case "createRequisition":
|
|
1841
|
+
data = await this.executeWithRetry(() => this.createRequisition(params));
|
|
1842
|
+
break;
|
|
1843
|
+
case "listAccounts":
|
|
1844
|
+
data = await this.executeWithRetry(() => this.listAccounts(params));
|
|
1845
|
+
break;
|
|
1846
|
+
case "listTransactions":
|
|
1847
|
+
data = await this.executeWithRetry(() => this.listTransactions(params));
|
|
1848
|
+
break;
|
|
1849
|
+
default:
|
|
1850
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1851
|
+
}
|
|
1852
|
+
return {
|
|
1853
|
+
success: true,
|
|
1854
|
+
data,
|
|
1855
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1856
|
+
};
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
return this.handleError(action, error);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
async token() {
|
|
1862
|
+
if (this.accessToken && Date.now() < this.accessTokenExpiresAt - 6e4) {
|
|
1863
|
+
return this.accessToken;
|
|
1864
|
+
}
|
|
1865
|
+
const response = await fetch(`${GC_BASE}/token/new/`, {
|
|
1866
|
+
method: "POST",
|
|
1867
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1868
|
+
body: JSON.stringify({ secret_id: this.secretId, secret_key: this.secretKey }),
|
|
1869
|
+
signal: AbortSignal.timeout(15e3)
|
|
1870
|
+
});
|
|
1871
|
+
if (!response.ok) {
|
|
1872
|
+
throw new Error(`GoCardless token request failed: ${response.status}`);
|
|
1873
|
+
}
|
|
1874
|
+
const body = await response.json();
|
|
1875
|
+
this.accessToken = body.access;
|
|
1876
|
+
this.accessTokenExpiresAt = Date.now() + body.access_expires * 1e3;
|
|
1877
|
+
return this.accessToken;
|
|
1878
|
+
}
|
|
1879
|
+
async gcRequest(path, init2) {
|
|
1880
|
+
const response = await fetch(`${GC_BASE}${path}`, {
|
|
1881
|
+
method: init2?.method ?? "GET",
|
|
1882
|
+
headers: {
|
|
1883
|
+
Accept: "application/json",
|
|
1884
|
+
"Content-Type": "application/json",
|
|
1885
|
+
Authorization: `Bearer ${await this.token()}`
|
|
1886
|
+
},
|
|
1887
|
+
body: init2?.body === void 0 ? void 0 : JSON.stringify(init2.body),
|
|
1888
|
+
signal: AbortSignal.timeout(2e4)
|
|
1889
|
+
});
|
|
1890
|
+
if (response.status >= 500) {
|
|
1891
|
+
throw new Error(`GoCardless returned ${response.status}`);
|
|
1892
|
+
}
|
|
1893
|
+
const body = await response.json();
|
|
1894
|
+
if (!response.ok) {
|
|
1895
|
+
throw new Error(`GoCardless error ${response.status}: ${JSON.stringify(body).slice(0, 300)}`);
|
|
1896
|
+
}
|
|
1897
|
+
return body;
|
|
1898
|
+
}
|
|
1899
|
+
async createRequisition(params) {
|
|
1900
|
+
const { institutionId, redirectUrl, reference } = params;
|
|
1901
|
+
const body = await this.gcRequest("/requisitions/", {
|
|
1902
|
+
method: "POST",
|
|
1903
|
+
body: {
|
|
1904
|
+
institution_id: institutionId,
|
|
1905
|
+
redirect: redirectUrl,
|
|
1906
|
+
reference: reference || void 0
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
return { requisitionId: body.id ?? "", link: body.link ?? "" };
|
|
1910
|
+
}
|
|
1911
|
+
async listAccounts(params) {
|
|
1912
|
+
const { requisitionId } = params;
|
|
1913
|
+
const body = await this.gcRequest(`/requisitions/${requisitionId}/`);
|
|
1914
|
+
return { accounts: body.accounts ?? [] };
|
|
1915
|
+
}
|
|
1916
|
+
async listTransactions(params) {
|
|
1917
|
+
const { accountId, dateFrom, dateTo } = params;
|
|
1918
|
+
const query = new URLSearchParams();
|
|
1919
|
+
if (dateFrom) query.set("date_from", dateFrom);
|
|
1920
|
+
if (dateTo) query.set("date_to", dateTo);
|
|
1921
|
+
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
|
1922
|
+
const body = await this.gcRequest(`/accounts/${accountId}/transactions/${suffix}`);
|
|
1923
|
+
return {
|
|
1924
|
+
transactions: (body.transactions?.booked ?? []).map((tx) => ({
|
|
1925
|
+
id: tx.transactionId ?? tx.internalTransactionId ?? "",
|
|
1926
|
+
amount: Number(tx.transactionAmount?.amount ?? 0),
|
|
1927
|
+
currency: tx.transactionAmount?.currency ?? "",
|
|
1928
|
+
date: tx.bookingDate ?? "",
|
|
1929
|
+
description: tx.remittanceInformationUnstructured ?? "",
|
|
1930
|
+
counterparty: tx.creditorName ?? tx.debtorName ?? ""
|
|
1931
|
+
}))
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
registerIntegration("banking", BankingIntegration);
|
|
1936
|
+
|
|
1937
|
+
// src/integrations/esign/index.ts
|
|
1938
|
+
var EsignIntegration = class extends BaseIntegration {
|
|
1939
|
+
constructor(config) {
|
|
1940
|
+
super(config);
|
|
1941
|
+
if (!config.env.DOCUSIGN_BASE_URL || !config.env.DOCUSIGN_ACCESS_TOKEN) {
|
|
1942
|
+
throw new Error("DOCUSIGN_BASE_URL / DOCUSIGN_ACCESS_TOKEN not configured");
|
|
1943
|
+
}
|
|
1944
|
+
this.logger.info("E-sign integration initialized (DocuSign)");
|
|
1945
|
+
}
|
|
1946
|
+
async execute(action, params) {
|
|
1947
|
+
const validation = this.validateParams(action, params);
|
|
1948
|
+
if (!validation.valid) {
|
|
1949
|
+
return {
|
|
1950
|
+
success: false,
|
|
1951
|
+
error: {
|
|
1952
|
+
name: "IntegrationError",
|
|
1953
|
+
message: "Validation failed",
|
|
1954
|
+
code: "VALIDATION_ERROR",
|
|
1955
|
+
details: validation.errors
|
|
1956
|
+
},
|
|
1957
|
+
metadata: this.createMetadata(action, 0)
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
const startTime = Date.now();
|
|
1961
|
+
try {
|
|
1962
|
+
let data;
|
|
1963
|
+
switch (action) {
|
|
1964
|
+
case "sendEnvelope":
|
|
1965
|
+
data = await this.executeWithRetry(() => this.sendEnvelope(params));
|
|
1966
|
+
break;
|
|
1967
|
+
case "getEnvelopeStatus":
|
|
1968
|
+
data = await this.executeWithRetry(() => this.getEnvelopeStatus(params));
|
|
1969
|
+
break;
|
|
1970
|
+
case "downloadDocument":
|
|
1971
|
+
data = await this.executeWithRetry(() => this.downloadDocument(params));
|
|
1972
|
+
break;
|
|
1973
|
+
default:
|
|
1974
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1975
|
+
}
|
|
1976
|
+
return {
|
|
1977
|
+
success: true,
|
|
1978
|
+
data,
|
|
1979
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
1980
|
+
};
|
|
1981
|
+
} catch (error) {
|
|
1982
|
+
return this.handleError(action, error);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
async dsRequest(path, init2) {
|
|
1986
|
+
const base = this.config.env.DOCUSIGN_BASE_URL.replace(/\/$/, "");
|
|
1987
|
+
const response = await fetch(`${base}${path}`, {
|
|
1988
|
+
method: init2?.method ?? "GET",
|
|
1989
|
+
headers: {
|
|
1990
|
+
Accept: init2?.raw ? "application/pdf" : "application/json",
|
|
1991
|
+
"Content-Type": "application/json",
|
|
1992
|
+
Authorization: `Bearer ${this.config.env.DOCUSIGN_ACCESS_TOKEN}`
|
|
1993
|
+
},
|
|
1994
|
+
body: init2?.body === void 0 ? void 0 : JSON.stringify(init2.body),
|
|
1995
|
+
signal: AbortSignal.timeout(3e4)
|
|
923
1996
|
});
|
|
924
1997
|
if (response.status >= 500) {
|
|
925
|
-
throw new Error(`
|
|
1998
|
+
throw new Error(`DocuSign returned ${response.status}`);
|
|
926
1999
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
2000
|
+
if (!response.ok) {
|
|
2001
|
+
const detail = await response.text();
|
|
2002
|
+
throw new Error(`DocuSign error ${response.status}: ${detail.slice(0, 300)}`);
|
|
2003
|
+
}
|
|
2004
|
+
if (init2?.raw) {
|
|
2005
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2006
|
+
}
|
|
2007
|
+
return response.json();
|
|
2008
|
+
}
|
|
2009
|
+
async sendEnvelope(params) {
|
|
2010
|
+
const { recipientEmail, recipientName, documentName, documentContent, emailSubject } = params;
|
|
2011
|
+
const rawContent = documentContent;
|
|
2012
|
+
const base64 = rawContent.startsWith("data:") ? rawContent.slice(rawContent.indexOf(",") + 1) : rawContent;
|
|
2013
|
+
const body = await this.dsRequest("/envelopes", {
|
|
2014
|
+
method: "POST",
|
|
2015
|
+
body: {
|
|
2016
|
+
emailSubject: emailSubject || `Please sign: ${documentName}`,
|
|
2017
|
+
status: "sent",
|
|
2018
|
+
documents: [
|
|
2019
|
+
{
|
|
2020
|
+
documentBase64: base64,
|
|
2021
|
+
name: documentName,
|
|
2022
|
+
fileExtension: String(documentName).split(".").pop() || "pdf",
|
|
2023
|
+
documentId: "1"
|
|
2024
|
+
}
|
|
2025
|
+
],
|
|
2026
|
+
recipients: {
|
|
2027
|
+
signers: [
|
|
2028
|
+
{
|
|
2029
|
+
email: recipientEmail,
|
|
2030
|
+
name: recipientName,
|
|
2031
|
+
recipientId: "1",
|
|
2032
|
+
routingOrder: "1"
|
|
2033
|
+
}
|
|
2034
|
+
]
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
return { envelopeId: body.envelopeId ?? "", status: body.status ?? "sent" };
|
|
2039
|
+
}
|
|
2040
|
+
async getEnvelopeStatus(params) {
|
|
2041
|
+
const { envelopeId } = params;
|
|
2042
|
+
const body = await this.dsRequest(`/envelopes/${envelopeId}`);
|
|
2043
|
+
return { status: body.status ?? "", completedAt: body.completedDateTime ?? "" };
|
|
2044
|
+
}
|
|
2045
|
+
async downloadDocument(params) {
|
|
2046
|
+
const { envelopeId } = params;
|
|
2047
|
+
const bytes = await this.dsRequest(`/envelopes/${envelopeId}/documents/combined`, {
|
|
2048
|
+
raw: true
|
|
2049
|
+
});
|
|
2050
|
+
return { content: bytes.toString("base64"), documentName: `envelope-${envelopeId}.pdf` };
|
|
932
2051
|
}
|
|
933
2052
|
};
|
|
934
|
-
registerIntegration("
|
|
2053
|
+
registerIntegration("esign", EsignIntegration);
|
|
935
2054
|
var LLMIntegration = class extends BaseIntegration {
|
|
936
2055
|
constructor(config) {
|
|
937
2056
|
super(config);
|
|
@@ -2502,25 +3621,59 @@ var OtelIntegration = class extends BaseIntegration {
|
|
|
2502
3621
|
}
|
|
2503
3622
|
};
|
|
2504
3623
|
registerIntegration("otel", OtelIntegration);
|
|
2505
|
-
|
|
2506
|
-
// src/integrations/oauth/index.ts
|
|
2507
3624
|
var PROVIDER_AUTH_URLS = {
|
|
2508
3625
|
google: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
2509
3626
|
github: "https://github.com/login/oauth/authorize",
|
|
2510
3627
|
auth0: "https://auth.example.com/authorize"
|
|
2511
3628
|
};
|
|
3629
|
+
var PROVIDER_ISSUERS = {
|
|
3630
|
+
google: "https://accounts.google.com"
|
|
3631
|
+
};
|
|
3632
|
+
var PENDING_GRANT_TTL_MS = 10 * 60 * 1e3;
|
|
3633
|
+
var InMemoryPendingGrantStore = class {
|
|
3634
|
+
constructor() {
|
|
3635
|
+
this.grants = /* @__PURE__ */ new Map();
|
|
3636
|
+
}
|
|
3637
|
+
async put(state, grant, ttlMs) {
|
|
3638
|
+
this.grants.set(state, { grant, expiresAt: Date.now() + ttlMs });
|
|
3639
|
+
}
|
|
3640
|
+
async take(state) {
|
|
3641
|
+
const entry = this.grants.get(state);
|
|
3642
|
+
if (!entry) return null;
|
|
3643
|
+
this.grants.delete(state);
|
|
3644
|
+
return entry.expiresAt >= Date.now() ? entry.grant : null;
|
|
3645
|
+
}
|
|
3646
|
+
async sweep() {
|
|
3647
|
+
const now = Date.now();
|
|
3648
|
+
for (const [state, entry] of this.grants) {
|
|
3649
|
+
if (entry.expiresAt < now) this.grants.delete(state);
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
};
|
|
2512
3653
|
var OAuthIntegration = class extends BaseIntegration {
|
|
2513
3654
|
constructor(config) {
|
|
2514
3655
|
super(config);
|
|
2515
|
-
/** Maps state token -> provider for pending authorization flows */
|
|
3656
|
+
/** Maps state token -> provider for pending MOCK authorization flows */
|
|
2516
3657
|
this.states = /* @__PURE__ */ new Map();
|
|
2517
|
-
/** Maps access token -> token set */
|
|
3658
|
+
/** Maps access token -> token set (mock backend) */
|
|
2518
3659
|
this.tokens = /* @__PURE__ */ new Map();
|
|
2519
|
-
/** Maps refresh token -> access token for refresh lookups */
|
|
3660
|
+
/** Maps refresh token -> access token for refresh lookups (mock backend) */
|
|
2520
3661
|
this.refreshIndex = /* @__PURE__ */ new Map();
|
|
2521
3662
|
/** Maps access token -> mock user session */
|
|
2522
3663
|
this.sessions = /* @__PURE__ */ new Map();
|
|
2523
|
-
|
|
3664
|
+
/** Pending OIDC authorizations (real backend) — injectable, in-memory default. */
|
|
3665
|
+
this.fallbackPending = new InMemoryPendingGrantStore();
|
|
3666
|
+
/** Maps access token -> ID-token subject, for userinfo subject checks */
|
|
3667
|
+
this.subjects = /* @__PURE__ */ new Map();
|
|
3668
|
+
/** Discovered issuer configurations, keyed by issuer URL */
|
|
3669
|
+
this.discovered = /* @__PURE__ */ new Map();
|
|
3670
|
+
this.real = config.env.OAUTH_MODE !== "mock" && Boolean(config.env.OAUTH_CLIENT_ID) && Boolean(config.env.OAUTH_CLIENT_SECRET);
|
|
3671
|
+
this.logger.info(
|
|
3672
|
+
this.real ? "OAuth integration initialized (OIDC backend via openid-client)" : "OAuth integration initialized (mock backend)"
|
|
3673
|
+
);
|
|
3674
|
+
}
|
|
3675
|
+
pendingStore() {
|
|
3676
|
+
return this.fallbackPending;
|
|
2524
3677
|
}
|
|
2525
3678
|
async execute(action, params) {
|
|
2526
3679
|
const validation = this.validateParams(action, params);
|
|
@@ -2541,19 +3694,19 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2541
3694
|
let data;
|
|
2542
3695
|
switch (action) {
|
|
2543
3696
|
case "authorize":
|
|
2544
|
-
data = await this.executeWithRetry(() => this.authorize(params));
|
|
3697
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcAuthorize(params) : this.authorize(params));
|
|
2545
3698
|
break;
|
|
2546
3699
|
case "token":
|
|
2547
|
-
data = await this.executeWithRetry(() => this.token(params));
|
|
3700
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcToken(params) : this.token(params));
|
|
2548
3701
|
break;
|
|
2549
3702
|
case "refresh":
|
|
2550
|
-
data = await this.executeWithRetry(() => this.refresh(params));
|
|
3703
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcRefresh(params) : this.refresh(params));
|
|
2551
3704
|
break;
|
|
2552
3705
|
case "revoke":
|
|
2553
|
-
data = await this.executeWithRetry(() => this.revoke(params));
|
|
3706
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcRevoke(params) : this.revoke(params));
|
|
2554
3707
|
break;
|
|
2555
3708
|
case "userinfo":
|
|
2556
|
-
data = await this.executeWithRetry(() => this.userinfo(params));
|
|
3709
|
+
data = await this.executeWithRetry(() => this.real ? this.oidcUserinfo(params) : this.userinfo(params));
|
|
2557
3710
|
break;
|
|
2558
3711
|
default:
|
|
2559
3712
|
throw new Error(`Unknown action: ${action}`);
|
|
@@ -2568,7 +3721,119 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2568
3721
|
}
|
|
2569
3722
|
}
|
|
2570
3723
|
// ---------------------------------------------------------------------------
|
|
2571
|
-
//
|
|
3724
|
+
// OIDC backend (openid-client)
|
|
3725
|
+
// ---------------------------------------------------------------------------
|
|
3726
|
+
issuerFor(provider) {
|
|
3727
|
+
const configured = this.config.env.OIDC_ISSUER_URL;
|
|
3728
|
+
if (configured) return configured;
|
|
3729
|
+
const issuer = PROVIDER_ISSUERS[provider];
|
|
3730
|
+
if (!issuer) {
|
|
3731
|
+
throw new Error(
|
|
3732
|
+
`Provider "${provider}" has no OIDC issuer \u2014 set OIDC_ISSUER_URL to an OIDC-compliant issuer, or use OAUTH_MODE=mock`
|
|
3733
|
+
);
|
|
3734
|
+
}
|
|
3735
|
+
return issuer;
|
|
3736
|
+
}
|
|
3737
|
+
async configurationFor(provider) {
|
|
3738
|
+
const issuer = this.issuerFor(provider);
|
|
3739
|
+
const cached = this.discovered.get(issuer);
|
|
3740
|
+
if (cached) return cached;
|
|
3741
|
+
const configuration = await oidc.discovery(
|
|
3742
|
+
new URL(issuer),
|
|
3743
|
+
this.config.env.OAUTH_CLIENT_ID,
|
|
3744
|
+
this.config.env.OAUTH_CLIENT_SECRET
|
|
3745
|
+
);
|
|
3746
|
+
this.discovered.set(issuer, configuration);
|
|
3747
|
+
return configuration;
|
|
3748
|
+
}
|
|
3749
|
+
async oidcAuthorize(params) {
|
|
3750
|
+
const provider = params.provider;
|
|
3751
|
+
const scopes = params.scopes;
|
|
3752
|
+
const redirectUri = params.redirectUri || this.config.env.OAUTH_REDIRECT_URI;
|
|
3753
|
+
const configuration = await this.configurationFor(provider);
|
|
3754
|
+
const state = oidc.randomState();
|
|
3755
|
+
const pkceVerifier = oidc.randomPKCECodeVerifier();
|
|
3756
|
+
const codeChallenge = await oidc.calculatePKCECodeChallenge(pkceVerifier);
|
|
3757
|
+
const parameters = {
|
|
3758
|
+
redirect_uri: redirectUri,
|
|
3759
|
+
scope: scopes.join(" "),
|
|
3760
|
+
state,
|
|
3761
|
+
code_challenge: codeChallenge,
|
|
3762
|
+
code_challenge_method: "S256"
|
|
3763
|
+
};
|
|
3764
|
+
if (provider === "google") {
|
|
3765
|
+
parameters.access_type = "offline";
|
|
3766
|
+
parameters.prompt = "consent";
|
|
3767
|
+
}
|
|
3768
|
+
const authUrl = oidc.buildAuthorizationUrl(configuration, parameters);
|
|
3769
|
+
await this.pendingStore().put(state, { provider, redirectUri, pkceVerifier }, PENDING_GRANT_TTL_MS);
|
|
3770
|
+
void this.pendingStore().sweep();
|
|
3771
|
+
return { authUrl: authUrl.toString(), state };
|
|
3772
|
+
}
|
|
3773
|
+
async oidcToken(params) {
|
|
3774
|
+
const code = params.code;
|
|
3775
|
+
const state = params.state;
|
|
3776
|
+
const pendingAuth = await this.pendingStore().take(state);
|
|
3777
|
+
if (!pendingAuth) {
|
|
3778
|
+
throw new Error(`Invalid or expired state token: ${state}`);
|
|
3779
|
+
}
|
|
3780
|
+
const configuration = await this.configurationFor(pendingAuth.provider);
|
|
3781
|
+
const callbackUrl = new URL(pendingAuth.redirectUri);
|
|
3782
|
+
callbackUrl.searchParams.set("code", code);
|
|
3783
|
+
callbackUrl.searchParams.set("state", state);
|
|
3784
|
+
const tokens = await oidc.authorizationCodeGrant(configuration, callbackUrl, {
|
|
3785
|
+
expectedState: state,
|
|
3786
|
+
pkceCodeVerifier: pendingAuth.pkceVerifier
|
|
3787
|
+
});
|
|
3788
|
+
const claims = tokens.claims();
|
|
3789
|
+
if (claims?.sub) {
|
|
3790
|
+
this.subjects.set(tokens.access_token, claims.sub);
|
|
3791
|
+
}
|
|
3792
|
+
return {
|
|
3793
|
+
accessToken: tokens.access_token,
|
|
3794
|
+
refreshToken: tokens.refresh_token ?? "",
|
|
3795
|
+
expiresIn: tokens.expires_in ?? 3600,
|
|
3796
|
+
tokenType: "bearer"
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
async oidcRefresh(params) {
|
|
3800
|
+
const refreshToken = params.refreshToken;
|
|
3801
|
+
const configuration = await this.configurationFor("google");
|
|
3802
|
+
const tokens = await oidc.refreshTokenGrant(configuration, refreshToken);
|
|
3803
|
+
const claims = tokens.claims();
|
|
3804
|
+
if (claims?.sub) {
|
|
3805
|
+
this.subjects.set(tokens.access_token, claims.sub);
|
|
3806
|
+
}
|
|
3807
|
+
return {
|
|
3808
|
+
accessToken: tokens.access_token,
|
|
3809
|
+
expiresIn: tokens.expires_in ?? 3600
|
|
3810
|
+
};
|
|
3811
|
+
}
|
|
3812
|
+
async oidcRevoke(params) {
|
|
3813
|
+
const token = params.token;
|
|
3814
|
+
const configuration = await this.configurationFor("google");
|
|
3815
|
+
await oidc.tokenRevocation(configuration, token);
|
|
3816
|
+
this.subjects.delete(token);
|
|
3817
|
+
return { revoked: true };
|
|
3818
|
+
}
|
|
3819
|
+
async oidcUserinfo(params) {
|
|
3820
|
+
const accessToken = params.accessToken;
|
|
3821
|
+
const configuration = await this.configurationFor("google");
|
|
3822
|
+
const subject = this.subjects.get(accessToken);
|
|
3823
|
+
const info = await oidc.fetchUserInfo(
|
|
3824
|
+
configuration,
|
|
3825
|
+
accessToken,
|
|
3826
|
+
subject ?? oidc.skipSubjectCheck
|
|
3827
|
+
);
|
|
3828
|
+
return {
|
|
3829
|
+
sub: info.sub,
|
|
3830
|
+
email: typeof info.email === "string" ? info.email : "",
|
|
3831
|
+
name: typeof info.name === "string" ? info.name : "",
|
|
3832
|
+
picture: typeof info.picture === "string" ? info.picture : ""
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
// ---------------------------------------------------------------------------
|
|
3836
|
+
// Mock backend helpers
|
|
2572
3837
|
// ---------------------------------------------------------------------------
|
|
2573
3838
|
/** Generate a random hex token of the given byte length. */
|
|
2574
3839
|
generateToken(bytes = 32) {
|
|
@@ -2589,7 +3854,7 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2589
3854
|
};
|
|
2590
3855
|
}
|
|
2591
3856
|
// ---------------------------------------------------------------------------
|
|
2592
|
-
//
|
|
3857
|
+
// Mock backend actions
|
|
2593
3858
|
// ---------------------------------------------------------------------------
|
|
2594
3859
|
async authorize(params) {
|
|
2595
3860
|
const provider = params.provider;
|
|
@@ -2698,19 +3963,242 @@ var OAuthIntegration = class extends BaseIntegration {
|
|
|
2698
3963
|
};
|
|
2699
3964
|
registerIntegration("oauth", OAuthIntegration);
|
|
2700
3965
|
|
|
2701
|
-
// src/integrations/
|
|
3966
|
+
// src/integrations/credentials/index.ts
|
|
3967
|
+
var ENV_VAR_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
3968
|
+
var ROLE_GATED_ACTIONS = /* @__PURE__ */ new Set(["set", "remove", "test", "rotate"]);
|
|
3969
|
+
var DEFAULT_ADMIN_ROLES = ["admin", "owner"];
|
|
3970
|
+
function isProbeService(service) {
|
|
3971
|
+
return service in serviceProbes;
|
|
3972
|
+
}
|
|
3973
|
+
var CredentialsIntegration = class extends BaseIntegration {
|
|
3974
|
+
constructor(config) {
|
|
3975
|
+
super(config);
|
|
3976
|
+
this.logger.info("Credentials integration initialized (tenant credential store surface)");
|
|
3977
|
+
}
|
|
3978
|
+
async execute(action, params, context) {
|
|
3979
|
+
if (ROLE_GATED_ACTIONS.has(action)) {
|
|
3980
|
+
const allowed = this.adminRoles();
|
|
3981
|
+
if (!context?.role || !allowed.includes(context.role)) {
|
|
3982
|
+
return {
|
|
3983
|
+
success: false,
|
|
3984
|
+
error: new IntegrationError(
|
|
3985
|
+
`forbidden: "${action}" requires one of roles: ${allowed.join(", ")}`,
|
|
3986
|
+
"AUTH_ERROR"
|
|
3987
|
+
),
|
|
3988
|
+
metadata: this.createMetadata(action, 0)
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
const validation = this.validateParams(action, params);
|
|
3993
|
+
if (!validation.valid) {
|
|
3994
|
+
return {
|
|
3995
|
+
success: false,
|
|
3996
|
+
error: {
|
|
3997
|
+
name: "IntegrationError",
|
|
3998
|
+
message: "Validation failed",
|
|
3999
|
+
code: "VALIDATION_ERROR",
|
|
4000
|
+
details: validation.errors
|
|
4001
|
+
},
|
|
4002
|
+
metadata: this.createMetadata(action, 0)
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
4005
|
+
const startTime = Date.now();
|
|
4006
|
+
try {
|
|
4007
|
+
let data;
|
|
4008
|
+
switch (action) {
|
|
4009
|
+
case "list":
|
|
4010
|
+
data = this.list(typeof params.service === "string" ? params.service : void 0);
|
|
4011
|
+
break;
|
|
4012
|
+
case "set":
|
|
4013
|
+
data = await this.set(params.service, params.envVar, params.value);
|
|
4014
|
+
break;
|
|
4015
|
+
case "remove":
|
|
4016
|
+
data = await this.remove(params.service, params.envVar);
|
|
4017
|
+
break;
|
|
4018
|
+
case "test":
|
|
4019
|
+
data = await this.test(params.service, context);
|
|
4020
|
+
break;
|
|
4021
|
+
case "rotate": {
|
|
4022
|
+
const store = getInstalledCredentialStore();
|
|
4023
|
+
if (!store) {
|
|
4024
|
+
throw new Error("No credential store is installed on this host");
|
|
4025
|
+
}
|
|
4026
|
+
data = await store.rotate();
|
|
4027
|
+
break;
|
|
4028
|
+
}
|
|
4029
|
+
default:
|
|
4030
|
+
throw new Error(`Unknown action: ${action}`);
|
|
4031
|
+
}
|
|
4032
|
+
return {
|
|
4033
|
+
success: true,
|
|
4034
|
+
data,
|
|
4035
|
+
metadata: this.createMetadata(action, Date.now() - startTime)
|
|
4036
|
+
};
|
|
4037
|
+
} catch (error) {
|
|
4038
|
+
return this.handleError(action, error);
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
adminRoles() {
|
|
4042
|
+
const raw = this.config.env["ALMADAR_CREDENTIAL_ADMIN_ROLES"] || process.env["ALMADAR_CREDENTIAL_ADMIN_ROLES"] || "";
|
|
4043
|
+
const roles = raw.split(",").map((r) => r.trim()).filter((r) => r.length > 0);
|
|
4044
|
+
return roles.length > 0 ? roles : DEFAULT_ADMIN_ROLES;
|
|
4045
|
+
}
|
|
4046
|
+
declaredFor(service) {
|
|
4047
|
+
return serviceCredentials[service] ?? [];
|
|
4048
|
+
}
|
|
4049
|
+
assertSettable(service, envVar) {
|
|
4050
|
+
if (service === "database") {
|
|
4051
|
+
if (!ENV_VAR_NAME.test(envVar)) {
|
|
4052
|
+
throw new Error(`"${envVar}" is not a well-formed connection reference (expected an env-var name)`);
|
|
4053
|
+
}
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
const declared = this.declaredFor(service);
|
|
4057
|
+
if (declared.length === 0) {
|
|
4058
|
+
throw new Error(`Service "${service}" declares no credentials`);
|
|
4059
|
+
}
|
|
4060
|
+
if (!declared.some((c) => c.envVar === envVar)) {
|
|
4061
|
+
const valid = declared.map((c) => c.envVar).join(", ");
|
|
4062
|
+
throw new Error(`"${envVar}" is not a declared credential of "${service}" (declared: ${valid})`);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
storeFirst() {
|
|
4066
|
+
const flag = this.config.env["ALMADAR_CREDENTIALS_SOURCE"] || process.env["ALMADAR_CREDENTIALS_SOURCE"];
|
|
4067
|
+
return flag === "store";
|
|
4068
|
+
}
|
|
4069
|
+
list(serviceFilter) {
|
|
4070
|
+
const store = getInstalledCredentialStore();
|
|
4071
|
+
const stored = new Map((store?.entries() ?? []).map((e) => [`${e.service}\0${e.envVar}`, e]));
|
|
4072
|
+
const entries = [];
|
|
4073
|
+
for (const [service, declared] of Object.entries(serviceCredentials)) {
|
|
4074
|
+
if (serviceFilter && service !== serviceFilter) continue;
|
|
4075
|
+
for (const { envVar, required, description } of declared) {
|
|
4076
|
+
const fromStore = stored.get(`${service}\0${envVar}`);
|
|
4077
|
+
stored.delete(`${service}\0${envVar}`);
|
|
4078
|
+
const fromEnv = this.storeFirst() ? void 0 : process.env[envVar];
|
|
4079
|
+
const source = fromStore ? "store" : fromEnv ? "env" : "none";
|
|
4080
|
+
entries.push({
|
|
4081
|
+
service,
|
|
4082
|
+
envVar,
|
|
4083
|
+
required,
|
|
4084
|
+
description,
|
|
4085
|
+
configured: source !== "none",
|
|
4086
|
+
source,
|
|
4087
|
+
last4: fromStore ? fromStore.last4 : fromEnv ? fromEnv.slice(-4) : ""
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
for (const e of stored.values()) {
|
|
4092
|
+
if (serviceFilter && e.service !== serviceFilter) continue;
|
|
4093
|
+
entries.push({
|
|
4094
|
+
service: e.service,
|
|
4095
|
+
envVar: e.envVar,
|
|
4096
|
+
required: false,
|
|
4097
|
+
description: "Stored connection reference",
|
|
4098
|
+
configured: true,
|
|
4099
|
+
source: "store",
|
|
4100
|
+
last4: e.last4
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
return { enabled: store?.enabled ?? false, entries };
|
|
4104
|
+
}
|
|
4105
|
+
async set(service, envVar, value) {
|
|
4106
|
+
const store = getInstalledCredentialStore();
|
|
4107
|
+
if (!store) {
|
|
4108
|
+
throw new Error("No credential store is installed on this host \u2014 set credentials via the environment");
|
|
4109
|
+
}
|
|
4110
|
+
this.assertSettable(service, envVar);
|
|
4111
|
+
const entry = await store.set(service, envVar, value);
|
|
4112
|
+
return { saved: true, service, envVar, last4: entry.last4 };
|
|
4113
|
+
}
|
|
4114
|
+
async remove(service, envVar) {
|
|
4115
|
+
const store = getInstalledCredentialStore();
|
|
4116
|
+
if (!store) {
|
|
4117
|
+
throw new Error("No credential store is installed on this host");
|
|
4118
|
+
}
|
|
4119
|
+
this.assertSettable(service, envVar);
|
|
4120
|
+
return { removed: await store.remove(envVar) };
|
|
4121
|
+
}
|
|
4122
|
+
async test(service, context) {
|
|
4123
|
+
const declared = this.declaredFor(service);
|
|
4124
|
+
const missing = declared.filter((c) => c.required && !resolveCredentialRef(c.envVar)).map((c) => c.envVar);
|
|
4125
|
+
const configured = missing.length === 0;
|
|
4126
|
+
if (!configured) {
|
|
4127
|
+
return { service, configured, missing, probed: false, ok: false, message: `Missing required credentials: ${missing.join(", ")}` };
|
|
4128
|
+
}
|
|
4129
|
+
const factory = getActiveFactory();
|
|
4130
|
+
const probe = isProbeService(service) ? serviceProbes[service] : void 0;
|
|
4131
|
+
if (!factory || !probe) {
|
|
4132
|
+
return { service, configured, missing, probed: false, ok: true, message: "Credentials present (no live probe declared for this service)" };
|
|
4133
|
+
}
|
|
4134
|
+
if (!factory.isConfigured(service)) {
|
|
4135
|
+
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" };
|
|
4136
|
+
}
|
|
4137
|
+
const result = await factory.execute(service, probe.action, probe.params, context);
|
|
4138
|
+
if (!result.success) {
|
|
4139
|
+
return { service, configured, missing, probed: true, ok: false, message: result.error?.message ?? `Probe ${probe.action} failed` };
|
|
4140
|
+
}
|
|
4141
|
+
const echoed = result.data !== null && typeof result.data === "object" && "_mock" in result.data;
|
|
4142
|
+
if (echoed) {
|
|
4143
|
+
return { service, configured, missing, probed: false, ok: false, message: "Probe was mock-echoed \u2014 the service is not actually configured" };
|
|
4144
|
+
}
|
|
4145
|
+
return { service, configured, missing, probed: true, ok: true, message: `Probe ${probe.action} succeeded` };
|
|
4146
|
+
}
|
|
4147
|
+
};
|
|
4148
|
+
registerIntegration("credentials", CredentialsIntegration);
|
|
4149
|
+
function isParamRecord2(value) {
|
|
4150
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
4151
|
+
}
|
|
4152
|
+
function toFilePayload(value) {
|
|
4153
|
+
if (!isParamRecord2(value)) return null;
|
|
4154
|
+
const { name, size, type, content } = value;
|
|
4155
|
+
if (typeof name !== "string") return null;
|
|
4156
|
+
return {
|
|
4157
|
+
name,
|
|
4158
|
+
size: typeof size === "number" ? size : 0,
|
|
4159
|
+
type: typeof type === "string" ? type : "application/octet-stream",
|
|
4160
|
+
content: typeof content === "string" ? content : void 0
|
|
4161
|
+
};
|
|
4162
|
+
}
|
|
4163
|
+
function decodeContent2(content) {
|
|
4164
|
+
const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
|
|
4165
|
+
if (dataUrlMatch) {
|
|
4166
|
+
const [, mime, isB64, body] = dataUrlMatch;
|
|
4167
|
+
const bytes = isB64 ? Buffer.from(body, "base64") : Buffer.from(decodeURIComponent(body), "utf8");
|
|
4168
|
+
return { bytes, contentType: mime || null };
|
|
4169
|
+
}
|
|
4170
|
+
return { bytes: Buffer.from(content, "utf8"), contentType: null };
|
|
4171
|
+
}
|
|
2702
4172
|
var StorageIntegration = class extends BaseIntegration {
|
|
2703
4173
|
constructor(config) {
|
|
2704
4174
|
super(config);
|
|
2705
4175
|
this.objects = /* @__PURE__ */ new Map();
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
4176
|
+
this.s3 = null;
|
|
4177
|
+
this.defaultBucket = config.env.STORAGE_BUCKET || "";
|
|
4178
|
+
this.publicUrlBase = config.env.STORAGE_PUBLIC_URL_BASE || "";
|
|
4179
|
+
const accessKeyId = config.env.STORAGE_ACCESS_KEY_ID || "";
|
|
4180
|
+
const secretAccessKey = config.env.STORAGE_SECRET_ACCESS_KEY || "";
|
|
4181
|
+
if (accessKeyId && secretAccessKey) {
|
|
4182
|
+
const endpoint = config.env.STORAGE_ENDPOINT || void 0;
|
|
4183
|
+
this.s3 = new S3Client({
|
|
4184
|
+
region: config.env.STORAGE_REGION || "us-east-1",
|
|
4185
|
+
endpoint,
|
|
4186
|
+
// Path-style is what MinIO/R2-style endpoints expect.
|
|
4187
|
+
forcePathStyle: Boolean(endpoint),
|
|
4188
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
4189
|
+
});
|
|
4190
|
+
this.logger.info("Storage integration initialized (S3 backend)", {
|
|
4191
|
+
endpoint: endpoint ?? "aws",
|
|
4192
|
+
bucket: this.defaultBucket
|
|
4193
|
+
});
|
|
4194
|
+
} else {
|
|
4195
|
+
if (process.env.NODE_ENV === "production") {
|
|
4196
|
+
throw new Error(
|
|
4197
|
+
"Storage credentials missing in production (STORAGE_ACCESS_KEY_ID / STORAGE_SECRET_ACCESS_KEY) \u2014 refusing the in-memory fallback. See SECRETS.md."
|
|
4198
|
+
);
|
|
4199
|
+
}
|
|
4200
|
+
this.logger.warn("Storage integration initialized (in-memory backend \u2014 dev only, nothing persists)");
|
|
2712
4201
|
}
|
|
2713
|
-
this.logger.info("Storage integration initialized (in-memory backend)");
|
|
2714
4202
|
}
|
|
2715
4203
|
async execute(action, params) {
|
|
2716
4204
|
const validation = this.validateParams(action, params);
|
|
@@ -2760,107 +4248,194 @@ var StorageIntegration = class extends BaseIntegration {
|
|
|
2760
4248
|
// ---------------------------------------------------------------------------
|
|
2761
4249
|
// Helpers
|
|
2762
4250
|
// ---------------------------------------------------------------------------
|
|
2763
|
-
|
|
4251
|
+
bucketOf(params) {
|
|
4252
|
+
return params.bucket || this.defaultBucket;
|
|
4253
|
+
}
|
|
2764
4254
|
compositeKey(bucket, key) {
|
|
2765
4255
|
return `${bucket}/${key}`;
|
|
2766
4256
|
}
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
4257
|
+
generateEtag(bytes) {
|
|
4258
|
+
return `"${createHash("md5").update(bytes).digest("hex")}"`;
|
|
4259
|
+
}
|
|
4260
|
+
/** Resolve the upload inputs from either admitted shape. */
|
|
4261
|
+
resolveUpload(params) {
|
|
4262
|
+
const file = toFilePayload(params.file);
|
|
4263
|
+
if (file) {
|
|
4264
|
+
const maxSize = typeof params.maxSize === "number" ? params.maxSize : 0;
|
|
4265
|
+
if (maxSize > 0 && file.size > maxSize) {
|
|
4266
|
+
throw new Error(`Upload rejected: ${file.name} is ${file.size} bytes (max ${maxSize})`);
|
|
4267
|
+
}
|
|
4268
|
+
if (!file.content) {
|
|
4269
|
+
throw new Error(
|
|
4270
|
+
`Upload rejected: file payload for '${file.name}' carries no content \u2014 the uploader must include the base64 data URL`
|
|
4271
|
+
);
|
|
4272
|
+
}
|
|
4273
|
+
const { bytes: bytes2, contentType: contentType2 } = decodeContent2(file.content);
|
|
4274
|
+
const safeName = file.name.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
4275
|
+
return {
|
|
4276
|
+
key: `${Date.now()}-${safeName}`,
|
|
4277
|
+
bytes: bytes2,
|
|
4278
|
+
contentType: contentType2 ?? file.type,
|
|
4279
|
+
acl: params.acl === "public" ? "public-read" : void 0
|
|
4280
|
+
};
|
|
4281
|
+
}
|
|
4282
|
+
const key = params.key;
|
|
4283
|
+
const content = params.content;
|
|
4284
|
+
if (!key || content === void 0 || content === null) {
|
|
4285
|
+
throw new Error("upload requires either `file` (with content) or the `key` + `content` pair");
|
|
2774
4286
|
}
|
|
2775
|
-
|
|
4287
|
+
const raw = typeof content === "string" ? content : JSON.stringify(content);
|
|
4288
|
+
const { bytes, contentType } = decodeContent2(raw);
|
|
4289
|
+
return {
|
|
4290
|
+
key,
|
|
4291
|
+
bytes,
|
|
4292
|
+
contentType: params.contentType || contentType || "application/octet-stream",
|
|
4293
|
+
acl: params.acl === "public" ? "public-read" : void 0
|
|
4294
|
+
};
|
|
2776
4295
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
4296
|
+
publicUrl(bucket, key) {
|
|
4297
|
+
if (this.publicUrlBase) {
|
|
4298
|
+
return `${this.publicUrlBase.replace(/\/$/, "")}/${key}`;
|
|
4299
|
+
}
|
|
4300
|
+
const endpoint = this.config.env.STORAGE_ENDPOINT;
|
|
4301
|
+
if (endpoint) {
|
|
4302
|
+
return `${endpoint.replace(/\/$/, "")}/${bucket}/${key}`;
|
|
2781
4303
|
}
|
|
2782
|
-
|
|
4304
|
+
const region = this.config.env.STORAGE_REGION || "us-east-1";
|
|
4305
|
+
return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
2783
4306
|
}
|
|
2784
4307
|
// ---------------------------------------------------------------------------
|
|
2785
4308
|
// Actions
|
|
2786
4309
|
// ---------------------------------------------------------------------------
|
|
2787
4310
|
async upload(params) {
|
|
2788
|
-
const bucket = params
|
|
2789
|
-
const key = params
|
|
2790
|
-
const
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
4311
|
+
const bucket = this.bucketOf(params);
|
|
4312
|
+
const { key, bytes, contentType, acl } = this.resolveUpload(params);
|
|
4313
|
+
const etag = this.generateEtag(bytes);
|
|
4314
|
+
this.logger.debug("Storage UPLOAD", { bucket, key, contentType, size: bytes.length });
|
|
4315
|
+
if (this.s3) {
|
|
4316
|
+
await this.s3.send(
|
|
4317
|
+
new PutObjectCommand({
|
|
4318
|
+
Bucket: bucket,
|
|
4319
|
+
Key: key,
|
|
4320
|
+
Body: bytes,
|
|
4321
|
+
ContentType: contentType,
|
|
4322
|
+
ACL: acl
|
|
4323
|
+
})
|
|
4324
|
+
);
|
|
4325
|
+
} else {
|
|
4326
|
+
this.objects.set(this.compositeKey(bucket, key), {
|
|
4327
|
+
content: bytes.toString("base64"),
|
|
4328
|
+
contentType,
|
|
4329
|
+
size: bytes.length,
|
|
4330
|
+
metadata: params.metadata ?? {},
|
|
4331
|
+
lastModified: Date.now(),
|
|
4332
|
+
etag
|
|
4333
|
+
});
|
|
4334
|
+
}
|
|
4335
|
+
const url = acl === "public-read" ? this.publicUrl(bucket, key) : (await this.signUrl(bucket, key, "get", 3600)).url;
|
|
4336
|
+
return { key, bucket, size: bytes.length, etag, id: key, url };
|
|
2806
4337
|
}
|
|
2807
4338
|
async download(params) {
|
|
2808
|
-
const bucket = params
|
|
4339
|
+
const bucket = this.bucketOf(params);
|
|
2809
4340
|
const key = params.key;
|
|
2810
4341
|
this.logger.debug("Storage DOWNLOAD", { bucket, key });
|
|
4342
|
+
if (this.s3) {
|
|
4343
|
+
const response = await this.s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
4344
|
+
const bytes = Buffer.from(await response.Body.transformToByteArray());
|
|
4345
|
+
return {
|
|
4346
|
+
content: bytes.toString("base64"),
|
|
4347
|
+
contentType: response.ContentType ?? "application/octet-stream",
|
|
4348
|
+
size: bytes.length,
|
|
4349
|
+
metadata: {}
|
|
4350
|
+
};
|
|
4351
|
+
}
|
|
2811
4352
|
const obj = this.objects.get(this.compositeKey(bucket, key));
|
|
2812
4353
|
if (!obj) {
|
|
2813
4354
|
throw new Error(`Object not found: ${bucket}/${key}`);
|
|
2814
4355
|
}
|
|
2815
4356
|
return {
|
|
2816
|
-
content: obj.content,
|
|
4357
|
+
content: String(obj.content),
|
|
2817
4358
|
contentType: obj.contentType,
|
|
2818
4359
|
size: obj.size,
|
|
2819
4360
|
metadata: obj.metadata
|
|
2820
4361
|
};
|
|
2821
4362
|
}
|
|
2822
4363
|
async list(params) {
|
|
2823
|
-
const bucket = params
|
|
4364
|
+
const bucket = this.bucketOf(params);
|
|
2824
4365
|
const prefix = params.prefix ?? "";
|
|
2825
4366
|
const maxKeys = params.maxKeys ?? 1e3;
|
|
4367
|
+
const continuationToken = params.continuationToken;
|
|
2826
4368
|
this.logger.debug("Storage LIST", { bucket, prefix, maxKeys });
|
|
4369
|
+
if (this.s3) {
|
|
4370
|
+
const response = await this.s3.send(
|
|
4371
|
+
new ListObjectsV2Command({
|
|
4372
|
+
Bucket: bucket,
|
|
4373
|
+
Prefix: prefix || void 0,
|
|
4374
|
+
MaxKeys: maxKeys,
|
|
4375
|
+
ContinuationToken: continuationToken
|
|
4376
|
+
})
|
|
4377
|
+
);
|
|
4378
|
+
return {
|
|
4379
|
+
keys: (response.Contents ?? []).map((entry) => ({
|
|
4380
|
+
key: entry.Key ?? "",
|
|
4381
|
+
size: entry.Size ?? 0,
|
|
4382
|
+
lastModified: entry.LastModified?.getTime() ?? 0
|
|
4383
|
+
})),
|
|
4384
|
+
truncated: Boolean(response.IsTruncated),
|
|
4385
|
+
...response.IsTruncated && response.NextContinuationToken !== void 0 ? { nextToken: response.NextContinuationToken } : {}
|
|
4386
|
+
};
|
|
4387
|
+
}
|
|
2827
4388
|
const bucketPrefix = `${bucket}/`;
|
|
2828
4389
|
const fullPrefix = `${bucket}/${prefix}`;
|
|
2829
4390
|
const results = [];
|
|
2830
4391
|
for (const [compositeKey, obj] of this.objects) {
|
|
2831
4392
|
if (!compositeKey.startsWith(fullPrefix)) continue;
|
|
2832
|
-
const objectKey = compositeKey.slice(bucketPrefix.length);
|
|
2833
4393
|
results.push({
|
|
2834
|
-
key:
|
|
4394
|
+
key: compositeKey.slice(bucketPrefix.length),
|
|
2835
4395
|
size: obj.size,
|
|
2836
4396
|
lastModified: obj.lastModified
|
|
2837
4397
|
});
|
|
2838
4398
|
}
|
|
2839
4399
|
results.sort((a, b) => a.key.localeCompare(b.key));
|
|
2840
|
-
const
|
|
4400
|
+
const offset = continuationToken !== void 0 ? Number.parseInt(continuationToken, 10) || 0 : 0;
|
|
4401
|
+
const page = results.slice(offset, offset + maxKeys);
|
|
4402
|
+
const truncated = offset + maxKeys < results.length;
|
|
2841
4403
|
return {
|
|
2842
|
-
keys:
|
|
2843
|
-
truncated
|
|
4404
|
+
keys: page,
|
|
4405
|
+
truncated,
|
|
4406
|
+
...truncated ? { nextToken: String(offset + maxKeys) } : {}
|
|
2844
4407
|
};
|
|
2845
4408
|
}
|
|
2846
4409
|
async deleteObject(params) {
|
|
2847
|
-
const bucket = params
|
|
4410
|
+
const bucket = this.bucketOf(params);
|
|
2848
4411
|
const key = params.key;
|
|
2849
4412
|
this.logger.debug("Storage DELETE", { bucket, key });
|
|
4413
|
+
if (this.s3) {
|
|
4414
|
+
await this.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
4415
|
+
return { deleted: true };
|
|
4416
|
+
}
|
|
2850
4417
|
const existed = this.objects.has(this.compositeKey(bucket, key));
|
|
2851
4418
|
this.objects.delete(this.compositeKey(bucket, key));
|
|
2852
4419
|
return { deleted: existed };
|
|
2853
4420
|
}
|
|
4421
|
+
async signUrl(bucket, key, operation, expiresIn) {
|
|
4422
|
+
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
4423
|
+
if (this.s3) {
|
|
4424
|
+
const command = operation === "put" ? new PutObjectCommand({ Bucket: bucket, Key: key }) : new GetObjectCommand({ Bucket: bucket, Key: key });
|
|
4425
|
+
const url2 = await getSignedUrl(this.s3, command, { expiresIn });
|
|
4426
|
+
return { url: url2, expiresAt };
|
|
4427
|
+
}
|
|
4428
|
+
const token = createHash("sha256").update(`${bucket}/${key}/${expiresAt}`).digest("hex").slice(0, 16);
|
|
4429
|
+
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}`;
|
|
4430
|
+
return { url, expiresAt };
|
|
4431
|
+
}
|
|
2854
4432
|
async getSignedUrl(params) {
|
|
2855
|
-
const bucket = params
|
|
4433
|
+
const bucket = this.bucketOf(params);
|
|
2856
4434
|
const key = params.key;
|
|
2857
4435
|
const expiresIn = params.expiresIn ?? 3600;
|
|
2858
4436
|
const operation = params.operation ?? "get";
|
|
2859
4437
|
this.logger.debug("Storage GET_SIGNED_URL", { bucket, key, expiresIn, operation });
|
|
2860
|
-
|
|
2861
|
-
const token = Math.random().toString(36).slice(2, 18);
|
|
2862
|
-
const url = `https://storage.mock.local/${bucket}/${key}?X-Amz-Algorithm=MOCK-HMAC-SHA256&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host&X-Amz-Signature=${token}&operation=${operation}`;
|
|
2863
|
-
return { url, expiresAt };
|
|
4438
|
+
return this.signUrl(bucket, key, operation, expiresIn);
|
|
2864
4439
|
}
|
|
2865
4440
|
};
|
|
2866
4441
|
registerIntegration("storage", StorageIntegration);
|
|
@@ -3367,10 +4942,10 @@ var DatabaseIntegration = class extends BaseIntegration {
|
|
|
3367
4942
|
}
|
|
3368
4943
|
/** Resolve (and cache) the driver for a connection reference. */
|
|
3369
4944
|
driverFor(connectionRef) {
|
|
3370
|
-
const connectionString =
|
|
4945
|
+
const connectionString = resolveCredentialRef(connectionRef);
|
|
3371
4946
|
if (!connectionString) {
|
|
3372
4947
|
throw new IntegrationError(
|
|
3373
|
-
`Connection reference "${connectionRef}" is not set in the environment`,
|
|
4948
|
+
`Connection reference "${connectionRef}" is not set in the credential store or environment`,
|
|
3374
4949
|
"AUTH_ERROR"
|
|
3375
4950
|
);
|
|
3376
4951
|
}
|
|
@@ -3670,8 +5245,8 @@ var MockIntegration = class extends BaseIntegration {
|
|
|
3670
5245
|
|
|
3671
5246
|
// src/runtime/effectHandler.ts
|
|
3672
5247
|
function createCallServiceHandler(factory) {
|
|
3673
|
-
const handler = async (service, action, params) => {
|
|
3674
|
-
const result = await factory.execute(service, action, params || {});
|
|
5248
|
+
const handler = async (service, action, params, context) => {
|
|
5249
|
+
const result = await factory.execute(service, action, params || {}, context);
|
|
3675
5250
|
if (!result.success) {
|
|
3676
5251
|
throw result.error;
|
|
3677
5252
|
}
|
|
@@ -3681,6 +5256,19 @@ function createCallServiceHandler(factory) {
|
|
|
3681
5256
|
}
|
|
3682
5257
|
|
|
3683
5258
|
// src/runtime/RuntimeIntegrationManager.ts
|
|
5259
|
+
var STORE_BOOTSTRAP_ENV_VARS = /* @__PURE__ */ new Set([
|
|
5260
|
+
"ALMADAR_CREDENTIAL_MASTER_KEY",
|
|
5261
|
+
"ALMADAR_CREDENTIAL_MASTER_KEY_PREVIOUS"
|
|
5262
|
+
]);
|
|
5263
|
+
function declaredCredentialEnvVars() {
|
|
5264
|
+
const vars = /* @__PURE__ */ new Set();
|
|
5265
|
+
for (const decls of Object.values(serviceCredentials)) {
|
|
5266
|
+
for (const { envVar } of decls) {
|
|
5267
|
+
if (!STORE_BOOTSTRAP_ENV_VARS.has(envVar)) vars.add(envVar);
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
return vars;
|
|
5271
|
+
}
|
|
3684
5272
|
function generateMockFromShape(shape) {
|
|
3685
5273
|
const data = {};
|
|
3686
5274
|
for (const [key, type] of Object.entries(shape)) {
|
|
@@ -3708,8 +5296,69 @@ function generateMockFromShape(shape) {
|
|
|
3708
5296
|
}
|
|
3709
5297
|
var RuntimeIntegrationManager = class {
|
|
3710
5298
|
constructor() {
|
|
5299
|
+
this.credentialStore = null;
|
|
5300
|
+
this.storeEnvBase = null;
|
|
5301
|
+
this.storeFirst = false;
|
|
3711
5302
|
this.factory = new IntegrationFactory();
|
|
3712
5303
|
this.installNotConfiguredFallback();
|
|
5304
|
+
installActiveFactory(this.factory);
|
|
5305
|
+
}
|
|
5306
|
+
/**
|
|
5307
|
+
* W4: configure with the tenant credential store layered over the env —
|
|
5308
|
+
* store → env → unconfigured. Installs the store as the process-wide
|
|
5309
|
+
* `resolveCredentialRef` source, warms it, and re-configures whenever a
|
|
5310
|
+
* credential changes (dropping cached instances so new keys go live
|
|
5311
|
+
* without a restart). Mock mode short-circuits inside `configureFromEnv`
|
|
5312
|
+
* exactly as before, so verify harnesses are unaffected.
|
|
5313
|
+
*/
|
|
5314
|
+
async configureFromStore(store, envOverride) {
|
|
5315
|
+
this.credentialStore = store;
|
|
5316
|
+
this.storeEnvBase = envOverride ?? process.env;
|
|
5317
|
+
this.storeFirst = this.storeEnvBase.ALMADAR_CREDENTIALS_SOURCE === "store";
|
|
5318
|
+
installCredentialStore(store);
|
|
5319
|
+
await store.warm();
|
|
5320
|
+
if (this.storeFirst && store.enabled) {
|
|
5321
|
+
await this.seedStoreFromEnv(store, this.storeEnvBase);
|
|
5322
|
+
}
|
|
5323
|
+
store.subscribe(() => this.refreshFromStore());
|
|
5324
|
+
this.refreshFromStore();
|
|
5325
|
+
}
|
|
5326
|
+
/** Whether store-first custody (I-31) is active on this manager. */
|
|
5327
|
+
get credentialsStoreFirst() {
|
|
5328
|
+
return this.storeFirst;
|
|
5329
|
+
}
|
|
5330
|
+
/** Copy each declared credential present in env but absent from the store. */
|
|
5331
|
+
async seedStoreFromEnv(store, env) {
|
|
5332
|
+
const present = new Set(store.entries().map((e) => e.envVar));
|
|
5333
|
+
for (const [service, decls] of Object.entries(serviceCredentials)) {
|
|
5334
|
+
for (const { envVar } of decls) {
|
|
5335
|
+
if (STORE_BOOTSTRAP_ENV_VARS.has(envVar)) continue;
|
|
5336
|
+
const value = env[envVar];
|
|
5337
|
+
if (value && !present.has(envVar)) {
|
|
5338
|
+
await store.set(service, envVar, value);
|
|
5339
|
+
present.add(envVar);
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
/**
|
|
5345
|
+
* Re-derive configs from base env + warmed store values. `reset()` (not
|
|
5346
|
+
* `invalidate()`): configs must go too, or a REMOVED credential's service
|
|
5347
|
+
* resurrects from its stale config on the next `get()` — removal must
|
|
5348
|
+
* revoke. `configureFromEnv` below rebuilds every configured service from
|
|
5349
|
+
* the merged env.
|
|
5350
|
+
*/
|
|
5351
|
+
refreshFromStore() {
|
|
5352
|
+
if (!this.credentialStore || !this.storeEnvBase) return;
|
|
5353
|
+
this.factory.reset();
|
|
5354
|
+
let base = this.storeEnvBase;
|
|
5355
|
+
if (this.storeFirst) {
|
|
5356
|
+
const declared = declaredCredentialEnvVars();
|
|
5357
|
+
base = Object.fromEntries(
|
|
5358
|
+
Object.entries(this.storeEnvBase).filter(([key]) => !declared.has(key))
|
|
5359
|
+
);
|
|
5360
|
+
}
|
|
5361
|
+
this.configureFromEnv({ ...base, ...this.credentialStore.snapshotEnv() });
|
|
3713
5362
|
}
|
|
3714
5363
|
/**
|
|
3715
5364
|
* Wrap `factory.execute` so an unknown/unconfigured service echoes its
|
|
@@ -3720,15 +5369,18 @@ var RuntimeIntegrationManager = class {
|
|
|
3720
5369
|
* events fire — only the two "not set up" errors (`Unknown integration`,
|
|
3721
5370
|
* `Integration not configured`) are caught. Subsumes the broader wrapper
|
|
3722
5371
|
* that previously lived inside `configureMockMode`.
|
|
5372
|
+
*
|
|
5373
|
+
* In production the fallback is OFF: a missing key must surface as the
|
|
5374
|
+
* circuit's failure event and a degraded health check, never a fake success.
|
|
3723
5375
|
*/
|
|
3724
5376
|
installNotConfiguredFallback() {
|
|
3725
5377
|
const originalExecute = this.factory.execute.bind(this.factory);
|
|
3726
|
-
this.factory.execute = async (integration, action, params) => {
|
|
5378
|
+
this.factory.execute = async (integration, action, params, context) => {
|
|
3727
5379
|
try {
|
|
3728
|
-
return await originalExecute(integration, action, params);
|
|
5380
|
+
return await originalExecute(integration, action, params, context);
|
|
3729
5381
|
} catch (err) {
|
|
3730
5382
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3731
|
-
if (/Unknown integration|not configured/i.test(msg)) {
|
|
5383
|
+
if (process.env.NODE_ENV !== "production" && /Unknown integration|not configured/i.test(msg)) {
|
|
3732
5384
|
return {
|
|
3733
5385
|
success: true,
|
|
3734
5386
|
data: { ...params, _mock: true, _service: integration, _action: action },
|
|
@@ -3742,10 +5394,18 @@ var RuntimeIntegrationManager = class {
|
|
|
3742
5394
|
/**
|
|
3743
5395
|
* Configure from environment variables.
|
|
3744
5396
|
* In mock mode (USE_MOCK_DATA=true), all services return realistic mock data.
|
|
5397
|
+
*
|
|
5398
|
+
* Pass an explicit env map to configure hermetically (verify harnesses,
|
|
5399
|
+
* per-tenant resolution); defaults to `process.env`.
|
|
3745
5400
|
*/
|
|
3746
|
-
configureFromEnv() {
|
|
3747
|
-
const env = process.env;
|
|
3748
|
-
if (env.USE_MOCK_DATA === "true") {
|
|
5401
|
+
configureFromEnv(envOverride) {
|
|
5402
|
+
const env = envOverride ?? process.env;
|
|
5403
|
+
if (env.ALMADAR_INTEGRATIONS_MODE === "mock" || env.USE_MOCK_DATA === "true") {
|
|
5404
|
+
if (env.NODE_ENV === "production") {
|
|
5405
|
+
throw new Error(
|
|
5406
|
+
"Mocked integrations are not permitted when NODE_ENV=production \u2014 unset ALMADAR_INTEGRATIONS_MODE/USE_MOCK_DATA."
|
|
5407
|
+
);
|
|
5408
|
+
}
|
|
3749
5409
|
this.configureMockMode();
|
|
3750
5410
|
return;
|
|
3751
5411
|
}
|
|
@@ -3789,6 +5449,88 @@ var RuntimeIntegrationManager = class {
|
|
|
3789
5449
|
}
|
|
3790
5450
|
});
|
|
3791
5451
|
}
|
|
5452
|
+
if (env.VAPID_PUBLIC_KEY && env.VAPID_PRIVATE_KEY && env.VAPID_SUBJECT) {
|
|
5453
|
+
this.factory.configure("push", {
|
|
5454
|
+
env: {
|
|
5455
|
+
VAPID_PUBLIC_KEY: env.VAPID_PUBLIC_KEY,
|
|
5456
|
+
VAPID_PRIVATE_KEY: env.VAPID_PRIVATE_KEY,
|
|
5457
|
+
VAPID_SUBJECT: env.VAPID_SUBJECT
|
|
5458
|
+
}
|
|
5459
|
+
});
|
|
5460
|
+
}
|
|
5461
|
+
if (env.GOOGLE_CALENDAR_SA_KEY) {
|
|
5462
|
+
this.factory.configure("calendar", {
|
|
5463
|
+
env: {
|
|
5464
|
+
GOOGLE_CALENDAR_SA_KEY: env.GOOGLE_CALENDAR_SA_KEY,
|
|
5465
|
+
GOOGLE_CALENDAR_SUBJECT: env.GOOGLE_CALENDAR_SUBJECT || "",
|
|
5466
|
+
GOOGLE_CALENDAR_ID: env.GOOGLE_CALENDAR_ID || ""
|
|
5467
|
+
}
|
|
5468
|
+
});
|
|
5469
|
+
}
|
|
5470
|
+
if (env.GOOGLE_DRIVE_SA_KEY || env.GOOGLE_DRIVE_REFRESH_TOKEN && env.OAUTH_CLIENT_ID && env.OAUTH_CLIENT_SECRET) {
|
|
5471
|
+
this.factory.configure("drive", {
|
|
5472
|
+
env: {
|
|
5473
|
+
GOOGLE_DRIVE_SA_KEY: env.GOOGLE_DRIVE_SA_KEY || "",
|
|
5474
|
+
GOOGLE_DRIVE_SUBJECT: env.GOOGLE_DRIVE_SUBJECT || "",
|
|
5475
|
+
GOOGLE_DRIVE_REFRESH_TOKEN: env.GOOGLE_DRIVE_REFRESH_TOKEN || "",
|
|
5476
|
+
GOOGLE_DRIVE_FOLDER_ID: env.GOOGLE_DRIVE_FOLDER_ID || "",
|
|
5477
|
+
OAUTH_CLIENT_ID: env.OAUTH_CLIENT_ID || "",
|
|
5478
|
+
OAUTH_CLIENT_SECRET: env.OAUTH_CLIENT_SECRET || ""
|
|
5479
|
+
}
|
|
5480
|
+
});
|
|
5481
|
+
}
|
|
5482
|
+
if (env.META_ACCESS_TOKEN) {
|
|
5483
|
+
this.factory.configure("metaAds", {
|
|
5484
|
+
env: {
|
|
5485
|
+
META_ACCESS_TOKEN: env.META_ACCESS_TOKEN,
|
|
5486
|
+
META_AD_ACCOUNT_ID: env.META_AD_ACCOUNT_ID || ""
|
|
5487
|
+
}
|
|
5488
|
+
});
|
|
5489
|
+
}
|
|
5490
|
+
this.factory.configure("accounting", { env: {} });
|
|
5491
|
+
if (env.GOCARDLESS_SECRET_ID && env.GOCARDLESS_SECRET_KEY) {
|
|
5492
|
+
this.factory.configure("banking", {
|
|
5493
|
+
env: {
|
|
5494
|
+
GOCARDLESS_SECRET_ID: env.GOCARDLESS_SECRET_ID,
|
|
5495
|
+
GOCARDLESS_SECRET_KEY: env.GOCARDLESS_SECRET_KEY
|
|
5496
|
+
}
|
|
5497
|
+
});
|
|
5498
|
+
}
|
|
5499
|
+
if (env.DOCUSIGN_BASE_URL && env.DOCUSIGN_ACCESS_TOKEN) {
|
|
5500
|
+
this.factory.configure("esign", {
|
|
5501
|
+
env: {
|
|
5502
|
+
DOCUSIGN_BASE_URL: env.DOCUSIGN_BASE_URL,
|
|
5503
|
+
DOCUSIGN_ACCESS_TOKEN: env.DOCUSIGN_ACCESS_TOKEN
|
|
5504
|
+
}
|
|
5505
|
+
});
|
|
5506
|
+
}
|
|
5507
|
+
if (env.OAUTH_CLIENT_ID && env.OAUTH_CLIENT_SECRET || env.OAUTH_MODE === "mock") {
|
|
5508
|
+
this.factory.configure("oauth", {
|
|
5509
|
+
env: {
|
|
5510
|
+
OAUTH_CLIENT_ID: env.OAUTH_CLIENT_ID || "",
|
|
5511
|
+
OAUTH_CLIENT_SECRET: env.OAUTH_CLIENT_SECRET || "",
|
|
5512
|
+
OAUTH_REDIRECT_URI: env.OAUTH_REDIRECT_URI || "",
|
|
5513
|
+
OIDC_ISSUER_URL: env.OIDC_ISSUER_URL || "",
|
|
5514
|
+
OAUTH_MODE: env.OAUTH_MODE || ""
|
|
5515
|
+
}
|
|
5516
|
+
});
|
|
5517
|
+
}
|
|
5518
|
+
this.factory.configure("credentials", {
|
|
5519
|
+
env: {
|
|
5520
|
+
ALMADAR_CREDENTIAL_ADMIN_ROLES: env.ALMADAR_CREDENTIAL_ADMIN_ROLES || "",
|
|
5521
|
+
ALMADAR_CREDENTIALS_SOURCE: env.ALMADAR_CREDENTIALS_SOURCE || ""
|
|
5522
|
+
}
|
|
5523
|
+
});
|
|
5524
|
+
this.factory.configure("storage", {
|
|
5525
|
+
env: {
|
|
5526
|
+
STORAGE_ACCESS_KEY_ID: env.STORAGE_ACCESS_KEY_ID || "",
|
|
5527
|
+
STORAGE_SECRET_ACCESS_KEY: env.STORAGE_SECRET_ACCESS_KEY || "",
|
|
5528
|
+
STORAGE_BUCKET: env.STORAGE_BUCKET || "",
|
|
5529
|
+
STORAGE_REGION: env.STORAGE_REGION || "",
|
|
5530
|
+
STORAGE_ENDPOINT: env.STORAGE_ENDPOINT || "",
|
|
5531
|
+
STORAGE_PUBLIC_URL_BASE: env.STORAGE_PUBLIC_URL_BASE || ""
|
|
5532
|
+
}
|
|
5533
|
+
});
|
|
3792
5534
|
this.factory.configure("webhook", {
|
|
3793
5535
|
env: {
|
|
3794
5536
|
WEBHOOK_SIGNING_SECRET: env.WEBHOOK_SIGNING_SECRET || "",
|