@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.
@@ -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 { createHmac } from 'crypto';
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
- if (this.instances.has(name)) {
236
- return this.instances.get(name);
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(name, instance);
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(to, subject, body, from) {
1016
+ async sendViaSendGrid(message) {
831
1017
  const msg = {
832
- to,
833
- from,
834
- subject,
835
- html: body
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(to, subject, body, from) {
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(`Webhook endpoint returned ${response.status}`);
1998
+ throw new Error(`DocuSign returned ${response.status}`);
926
1999
  }
927
- return {
928
- status: response.status,
929
- ok: response.ok,
930
- durationMs: Date.now() - startTime
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("webhook", WebhookIntegration);
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
- this.logger.info("OAuth integration initialized (mock backend)");
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
- // Helpers
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
- // Actions
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/storage/index.ts
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
- const storageUrl = config.env.STORAGE_URL;
2707
- if (storageUrl) {
2708
- this.logger.warn(
2709
- "STORAGE_URL is configured but real storage client is not yet implemented. Falling back to in-memory store.",
2710
- { storageUrl }
2711
- );
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
- /** Build a composite key from bucket and object key. */
4251
+ bucketOf(params) {
4252
+ return params.bucket || this.defaultBucket;
4253
+ }
2764
4254
  compositeKey(bucket, key) {
2765
4255
  return `${bucket}/${key}`;
2766
4256
  }
2767
- /** Generate a deterministic etag from content. */
2768
- generateEtag(content) {
2769
- const raw = typeof content === "string" ? content : JSON.stringify(content);
2770
- let hash = 0;
2771
- for (let i = 0; i < raw.length; i++) {
2772
- const ch = raw.charCodeAt(i);
2773
- hash = (hash << 5) - hash + ch | 0;
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
- return `"${Math.abs(hash).toString(16).padStart(8, "0")}"`;
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
- /** Compute the byte size of content. */
2778
- computeSize(content) {
2779
- if (typeof content === "string") {
2780
- return new TextEncoder().encode(content).byteLength;
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
- return JSON.stringify(content).length;
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.bucket;
2789
- const key = params.key;
2790
- const content = params.content;
2791
- const contentType = params.contentType ?? "application/octet-stream";
2792
- const metadata = params.metadata ?? {};
2793
- this.logger.debug("Storage UPLOAD", { bucket, key, contentType });
2794
- const size = this.computeSize(content);
2795
- const etag = this.generateEtag(content);
2796
- const obj = {
2797
- content,
2798
- contentType,
2799
- size,
2800
- metadata,
2801
- lastModified: Date.now(),
2802
- etag
2803
- };
2804
- this.objects.set(this.compositeKey(bucket, key), obj);
2805
- return { key, bucket, size, etag };
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.bucket;
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.bucket;
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: objectKey,
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 truncated = results.length > maxKeys;
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: results.slice(0, maxKeys),
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.bucket;
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.bucket;
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
- const expiresAt = Date.now() + expiresIn * 1e3;
2861
- const token = Math.random().toString(36).slice(2, 18);
2862
- const url = `https://storage.mock.local/${bucket}/${key}?X-Amz-Algorithm=MOCK-HMAC-SHA256&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host&X-Amz-Signature=${token}&operation=${operation}`;
2863
- return { url, expiresAt };
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 = process.env[connectionRef];
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 || "",