@almadar/integrations 2.24.0 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,17 +1,22 @@
1
1
  import { createLogger } from '@almadar/logger';
2
2
  import { integratorsRegistry } from '@almadar/core/patterns';
3
+ import { randomBytes, createCipheriv, createDecipheriv, createHmac, createHash } from 'crypto';
4
+ import { readFileSync, mkdirSync, writeFileSync, renameSync, mkdtempSync, rmSync, promises } from 'fs';
5
+ import { join, dirname } from 'path';
3
6
  import Stripe 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
 
17
22
  // src/types.ts
@@ -33,6 +38,143 @@ var IntegrationError = class extends Error {
33
38
  };
34
39
  }
35
40
  };
41
+
42
+ // src/contracts.ts
43
+ var serviceCredentials = {
44
+ stripe: [
45
+ { envVar: "STRIPE_SECRET_KEY", required: true, description: "Stripe secret API key" },
46
+ { envVar: "STRIPE_WEBHOOK_SECRET", required: false, description: "Webhook endpoint signing secret" }
47
+ ],
48
+ youtube: [
49
+ { envVar: "YOUTUBE_API_KEY", required: true, description: "YouTube Data API key" }
50
+ ],
51
+ twilio: [
52
+ { envVar: "TWILIO_ACCOUNT_SID", required: true, description: "Twilio account SID" },
53
+ { envVar: "TWILIO_AUTH_TOKEN", required: true, description: "Twilio auth token" },
54
+ { envVar: "TWILIO_PHONE_NUMBER", required: false, description: "Default sender phone number" }
55
+ ],
56
+ email: [
57
+ { envVar: "SENDGRID_API_KEY", required: false, description: "SendGrid API key (use this OR RESEND_API_KEY)" },
58
+ { envVar: "RESEND_API_KEY", required: false, description: "Resend API key (use this OR SENDGRID_API_KEY)" },
59
+ { envVar: "FROM_EMAIL", required: false, description: "Default sender email address" }
60
+ ],
61
+ webhook: [
62
+ { envVar: "WEBHOOK_SIGNING_SECRET", required: false, description: "Default HMAC-SHA256 signing secret (per-call secret overrides)" },
63
+ { envVar: "WEBHOOK_TIMEOUT_MS", required: false, description: "Per-request timeout (ms, default 10000)" }
64
+ ],
65
+ push: [
66
+ { envVar: "VAPID_PUBLIC_KEY", required: true, description: "VAPID public key (web-push generate-vapid-keys)" },
67
+ { envVar: "VAPID_PRIVATE_KEY", required: true, description: "VAPID private key" },
68
+ { envVar: "VAPID_SUBJECT", required: true, description: "VAPID subject (mailto: or https: contact URI)" }
69
+ ],
70
+ calendar: [
71
+ { 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" },
72
+ { envVar: "GOOGLE_CALENDAR_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" },
73
+ { envVar: "GOOGLE_CALENDAR_ID", required: false, description: "Default calendar id (default: primary)" },
74
+ { envVar: "GOOGLE_CALENDAR_CHANNEL_TOKEN", required: false, description: "Watch-channel verification token \u2014 required to receive inbound calendar hooks (two-way sync)" }
75
+ ],
76
+ drive: [
77
+ { 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" },
78
+ { envVar: "GOOGLE_DRIVE_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" },
79
+ { 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)" },
80
+ { envVar: "GOOGLE_DRIVE_FOLDER_ID", required: false, description: "Default parent folder for uploads/new folders when the call names none" }
81
+ ],
82
+ metaAds: [
83
+ { envVar: "META_ACCESS_TOKEN", required: true, description: "Meta Graph API access token (Marketing API, ads_read)" },
84
+ { envVar: "META_AD_ACCOUNT_ID", required: false, description: "Default ad account id (act_\u2026); per-call accountId overrides" }
85
+ ],
86
+ accounting: [],
87
+ banking: [
88
+ { envVar: "GOCARDLESS_SECRET_ID", required: true, description: "GoCardless Bank Account Data secret id" },
89
+ { envVar: "GOCARDLESS_SECRET_KEY", required: true, description: "GoCardless Bank Account Data secret key" }
90
+ ],
91
+ esign: [
92
+ { envVar: "DOCUSIGN_BASE_URL", required: true, description: "DocuSign REST base (e.g. https://demo.docusign.net/restapi/v2.1/accounts/<accountId>)" },
93
+ { envVar: "DOCUSIGN_ACCESS_TOKEN", required: true, description: "DocuSign OAuth access token (JWT grant rotation is the deployment concern)" }
94
+ ],
95
+ llm: [
96
+ { envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key (use this OR OPENAI_API_KEY)" },
97
+ { envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key (use this OR ANTHROPIC_API_KEY)" }
98
+ ],
99
+ "llm-integration": [
100
+ { envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key" },
101
+ { envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key" }
102
+ ],
103
+ ml: [
104
+ { envVar: "MASAR_URL", required: true, description: "Base URL of the deployed model-serving orbital" },
105
+ { envVar: "MASAR_ML_TRAIT", required: true, description: "Kebab-case trait name the serving orbital exposes its /events route under" }
106
+ ],
107
+ deepagent: [
108
+ { envVar: "DEEPAGENT_API_URL", required: true, description: "DeepAgent server URL" },
109
+ { envVar: "DEEPAGENT_API_KEY", required: false, description: "DeepAgent API key" }
110
+ ],
111
+ github: [
112
+ { envVar: "GITHUB_TOKEN", required: true, description: "GitHub personal access token" },
113
+ { envVar: "GITHUB_OWNER", required: false, description: "Default repository owner" },
114
+ { envVar: "GITHUB_REPO", required: false, description: "Default repository name" }
115
+ ],
116
+ docker: [
117
+ { envVar: "DOCKER_HOST", required: false, description: "Docker daemon host URL" }
118
+ ],
119
+ storage: [
120
+ { envVar: "STORAGE_ACCESS_KEY_ID", required: true, description: "S3-compatible access key id" },
121
+ { envVar: "STORAGE_SECRET_ACCESS_KEY", required: true, description: "S3-compatible secret access key" },
122
+ { envVar: "STORAGE_BUCKET", required: true, description: "Default bucket name" },
123
+ { envVar: "STORAGE_REGION", required: false, description: "Region (default us-east-1)" },
124
+ { envVar: "STORAGE_ENDPOINT", required: false, description: "Custom S3-compatible endpoint (R2/MinIO/\u2026); empty = AWS S3" },
125
+ { envVar: "STORAGE_PUBLIC_URL_BASE", required: false, description: "Base URL for public-acl object links; empty = endpoint-derived" }
126
+ ],
127
+ queue: [
128
+ { envVar: "QUEUE_URL", required: true, description: "Message queue connection URL" }
129
+ ],
130
+ redis: [
131
+ { envVar: "REDIS_URL", required: true, description: "Redis connection URL" }
132
+ ],
133
+ oauth: [
134
+ { envVar: "OAUTH_CLIENT_ID", required: true, description: "OIDC client ID" },
135
+ { envVar: "OAUTH_CLIENT_SECRET", required: true, description: "OIDC client secret" },
136
+ { envVar: "OAUTH_REDIRECT_URI", required: false, description: "OIDC redirect URI (default per-call param)" },
137
+ { envVar: "OIDC_ISSUER_URL", required: false, description: "OIDC issuer URL (default https://accounts.google.com)" }
138
+ ],
139
+ credentials: [
140
+ { 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" },
141
+ { 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" }
142
+ ],
143
+ otel: [
144
+ { envVar: "OTEL_EXPORTER_OTLP_ENDPOINT", required: true, description: "OpenTelemetry collector endpoint" },
145
+ { envVar: "OTEL_SERVICE_NAME", required: false, description: "Service name for traces" }
146
+ ],
147
+ cli: [],
148
+ // No fixed env vars — connection strings are resolved per-query from the
149
+ // caller-supplied connectionRef, so credentials cannot be declared statically.
150
+ database: [],
151
+ wikimedia: [
152
+ { envVar: "WIKIMEDIA_USER_AGENT", required: false, description: "Descriptive User-Agent for the Wikipedia API" },
153
+ { envVar: "WIKIMEDIA_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
154
+ ],
155
+ iconify: [
156
+ { envVar: "ICONIFY_USER_AGENT", required: false, description: "User-Agent for the Iconify API" },
157
+ { envVar: "ICONIFY_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
158
+ ],
159
+ arxiv: [
160
+ { envVar: "ARXIV_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
161
+ ]
162
+ };
163
+ var serviceHooks = {
164
+ calendar: [
165
+ {
166
+ provider: "google-calendar",
167
+ event: "CAL_REMOTE_CHANGED",
168
+ credentialEnv: "GOOGLE_CALENDAR_CHANNEL_TOKEN",
169
+ providerExport: "googleCalendarHookProvider"
170
+ }
171
+ ]
172
+ };
173
+ var serviceProbes = {
174
+ calendar: { action: "listEvents", params: { maxResults: 1 } },
175
+ drive: { action: "listFiles", params: { maxResults: 1 } },
176
+ metaAds: { action: "listCampaigns", params: {} }
177
+ };
36
178
  var ConsoleLogger = class {
37
179
  constructor(_level = "info") {
38
180
  this.log = createLogger("almadar:integrations");
@@ -50,6 +192,7 @@ var ConsoleLogger = class {
50
192
  this.log.error(message, meta);
51
193
  }
52
194
  };
195
+ var RESERVED_ENVELOPE_KEYS = /* @__PURE__ */ new Set(["emit", "onSuccess", "onError", "timeout"]);
53
196
  function validateParams(integration, action, params) {
54
197
  const typedRegistry = integratorsRegistry;
55
198
  const registry = typedRegistry.integrators[integration];
@@ -72,6 +215,16 @@ function validateParams(integration, action, params) {
72
215
  };
73
216
  }
74
217
  const errors = [];
218
+ const declaredNames = new Set(actionDef.params.map((p) => p.name));
219
+ for (const key of Object.keys(params)) {
220
+ if (RESERVED_ENVELOPE_KEYS.has(key)) continue;
221
+ if (!declaredNames.has(key)) {
222
+ errors.push({
223
+ param: key,
224
+ message: `Unknown parameter: ${key} (declared: ${[...declaredNames].sort().join(", ")})`
225
+ });
226
+ }
227
+ }
75
228
  for (const paramDef of actionDef.params) {
76
229
  if (paramDef.required && !(paramDef.name in params)) {
77
230
  errors.push({
@@ -222,56 +375,80 @@ function getRegisteredIntegrations() {
222
375
  }
223
376
 
224
377
  // src/factory.ts
378
+ function instanceKey(name, principal) {
379
+ return principal ? `${name}\0${principal}` : name;
380
+ }
225
381
  var IntegrationFactory = class {
226
382
  constructor() {
227
383
  this.instances = /* @__PURE__ */ new Map();
228
384
  this.configs = /* @__PURE__ */ new Map();
229
385
  }
230
386
  /**
231
- * Configure an integration (doesn't instantiate yet)
387
+ * Configure an integration (doesn't instantiate yet). A `principal` scopes
388
+ * the config to that principal; the app-wide config (no principal) is the
389
+ * fallback for every principal.
232
390
  */
233
- configure(name, config) {
234
- this.configs.set(name, { name, ...config });
391
+ configure(name, config, principal) {
392
+ this.configs.set(instanceKey(name, principal), { name, ...config });
235
393
  }
236
394
  /**
237
- * Get or create an integration instance
395
+ * Get or create an integration instance. Principal-scoped lookups fall
396
+ * back to the app-wide config when no per-principal config exists.
238
397
  */
239
- get(name) {
240
- if (this.instances.has(name)) {
241
- return this.instances.get(name);
398
+ get(name, principal) {
399
+ const key = instanceKey(name, principal);
400
+ const cached = this.instances.get(key);
401
+ if (cached) {
402
+ return cached;
242
403
  }
243
404
  const Constructor = getIntegration(name);
244
405
  if (!Constructor) {
245
406
  throw new Error(`Unknown integration: ${name}. Make sure it's imported.`);
246
407
  }
247
- const config = this.configs.get(name);
408
+ const config = this.configs.get(key) ?? this.configs.get(name);
248
409
  if (!config) {
249
410
  throw new Error(
250
411
  `Integration not configured: ${name}. Call configure() first.`
251
412
  );
252
413
  }
253
414
  const instance = new Constructor(config);
254
- this.instances.set(name, instance);
415
+ this.instances.set(key, instance);
255
416
  return instance;
256
417
  }
257
418
  /**
258
419
  * Execute an action on an integration
259
420
  */
260
- async execute(integration, action, params) {
261
- const instance = this.get(integration);
262
- return await instance.execute(action, params);
421
+ async execute(integration, action, params, context) {
422
+ const instance = this.get(integration, context?.principal);
423
+ return await instance.execute(action, params, context);
263
424
  }
264
425
  /**
265
426
  * Check if integration is configured
266
427
  */
267
- isConfigured(name) {
268
- return this.configs.has(name);
428
+ isConfigured(name, principal) {
429
+ return this.configs.has(instanceKey(name, principal)) || this.configs.has(name);
269
430
  }
270
431
  /**
271
432
  * Register an integration instance directly (used by mock infrastructure)
272
433
  */
273
- registerInstance(name, instance) {
274
- this.instances.set(name, instance);
434
+ registerInstance(name, instance, principal) {
435
+ this.instances.set(instanceKey(name, principal), instance);
436
+ }
437
+ /**
438
+ * Drop the cached instance(s) for a name so the next `get` rebuilds from
439
+ * the current config — how a credential change goes live without restart.
440
+ * Configs are kept; without a name, every instance is dropped.
441
+ */
442
+ invalidate(name) {
443
+ if (name === void 0) {
444
+ this.instances.clear();
445
+ return;
446
+ }
447
+ for (const key of this.instances.keys()) {
448
+ if (key === name || key.startsWith(`${name}\0`)) {
449
+ this.instances.delete(key);
450
+ }
451
+ }
275
452
  }
276
453
  /**
277
454
  * Clear all instances (useful for testing)
@@ -298,6 +475,302 @@ function resetIntegrationFactory() {
298
475
  _factory?.reset();
299
476
  _factory = null;
300
477
  }
478
+ var CREDENTIAL_ENTITY_TYPE = "AlmadarIntegrationCredential";
479
+ var CREDENTIAL_MASTER_KEY_ENV = "ALMADAR_CREDENTIAL_MASTER_KEY";
480
+ var CREDENTIAL_PREVIOUS_MASTER_KEY_ENV = "ALMADAR_CREDENTIAL_MASTER_KEY_PREVIOUS";
481
+ var ENCRYPTION_ALGORITHM = "aes-256-gcm";
482
+ function isNonEmptyString(v) {
483
+ return typeof v === "string" && v.length > 0;
484
+ }
485
+ function keyIdOf(masterKeyHex) {
486
+ return createHash("sha256").update(masterKeyHex, "hex").digest("hex").slice(0, 16);
487
+ }
488
+ var MASTER_KEY_SHAPE = /^[0-9a-fA-F]{64}$/;
489
+ var CredentialStore = class {
490
+ constructor(adapter, masterKey, previousMasterKey) {
491
+ this.byEnvVar = /* @__PURE__ */ new Map();
492
+ this.listeners = /* @__PURE__ */ new Set();
493
+ this.warmed = false;
494
+ this.adapter = adapter;
495
+ const key = masterKey ?? process.env[CREDENTIAL_MASTER_KEY_ENV];
496
+ const valid = typeof key === "string" && MASTER_KEY_SHAPE.test(key);
497
+ if (!valid && process.env.NODE_ENV === "production") {
498
+ throw new Error(
499
+ `${CREDENTIAL_MASTER_KEY_ENV} must be a 64-character hex string (32 bytes) in production \u2014 generate one with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
500
+ );
501
+ }
502
+ this.masterKeyHex = valid ? key : void 0;
503
+ const prev = previousMasterKey ?? process.env[CREDENTIAL_PREVIOUS_MASTER_KEY_ENV];
504
+ this.previousKeyHex = typeof prev === "string" && MASTER_KEY_SHAPE.test(prev) ? prev : void 0;
505
+ }
506
+ /** True when a valid master key is present (writes and decryption enabled). */
507
+ get enabled() {
508
+ return this.masterKeyHex !== void 0;
509
+ }
510
+ /** Subscribe to credential changes; returns an unsubscribe function. */
511
+ subscribe(listener) {
512
+ this.listeners.add(listener);
513
+ return () => this.listeners.delete(listener);
514
+ }
515
+ notify() {
516
+ for (const listener of this.listeners) listener();
517
+ }
518
+ encrypt(plaintext) {
519
+ if (!this.masterKeyHex) {
520
+ throw new Error(`${CREDENTIAL_MASTER_KEY_ENV} is not configured \u2014 cannot store credentials`);
521
+ }
522
+ const iv = randomBytes(12);
523
+ const cipher = createCipheriv(ENCRYPTION_ALGORITHM, Buffer.from(this.masterKeyHex, "hex"), iv);
524
+ let ciphertext = cipher.update(plaintext, "utf8", "hex");
525
+ ciphertext += cipher.final("hex");
526
+ return {
527
+ ciphertext,
528
+ iv: iv.toString("hex"),
529
+ authTag: cipher.getAuthTag().toString("hex"),
530
+ keyId: keyIdOf(this.masterKeyHex)
531
+ };
532
+ }
533
+ decryptWithKey(keyHex, ciphertext, ivHex, authTagHex) {
534
+ const decipher = createDecipheriv(
535
+ ENCRYPTION_ALGORITHM,
536
+ Buffer.from(keyHex, "hex"),
537
+ Buffer.from(ivHex, "hex")
538
+ );
539
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
540
+ let plaintext = decipher.update(ciphertext, "hex", "utf8");
541
+ plaintext += decipher.final("utf8");
542
+ return plaintext;
543
+ }
544
+ /**
545
+ * Decrypt one row, selecting the key by the row's stamped `keyId` (I-28):
546
+ * current key first, then the rotation-window previous key. Legacy rows
547
+ * (no `keyId`) try current then previous. Returns null when no held key
548
+ * decrypts the row — the row is unreadable, never silently wrong (GCM
549
+ * auth tags make a wrong-key success impossible).
550
+ */
551
+ decryptRow(ciphertext, ivHex, authTagHex, rowKeyId) {
552
+ if (!this.masterKeyHex) {
553
+ throw new Error(`${CREDENTIAL_MASTER_KEY_ENV} is not configured \u2014 cannot decrypt credentials`);
554
+ }
555
+ const candidates = [];
556
+ if (rowKeyId === void 0 || rowKeyId === keyIdOf(this.masterKeyHex)) {
557
+ candidates.push(this.masterKeyHex);
558
+ }
559
+ if (this.previousKeyHex !== void 0 && (rowKeyId === void 0 || rowKeyId === keyIdOf(this.previousKeyHex))) {
560
+ candidates.push(this.previousKeyHex);
561
+ }
562
+ for (const key of candidates) {
563
+ try {
564
+ return this.decryptWithKey(key, ciphertext, ivHex, authTagHex);
565
+ } catch {
566
+ }
567
+ }
568
+ return null;
569
+ }
570
+ /**
571
+ * Load and decrypt every stored row into memory. Returns the number of
572
+ * resolvable credentials. Without a master key nothing decrypts (rows are
573
+ * left in place, resolution falls through to env).
574
+ */
575
+ async warm() {
576
+ this.byEnvVar.clear();
577
+ this.warmed = true;
578
+ if (!this.enabled) return 0;
579
+ const rows = await this.adapter.list(CREDENTIAL_ENTITY_TYPE);
580
+ for (const row of rows) {
581
+ const { id, service, envVar, ciphertext, iv, authTag } = row;
582
+ if (!isNonEmptyString(id) || !isNonEmptyString(service) || !isNonEmptyString(envVar) || !isNonEmptyString(ciphertext) || !isNonEmptyString(iv) || !isNonEmptyString(authTag)) {
583
+ continue;
584
+ }
585
+ const value = this.decryptRow(
586
+ ciphertext,
587
+ iv,
588
+ authTag,
589
+ isNonEmptyString(row.keyId) ? row.keyId : void 0
590
+ );
591
+ if (value === null) continue;
592
+ this.byEnvVar.set(envVar, {
593
+ id,
594
+ service,
595
+ envVar,
596
+ value,
597
+ last4: value.slice(-4),
598
+ updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : 0
599
+ });
600
+ }
601
+ return this.byEnvVar.size;
602
+ }
603
+ /** Resolve one credential by env-var name (warmed values only). */
604
+ resolve(envVar) {
605
+ return this.byEnvVar.get(envVar)?.value;
606
+ }
607
+ /** All warmed values as an env-shaped map, for merging over the process env. */
608
+ snapshotEnv() {
609
+ const env = {};
610
+ for (const [envVar, entry] of this.byEnvVar) env[envVar] = entry.value;
611
+ return env;
612
+ }
613
+ /** Masked entries for display — never includes plaintext. */
614
+ entries() {
615
+ return Array.from(this.byEnvVar.values()).map(({ service, envVar, last4, updatedAt }) => ({ service, envVar, last4, updatedAt })).sort((a, b) => a.service.localeCompare(b.service) || a.envVar.localeCompare(b.envVar));
616
+ }
617
+ /** Upsert one credential (encrypts, persists, re-warms the entry, notifies). */
618
+ async set(service, envVar, value) {
619
+ if (!this.warmed) await this.warm();
620
+ const { ciphertext, iv, authTag, keyId } = this.encrypt(value);
621
+ const updatedAt = Date.now();
622
+ const existing = this.byEnvVar.get(envVar);
623
+ const data = { service, envVar, ciphertext, iv, authTag, keyId, updatedAt };
624
+ let id;
625
+ if (existing) {
626
+ id = existing.id;
627
+ await this.adapter.update(CREDENTIAL_ENTITY_TYPE, id, data);
628
+ } else {
629
+ ({ id } = await this.adapter.create(CREDENTIAL_ENTITY_TYPE, data));
630
+ }
631
+ const entry = { id, service, envVar, value, last4: value.slice(-4), updatedAt };
632
+ this.byEnvVar.set(envVar, entry);
633
+ this.notify();
634
+ const { service: s, envVar: e, last4, updatedAt: u } = entry;
635
+ return { service: s, envVar: e, last4, updatedAt: u };
636
+ }
637
+ /**
638
+ * Re-encrypt every readable row under the CURRENT master key (I-28).
639
+ * Rotation window: deploy the new key as `ALMADAR_CREDENTIAL_MASTER_KEY`
640
+ * with the old one as `..._PREVIOUS`, call `rotate()`, then retire the
641
+ * previous key. Rows no held key can read are counted, never dropped.
642
+ */
643
+ async rotate() {
644
+ if (!this.masterKeyHex) {
645
+ throw new Error(`${CREDENTIAL_MASTER_KEY_ENV} is not configured \u2014 cannot rotate`);
646
+ }
647
+ const currentKeyId = keyIdOf(this.masterKeyHex);
648
+ let rotated = 0;
649
+ let alreadyCurrent = 0;
650
+ let unreadable = 0;
651
+ const rows = await this.adapter.list(CREDENTIAL_ENTITY_TYPE);
652
+ for (const row of rows) {
653
+ const { id, service, envVar, ciphertext, iv, authTag } = row;
654
+ if (!isNonEmptyString(id) || !isNonEmptyString(service) || !isNonEmptyString(envVar) || !isNonEmptyString(ciphertext) || !isNonEmptyString(iv) || !isNonEmptyString(authTag)) {
655
+ unreadable += 1;
656
+ continue;
657
+ }
658
+ const rowKeyId = isNonEmptyString(row.keyId) ? row.keyId : void 0;
659
+ if (rowKeyId === currentKeyId) {
660
+ alreadyCurrent += 1;
661
+ continue;
662
+ }
663
+ const value = this.decryptRow(ciphertext, iv, authTag, rowKeyId);
664
+ if (value === null) {
665
+ unreadable += 1;
666
+ continue;
667
+ }
668
+ if (rowKeyId === void 0 && this.decryptRow(ciphertext, iv, authTag, currentKeyId) !== null) {
669
+ alreadyCurrent += 1;
670
+ await this.adapter.update(CREDENTIAL_ENTITY_TYPE, id, { ...row, keyId: currentKeyId });
671
+ continue;
672
+ }
673
+ const fresh = this.encrypt(value);
674
+ await this.adapter.update(CREDENTIAL_ENTITY_TYPE, id, {
675
+ service,
676
+ envVar,
677
+ ciphertext: fresh.ciphertext,
678
+ iv: fresh.iv,
679
+ authTag: fresh.authTag,
680
+ keyId: fresh.keyId,
681
+ updatedAt: Date.now()
682
+ });
683
+ rotated += 1;
684
+ }
685
+ await this.warm();
686
+ this.notify();
687
+ return { rotated, alreadyCurrent, unreadable };
688
+ }
689
+ /** Delete one credential row; resolution falls back to env afterwards. */
690
+ async remove(envVar) {
691
+ if (!this.warmed) await this.warm();
692
+ const existing = this.byEnvVar.get(envVar);
693
+ if (!existing) return false;
694
+ await this.adapter.delete(CREDENTIAL_ENTITY_TYPE, existing.id);
695
+ this.byEnvVar.delete(envVar);
696
+ this.notify();
697
+ return true;
698
+ }
699
+ };
700
+
701
+ // src/credentials/resolver.ts
702
+ var installedStore = null;
703
+ var activeFactory = null;
704
+ function installActiveFactory(factory) {
705
+ activeFactory = factory;
706
+ }
707
+ function getActiveFactory() {
708
+ return activeFactory;
709
+ }
710
+ function installCredentialStore(store) {
711
+ installedStore = store;
712
+ }
713
+ function getInstalledCredentialStore() {
714
+ return installedStore;
715
+ }
716
+ function uninstallCredentialStore() {
717
+ installedStore = null;
718
+ }
719
+ function resolveCredentialRef(ref, env = process.env) {
720
+ return installedStore?.resolve(ref) ?? env[ref];
721
+ }
722
+ var CREDENTIALS_FILE_ENV = "ALMADAR_CREDENTIALS_FILE";
723
+ var DEFAULT_RELATIVE_PATH = join(".almadar", "dev-credentials.json");
724
+ var FileCredentialPersistence = class {
725
+ constructor(path) {
726
+ this.counter = 0;
727
+ this.path = path ?? process.env[CREDENTIALS_FILE_ENV] ?? join(process.cwd(), DEFAULT_RELATIVE_PATH);
728
+ }
729
+ get filePath() {
730
+ return this.path;
731
+ }
732
+ load() {
733
+ try {
734
+ const raw = readFileSync(this.path, "utf-8");
735
+ const rows = JSON.parse(raw);
736
+ const map = /* @__PURE__ */ new Map();
737
+ for (const row of rows) {
738
+ if (typeof row.id === "string") map.set(row.id, row);
739
+ }
740
+ return map;
741
+ } catch {
742
+ return /* @__PURE__ */ new Map();
743
+ }
744
+ }
745
+ save(rows) {
746
+ mkdirSync(dirname(this.path), { recursive: true });
747
+ const tmp = `${this.path}.tmp`;
748
+ writeFileSync(tmp, JSON.stringify([...rows.values()], null, 2), "utf-8");
749
+ renameSync(tmp, this.path);
750
+ }
751
+ async create(entityType, data) {
752
+ const rows = this.load();
753
+ const id = `${entityType}-${Date.now()}-${++this.counter}`;
754
+ rows.set(id, { ...data, id });
755
+ this.save(rows);
756
+ return { id };
757
+ }
758
+ async update(_entityType, id, data) {
759
+ const rows = this.load();
760
+ const existing = rows.get(id);
761
+ if (existing) {
762
+ rows.set(id, { ...existing, ...data, id });
763
+ this.save(rows);
764
+ }
765
+ }
766
+ async delete(_entityType, id) {
767
+ const rows = this.load();
768
+ if (rows.delete(id)) this.save(rows);
769
+ }
770
+ async list(_entityType) {
771
+ return [...this.load().values()];
772
+ }
773
+ };
301
774
  var STRIPE_API_VERSION = "2025-02-24.acacia";
302
775
  function priceToTier(priceId, prices) {
303
776
  if (priceId === prices.solo) return "solo";
@@ -863,10 +1336,10 @@ var TwilioIntegration = class extends BaseIntegration {
863
1336
  }
864
1337
  }
865
1338
  async sendSMS(params) {
866
- const { to, body } = params;
1339
+ const { to, body, from } = params;
867
1340
  this.logger.debug("Sending SMS", { to: String(to ?? "") });
868
1341
  const message = await this.client.messages.create({
869
- from: this.phoneNumber,
1342
+ from: from || this.phoneNumber,
870
1343
  to,
871
1344
  body
872
1345
  });
@@ -945,61 +1418,1015 @@ var EmailIntegration = class extends BaseIntegration {
945
1418
  }
946
1419
  }
947
1420
  async send(params) {
948
- const { to, subject, body, from } = params;
1421
+ const { to, subject, body, from, htmlBody, replyTo, templateId } = params;
949
1422
  this.logger.debug("Sending email", { to: String(to), subject: String(subject), provider: this.provider });
1423
+ const message = {
1424
+ to,
1425
+ subject,
1426
+ body,
1427
+ from: from || this.fromEmail,
1428
+ htmlBody: htmlBody || void 0,
1429
+ replyTo: replyTo || void 0,
1430
+ templateId: templateId || void 0
1431
+ };
950
1432
  if (this.provider === "sendgrid") {
951
- return await this.sendViaSendGrid(
952
- to,
953
- subject,
954
- body,
955
- from || this.fromEmail
956
- );
1433
+ return await this.sendViaSendGrid(message);
957
1434
  } else if (this.provider === "resend") {
958
- return await this.sendViaResend(
959
- to,
960
- subject,
961
- body,
962
- from || this.fromEmail
963
- );
1435
+ return await this.sendViaResend(message);
1436
+ }
1437
+ throw new Error(`Unknown email provider: ${this.provider}`);
1438
+ }
1439
+ async sendViaSendGrid(message) {
1440
+ const msg = {
1441
+ to: message.to,
1442
+ from: message.from,
1443
+ subject: message.subject,
1444
+ // htmlBody present → it carries the HTML and body becomes the
1445
+ // plain-text alternative; absent → body renders as HTML (legacy).
1446
+ html: message.htmlBody ?? message.body,
1447
+ ...message.htmlBody ? { text: message.body } : {},
1448
+ ...message.replyTo ? { replyTo: message.replyTo } : {},
1449
+ ...message.templateId ? { templateId: message.templateId } : {}
1450
+ };
1451
+ const response = await sgMail.send(msg);
1452
+ return {
1453
+ id: response[0].headers["x-message-id"],
1454
+ status: "sent"
1455
+ };
1456
+ }
1457
+ async sendViaResend(message) {
1458
+ if (!this.resendClient) {
1459
+ throw new Error("Resend client not initialized");
1460
+ }
1461
+ if (message.templateId) {
1462
+ throw new Error("templateId is not supported by the resend provider \u2014 use sendgrid or drop templateId");
1463
+ }
1464
+ const response = await this.resendClient.emails.send({
1465
+ from: message.from,
1466
+ to: message.to,
1467
+ subject: message.subject,
1468
+ html: message.htmlBody ?? message.body,
1469
+ ...message.htmlBody ? { text: message.body } : {},
1470
+ ...message.replyTo ? { replyTo: message.replyTo } : {}
1471
+ });
1472
+ return {
1473
+ id: response.data?.id,
1474
+ status: "sent"
1475
+ };
1476
+ }
1477
+ };
1478
+ registerIntegration("email", EmailIntegration);
1479
+ var WebhookIntegration = class extends BaseIntegration {
1480
+ constructor(config) {
1481
+ super(config);
1482
+ this.signingSecret = config.env.WEBHOOK_SIGNING_SECRET || "";
1483
+ this.timeoutMs = Number(config.env.WEBHOOK_TIMEOUT_MS) || 1e4;
1484
+ this.logger.info("Webhook integration initialized");
1485
+ }
1486
+ async execute(action, params) {
1487
+ const validation = this.validateParams(action, params);
1488
+ if (!validation.valid) {
1489
+ return {
1490
+ success: false,
1491
+ error: {
1492
+ name: "IntegrationError",
1493
+ message: "Validation failed",
1494
+ code: "VALIDATION_ERROR",
1495
+ details: validation.errors
1496
+ },
1497
+ metadata: this.createMetadata(action, 0)
1498
+ };
1499
+ }
1500
+ const startTime = Date.now();
1501
+ let retries = 0;
1502
+ try {
1503
+ let data;
1504
+ switch (action) {
1505
+ case "send":
1506
+ data = await this.executeWithRetry(() => this.send(params));
1507
+ break;
1508
+ default:
1509
+ throw new Error(`Unknown action: ${action}`);
1510
+ }
1511
+ return {
1512
+ success: true,
1513
+ data,
1514
+ metadata: this.createMetadata(action, Date.now() - startTime, retries)
1515
+ };
1516
+ } catch (error) {
1517
+ return this.handleError(action, error);
1518
+ }
1519
+ }
1520
+ async send(params) {
1521
+ const { url, event, payload, secret } = params;
1522
+ const body = JSON.stringify({
1523
+ event,
1524
+ payload: payload ?? {},
1525
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1526
+ });
1527
+ const headers = {
1528
+ "Content-Type": "application/json",
1529
+ "X-Almadar-Event": event
1530
+ };
1531
+ const signingSecret = secret || this.signingSecret;
1532
+ if (signingSecret) {
1533
+ headers["X-Almadar-Signature"] = "sha256=" + createHmac("sha256", signingSecret).update(body).digest("hex");
1534
+ }
1535
+ this.logger.debug("Sending webhook", { url: String(url), event: String(event) });
1536
+ const startTime = Date.now();
1537
+ const response = await fetch(url, {
1538
+ method: "POST",
1539
+ headers,
1540
+ body,
1541
+ signal: AbortSignal.timeout(this.timeoutMs)
1542
+ });
1543
+ if (response.status >= 500) {
1544
+ throw new Error(`Webhook endpoint returned ${response.status}`);
1545
+ }
1546
+ return {
1547
+ status: response.status,
1548
+ ok: response.ok,
1549
+ durationMs: Date.now() - startTime
1550
+ };
1551
+ }
1552
+ };
1553
+ registerIntegration("webhook", WebhookIntegration);
1554
+ function isParamRecord(value) {
1555
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
1556
+ }
1557
+ function toSubscription(value) {
1558
+ if (isParamRecord(value)) {
1559
+ const { endpoint, keys } = value;
1560
+ if (typeof endpoint === "string" && isParamRecord(keys)) {
1561
+ const { p256dh, auth } = keys;
1562
+ if (typeof p256dh === "string" && typeof auth === "string") {
1563
+ return { endpoint, keys: { p256dh, auth } };
1564
+ }
1565
+ }
1566
+ }
1567
+ throw new Error("push.send: subscription must be { endpoint, keys: { p256dh, auth } }");
1568
+ }
1569
+ var PushIntegration = class extends BaseIntegration {
1570
+ constructor(config) {
1571
+ super(config);
1572
+ this.vapidPublicKey = config.env.VAPID_PUBLIC_KEY || "";
1573
+ this.vapidPrivateKey = config.env.VAPID_PRIVATE_KEY || "";
1574
+ this.vapidSubject = config.env.VAPID_SUBJECT || "";
1575
+ this.logger.info("Push integration initialized");
1576
+ }
1577
+ async execute(action, params) {
1578
+ const validation = this.validateParams(action, params);
1579
+ if (!validation.valid) {
1580
+ return {
1581
+ success: false,
1582
+ error: {
1583
+ name: "IntegrationError",
1584
+ message: "Validation failed",
1585
+ code: "VALIDATION_ERROR",
1586
+ details: validation.errors
1587
+ },
1588
+ metadata: this.createMetadata(action, 0)
1589
+ };
1590
+ }
1591
+ const startTime = Date.now();
1592
+ let retries = 0;
1593
+ try {
1594
+ let data;
1595
+ switch (action) {
1596
+ case "send":
1597
+ data = await this.executeWithRetry(() => this.send(params));
1598
+ break;
1599
+ default:
1600
+ throw new Error(`Unknown action: ${action}`);
1601
+ }
1602
+ return {
1603
+ success: true,
1604
+ data,
1605
+ metadata: this.createMetadata(action, Date.now() - startTime, retries)
1606
+ };
1607
+ } catch (error) {
1608
+ return this.handleError(action, error);
1609
+ }
1610
+ }
1611
+ async send(params) {
1612
+ const { subscription, title, body, url, icon } = params;
1613
+ const sub = toSubscription(subscription);
1614
+ const payload = JSON.stringify({
1615
+ title,
1616
+ body,
1617
+ url: url || void 0,
1618
+ icon: icon || void 0
1619
+ });
1620
+ this.logger.debug("Sending push notification", { endpoint: sub.endpoint });
1621
+ try {
1622
+ const response = await webpush.sendNotification(sub, payload, {
1623
+ vapidDetails: {
1624
+ subject: this.vapidSubject,
1625
+ publicKey: this.vapidPublicKey,
1626
+ privateKey: this.vapidPrivateKey
1627
+ }
1628
+ });
1629
+ return { statusCode: response.statusCode, ok: true, expired: false };
1630
+ } catch (error) {
1631
+ if (error instanceof webpush.WebPushError) {
1632
+ if (error.statusCode >= 500) {
1633
+ throw new Error(`Push endpoint returned ${error.statusCode}`);
1634
+ }
1635
+ return {
1636
+ statusCode: error.statusCode,
1637
+ ok: false,
1638
+ expired: error.statusCode === 404 || error.statusCode === 410
1639
+ };
1640
+ }
1641
+ throw error;
1642
+ }
1643
+ }
1644
+ };
1645
+ registerIntegration("push", PushIntegration);
1646
+
1647
+ // src/integrations/calendar/webhooks.ts
1648
+ function header(headers, name) {
1649
+ const direct = headers[name] ?? headers[name.toLowerCase()];
1650
+ if (direct !== void 0) return direct;
1651
+ const found = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
1652
+ return (found !== void 0 ? headers[found] : void 0) ?? "";
1653
+ }
1654
+ function parseCalendarPushNotification(headers, expectedToken) {
1655
+ const channelId = header(headers, "x-goog-channel-id");
1656
+ const resourceId = header(headers, "x-goog-resource-id");
1657
+ const resourceState = header(headers, "x-goog-resource-state");
1658
+ if (!channelId || !resourceId || !resourceState) {
1659
+ return { error: "missing-headers" };
1660
+ }
1661
+ if (expectedToken && header(headers, "x-goog-channel-token") !== expectedToken) {
1662
+ return { error: "bad-token" };
1663
+ }
1664
+ return {
1665
+ type: "calendar.changed",
1666
+ channelId,
1667
+ resourceId,
1668
+ resourceState,
1669
+ messageNumber: Number(header(headers, "x-goog-message-number")) || 0
1670
+ };
1671
+ }
1672
+ function googleCalendarHookProvider(expectedToken) {
1673
+ return (input) => {
1674
+ const parsed = parseCalendarPushNotification(input.headers, expectedToken);
1675
+ if ("error" in parsed) return { error: parsed.error };
1676
+ if (parsed.resourceState === "sync") return { ack: true };
1677
+ return {
1678
+ event: "CAL_REMOTE_CHANGED",
1679
+ payload: {
1680
+ channelId: parsed.channelId,
1681
+ resourceId: parsed.resourceId,
1682
+ resourceState: parsed.resourceState
1683
+ }
1684
+ };
1685
+ };
1686
+ }
1687
+ var CalendarIntegration = class extends BaseIntegration {
1688
+ constructor(config) {
1689
+ super(config);
1690
+ const rawKey = config.env.GOOGLE_CALENDAR_SA_KEY;
1691
+ if (!rawKey) {
1692
+ throw new Error("GOOGLE_CALENDAR_SA_KEY not configured");
1693
+ }
1694
+ const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
1695
+ const key = JSON.parse(keyJson);
1696
+ const subject = config.env.GOOGLE_CALENDAR_SUBJECT || void 0;
1697
+ const auth = new google.auth.JWT({
1698
+ email: key.client_email,
1699
+ key: key.private_key,
1700
+ scopes: ["https://www.googleapis.com/auth/calendar"],
1701
+ subject
1702
+ });
1703
+ this.client = google.calendar({ version: "v3", auth });
1704
+ this.defaultCalendarId = config.env.GOOGLE_CALENDAR_ID || "primary";
1705
+ this.logger.info("Calendar integration initialized", {
1706
+ delegated: Boolean(subject)
1707
+ });
1708
+ }
1709
+ async execute(action, params) {
1710
+ const validation = this.validateParams(action, params);
1711
+ if (!validation.valid) {
1712
+ return {
1713
+ success: false,
1714
+ error: {
1715
+ name: "IntegrationError",
1716
+ message: "Validation failed",
1717
+ code: "VALIDATION_ERROR",
1718
+ details: validation.errors
1719
+ },
1720
+ metadata: this.createMetadata(action, 0)
1721
+ };
1722
+ }
1723
+ const startTime = Date.now();
1724
+ let retries = 0;
1725
+ try {
1726
+ let data;
1727
+ switch (action) {
1728
+ case "listEvents":
1729
+ data = await this.executeWithRetry(() => this.listEvents(params));
1730
+ break;
1731
+ case "createEvent":
1732
+ data = await this.executeWithRetry(() => this.createEvent(params));
1733
+ break;
1734
+ case "updateEvent":
1735
+ data = await this.executeWithRetry(() => this.updateEvent(params));
1736
+ break;
1737
+ case "deleteEvent":
1738
+ data = await this.executeWithRetry(() => this.deleteEvent(params));
1739
+ break;
1740
+ case "freeBusy":
1741
+ data = await this.executeWithRetry(() => this.freeBusy(params));
1742
+ break;
1743
+ case "watch":
1744
+ data = await this.executeWithRetry(() => this.watch(params));
1745
+ break;
1746
+ case "stopWatch":
1747
+ data = await this.executeWithRetry(() => this.stopWatch(params));
1748
+ break;
1749
+ default:
1750
+ throw new Error(`Unknown action: ${action}`);
1751
+ }
1752
+ return {
1753
+ success: true,
1754
+ data,
1755
+ metadata: this.createMetadata(action, Date.now() - startTime, retries)
1756
+ };
1757
+ } catch (error) {
1758
+ return this.handleError(action, error);
1759
+ }
1760
+ }
1761
+ calendarId(params) {
1762
+ return params.calendarId || this.defaultCalendarId;
1763
+ }
1764
+ toEventTime(value) {
1765
+ return value.length === 10 ? { date: value } : { dateTime: value };
1766
+ }
1767
+ fromEventTime(time) {
1768
+ return time?.dateTime ?? time?.date ?? "";
1769
+ }
1770
+ async listEvents(params) {
1771
+ const { timeMin, timeMax, syncToken, maxResults } = params;
1772
+ const response = await this.client.events.list({
1773
+ calendarId: this.calendarId(params),
1774
+ // Incremental sync: a syncToken supersedes the window params (the API
1775
+ // rejects combining them).
1776
+ ...syncToken ? { syncToken } : {
1777
+ timeMin: timeMin || void 0,
1778
+ timeMax: timeMax || void 0,
1779
+ singleEvents: true,
1780
+ orderBy: "startTime"
1781
+ },
1782
+ maxResults: maxResults || 250
1783
+ });
1784
+ const items = response.data.items ?? [];
1785
+ return {
1786
+ events: items.map((event) => ({
1787
+ id: event.id ?? "",
1788
+ summary: event.summary ?? "",
1789
+ description: event.description ?? "",
1790
+ location: event.location ?? "",
1791
+ start: this.fromEventTime(event.start ?? void 0),
1792
+ end: this.fromEventTime(event.end ?? void 0),
1793
+ status: event.status ?? "",
1794
+ updated: event.updated ?? ""
1795
+ })),
1796
+ nextSyncToken: response.data.nextSyncToken ?? null
1797
+ };
1798
+ }
1799
+ resolveEnd(start, end, durationMinutes) {
1800
+ if (end) return end;
1801
+ if (start.length === 10) {
1802
+ const next = new Date(Date.parse(start) + 24 * 60 * 6e4);
1803
+ return next.toISOString().slice(0, 10);
1804
+ }
1805
+ if (durationMinutes && durationMinutes > 0) {
1806
+ return new Date(Date.parse(start) + durationMinutes * 6e4).toISOString();
1807
+ }
1808
+ throw new Error("createEvent requires `end` or a positive `durationMinutes`");
1809
+ }
1810
+ async createEvent(params) {
1811
+ const { summary, description, location, start, end, durationMinutes } = params;
1812
+ const response = await this.client.events.insert({
1813
+ calendarId: this.calendarId(params),
1814
+ requestBody: {
1815
+ summary,
1816
+ description: description || void 0,
1817
+ location: location || void 0,
1818
+ start: this.toEventTime(start),
1819
+ end: this.toEventTime(
1820
+ this.resolveEnd(start, end || void 0, durationMinutes || void 0)
1821
+ )
1822
+ }
1823
+ });
1824
+ return {
1825
+ id: response.data.id ?? "",
1826
+ status: response.data.status ?? "",
1827
+ htmlLink: response.data.htmlLink ?? ""
1828
+ };
1829
+ }
1830
+ async updateEvent(params) {
1831
+ const { eventId, summary, description, location, start, end } = params;
1832
+ const requestBody = {};
1833
+ if (typeof summary === "string") requestBody.summary = summary;
1834
+ if (typeof description === "string") requestBody.description = description;
1835
+ if (typeof location === "string") requestBody.location = location;
1836
+ if (typeof start === "string" && start) requestBody.start = this.toEventTime(start);
1837
+ if (typeof end === "string" && end) requestBody.end = this.toEventTime(end);
1838
+ const response = await this.client.events.patch({
1839
+ calendarId: this.calendarId(params),
1840
+ eventId,
1841
+ requestBody
1842
+ });
1843
+ return {
1844
+ id: response.data.id ?? "",
1845
+ status: response.data.status ?? ""
1846
+ };
1847
+ }
1848
+ async deleteEvent(params) {
1849
+ const { eventId } = params;
1850
+ await this.client.events.delete({
1851
+ calendarId: this.calendarId(params),
1852
+ eventId
1853
+ });
1854
+ return { id: eventId, deleted: true };
1855
+ }
1856
+ async freeBusy(params) {
1857
+ const { timeMin, timeMax } = params;
1858
+ const id = this.calendarId(params);
1859
+ const response = await this.client.freebusy.query({
1860
+ requestBody: {
1861
+ timeMin,
1862
+ timeMax,
1863
+ items: [{ id }]
1864
+ }
1865
+ });
1866
+ const busy = response.data.calendars?.[id]?.busy ?? [];
1867
+ return {
1868
+ busy: busy.map((slot) => ({ start: slot.start ?? "", end: slot.end ?? "" }))
1869
+ };
1870
+ }
1871
+ async watch(params) {
1872
+ const { channelId, address, ttlSeconds } = params;
1873
+ const response = await this.client.events.watch({
1874
+ calendarId: this.calendarId(params),
1875
+ requestBody: {
1876
+ id: channelId,
1877
+ type: "web_hook",
1878
+ address,
1879
+ params: ttlSeconds ? { ttl: String(ttlSeconds) } : void 0
1880
+ }
1881
+ });
1882
+ return {
1883
+ channelId: response.data.id ?? channelId,
1884
+ resourceId: response.data.resourceId ?? "",
1885
+ expiration: response.data.expiration ?? ""
1886
+ };
1887
+ }
1888
+ async stopWatch(params) {
1889
+ const { channelId, resourceId } = params;
1890
+ await this.client.channels.stop({
1891
+ requestBody: {
1892
+ id: channelId,
1893
+ resourceId
1894
+ }
1895
+ });
1896
+ return { stopped: true };
1897
+ }
1898
+ };
1899
+ registerIntegration("calendar", CalendarIntegration);
1900
+
1901
+ // src/hooks.ts
1902
+ var HOOK_PROVIDER_FACTORIES = {
1903
+ googleCalendarHookProvider
1904
+ };
1905
+ function allHookDeclarations() {
1906
+ return Object.values(serviceHooks).flat();
1907
+ }
1908
+ function registeredHookProviders(env = process.env) {
1909
+ const providers = {};
1910
+ for (const decl of allHookDeclarations()) {
1911
+ const factory = HOOK_PROVIDER_FACTORIES[decl.providerExport];
1912
+ if (!factory) {
1913
+ throw new Error(
1914
+ `serviceHooks declares provider export '${decl.providerExport}' but HOOK_PROVIDER_FACTORIES has no entry for it`
1915
+ );
1916
+ }
1917
+ providers[decl.provider] = factory(env[decl.credentialEnv]);
1918
+ }
1919
+ return providers;
1920
+ }
1921
+ function decodeContent(content) {
1922
+ const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
1923
+ if (dataUrlMatch) {
1924
+ const [, mime, isB64, body] = dataUrlMatch;
1925
+ const bytes = isB64 ? Buffer.from(body, "base64") : Buffer.from(decodeURIComponent(body), "utf8");
1926
+ return { bytes, contentType: mime || null };
1927
+ }
1928
+ return { bytes: Buffer.from(content, "utf8"), contentType: null };
1929
+ }
1930
+ var DriveIntegration = class extends BaseIntegration {
1931
+ constructor(config) {
1932
+ super(config);
1933
+ const rawKey = config.env.GOOGLE_DRIVE_SA_KEY;
1934
+ if (rawKey) {
1935
+ const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
1936
+ const key = JSON.parse(keyJson);
1937
+ const subject = config.env.GOOGLE_DRIVE_SUBJECT || void 0;
1938
+ const auth = new google.auth.JWT({
1939
+ email: key.client_email,
1940
+ key: key.private_key,
1941
+ scopes: ["https://www.googleapis.com/auth/drive"],
1942
+ subject
1943
+ });
1944
+ this.saClient = google.drive({ version: "v3", auth });
1945
+ } else {
1946
+ this.saClient = null;
1947
+ }
1948
+ const refreshToken = config.env.GOOGLE_DRIVE_REFRESH_TOKEN;
1949
+ const clientId = config.env.OAUTH_CLIENT_ID;
1950
+ const clientSecret = config.env.OAUTH_CLIENT_SECRET;
1951
+ if (refreshToken && clientId && clientSecret) {
1952
+ const oauth2 = new google.auth.OAuth2(clientId, clientSecret);
1953
+ oauth2.setCredentials({ refresh_token: refreshToken });
1954
+ this.userClient = google.drive({ version: "v3", auth: oauth2 });
1955
+ } else {
1956
+ this.userClient = null;
1957
+ }
1958
+ if (!this.saClient && !this.userClient) {
1959
+ throw new Error(
1960
+ "Drive not configured \u2014 set GOOGLE_DRIVE_SA_KEY (reads) and/or GOOGLE_DRIVE_REFRESH_TOKEN + OAUTH_CLIENT_ID/SECRET (writes)"
1961
+ );
1962
+ }
1963
+ this.defaultFolderId = config.env.GOOGLE_DRIVE_FOLDER_ID || void 0;
1964
+ this.logger.info("Drive integration initialized", {
1965
+ serviceAccount: this.saClient !== null,
1966
+ userToken: this.userClient !== null,
1967
+ delegated: Boolean(config.env.GOOGLE_DRIVE_SUBJECT)
1968
+ });
1969
+ }
1970
+ /** Reads prefer the SA client (delegation-aware); user client covers its absence. */
1971
+ readClient() {
1972
+ const client = this.saClient ?? this.userClient;
1973
+ if (!client) throw new Error("Drive not configured");
1974
+ return client;
1975
+ }
1976
+ /** Writes REQUIRE the user client on personal accounts (SA has no storage quota); SA only as a Workspace fallback. */
1977
+ writeClient() {
1978
+ const client = this.userClient ?? this.saClient;
1979
+ if (!client) throw new Error("Drive not configured");
1980
+ return client;
1981
+ }
1982
+ async execute(action, params) {
1983
+ const validation = this.validateParams(action, params);
1984
+ if (!validation.valid) {
1985
+ return {
1986
+ success: false,
1987
+ error: {
1988
+ name: "IntegrationError",
1989
+ message: "Validation failed",
1990
+ code: "VALIDATION_ERROR",
1991
+ details: validation.errors
1992
+ },
1993
+ metadata: this.createMetadata(action, 0)
1994
+ };
1995
+ }
1996
+ const startTime = Date.now();
1997
+ try {
1998
+ let data;
1999
+ switch (action) {
2000
+ case "listFiles":
2001
+ data = await this.executeWithRetry(() => this.listFiles(params));
2002
+ break;
2003
+ case "getFile":
2004
+ data = await this.executeWithRetry(() => this.getFile(params));
2005
+ break;
2006
+ case "uploadFile":
2007
+ data = await this.executeWithRetry(() => this.uploadFile(params));
2008
+ break;
2009
+ case "createFolder":
2010
+ data = await this.executeWithRetry(() => this.createFolder(params));
2011
+ break;
2012
+ case "shareFile":
2013
+ data = await this.executeWithRetry(() => this.shareFile(params));
2014
+ break;
2015
+ default:
2016
+ throw new Error(`Unknown action: ${action}`);
2017
+ }
2018
+ return {
2019
+ success: true,
2020
+ data,
2021
+ metadata: this.createMetadata(action, Date.now() - startTime)
2022
+ };
2023
+ } catch (error) {
2024
+ return this.handleError(action, error);
2025
+ }
2026
+ }
2027
+ async listFiles(params) {
2028
+ const { folderId, query, maxResults } = params;
2029
+ const clauses = ["trashed = false"];
2030
+ if (folderId) clauses.push(`'${String(folderId).replace(/'/g, "\\'")}' in parents`);
2031
+ if (query) clauses.push(String(query));
2032
+ const response = await this.readClient().files.list({
2033
+ q: clauses.join(" and "),
2034
+ pageSize: maxResults || 100,
2035
+ fields: "files(id, name, mimeType, size, modifiedTime, webViewLink)"
2036
+ });
2037
+ return {
2038
+ files: (response.data.files ?? []).map((file) => ({
2039
+ id: file.id ?? "",
2040
+ name: file.name ?? "",
2041
+ mimeType: file.mimeType ?? "",
2042
+ size: Number(file.size ?? 0),
2043
+ modifiedTime: file.modifiedTime ?? "",
2044
+ webViewLink: file.webViewLink ?? ""
2045
+ }))
2046
+ };
2047
+ }
2048
+ async getFile(params) {
2049
+ const fileId = params.fileId;
2050
+ const meta = await this.readClient().files.get({
2051
+ fileId,
2052
+ fields: "id, name, mimeType, size"
2053
+ });
2054
+ const content = await this.readClient().files.get(
2055
+ { fileId, alt: "media" },
2056
+ { responseType: "arraybuffer" }
2057
+ );
2058
+ const bytes = Buffer.from(content.data);
2059
+ return {
2060
+ id: meta.data.id ?? fileId,
2061
+ name: meta.data.name ?? "",
2062
+ mimeType: meta.data.mimeType ?? "application/octet-stream",
2063
+ content: bytes.toString("base64"),
2064
+ size: bytes.length
2065
+ };
2066
+ }
2067
+ async uploadFile(params) {
2068
+ const { name, content, mimeType, folderId } = params;
2069
+ const { bytes, contentType } = decodeContent(content);
2070
+ const parent = folderId || this.defaultFolderId;
2071
+ const response = await this.writeClient().files.create({
2072
+ requestBody: {
2073
+ name,
2074
+ parents: parent ? [parent] : void 0
2075
+ },
2076
+ media: {
2077
+ mimeType: mimeType || contentType || "application/octet-stream",
2078
+ body: Readable.from(bytes)
2079
+ },
2080
+ fields: "id, name, webViewLink"
2081
+ });
2082
+ return {
2083
+ id: response.data.id ?? "",
2084
+ name: response.data.name ?? name,
2085
+ webViewLink: response.data.webViewLink ?? ""
2086
+ };
2087
+ }
2088
+ async createFolder(params) {
2089
+ const { name, parentId } = params;
2090
+ const parent = parentId || this.defaultFolderId;
2091
+ const response = await this.writeClient().files.create({
2092
+ requestBody: {
2093
+ name,
2094
+ mimeType: "application/vnd.google-apps.folder",
2095
+ parents: parent ? [parent] : void 0
2096
+ },
2097
+ fields: "id, name"
2098
+ });
2099
+ return { id: response.data.id ?? "", name: response.data.name ?? name };
2100
+ }
2101
+ async shareFile(params) {
2102
+ const { fileId, email, role } = params;
2103
+ const response = await this.readClient().permissions.create({
2104
+ fileId,
2105
+ requestBody: {
2106
+ type: "user",
2107
+ role: role || "reader",
2108
+ emailAddress: email
2109
+ },
2110
+ fields: "id"
2111
+ });
2112
+ return { shared: true, permissionId: response.data.id ?? "" };
2113
+ }
2114
+ };
2115
+ registerIntegration("drive", DriveIntegration);
2116
+
2117
+ // src/integrations/metaAds/index.ts
2118
+ var GRAPH_BASE = "https://graph.facebook.com/v21.0";
2119
+ var MetaAdsIntegration = class extends BaseIntegration {
2120
+ constructor(config) {
2121
+ super(config);
2122
+ this.accessToken = config.env.META_ACCESS_TOKEN || "";
2123
+ if (!this.accessToken) {
2124
+ throw new Error("META_ACCESS_TOKEN not configured");
2125
+ }
2126
+ this.defaultAccountId = config.env.META_AD_ACCOUNT_ID || "";
2127
+ this.logger.info("Meta Ads integration initialized");
2128
+ }
2129
+ async execute(action, params) {
2130
+ const validation = this.validateParams(action, params);
2131
+ if (!validation.valid) {
2132
+ return {
2133
+ success: false,
2134
+ error: {
2135
+ name: "IntegrationError",
2136
+ message: "Validation failed",
2137
+ code: "VALIDATION_ERROR",
2138
+ details: validation.errors
2139
+ },
2140
+ metadata: this.createMetadata(action, 0)
2141
+ };
2142
+ }
2143
+ const startTime = Date.now();
2144
+ try {
2145
+ let data;
2146
+ switch (action) {
2147
+ case "getSpend":
2148
+ data = await this.executeWithRetry(() => this.getSpend(params));
2149
+ break;
2150
+ case "listCampaigns":
2151
+ data = await this.executeWithRetry(() => this.listCampaigns(params));
2152
+ break;
2153
+ default:
2154
+ throw new Error(`Unknown action: ${action}`);
2155
+ }
2156
+ return {
2157
+ success: true,
2158
+ data,
2159
+ metadata: this.createMetadata(action, Date.now() - startTime)
2160
+ };
2161
+ } catch (error) {
2162
+ return this.handleError(action, error);
2163
+ }
2164
+ }
2165
+ accountId(params) {
2166
+ const id = params.accountId || this.defaultAccountId;
2167
+ if (!id) {
2168
+ throw new Error("No ad account: pass `accountId` or set META_AD_ACCOUNT_ID");
2169
+ }
2170
+ return id.startsWith("act_") ? id : `act_${id}`;
2171
+ }
2172
+ async graphGet(path, query) {
2173
+ const url = new URL(`${GRAPH_BASE}/${path}`);
2174
+ for (const [key, value] of Object.entries(query)) {
2175
+ url.searchParams.set(key, value);
2176
+ }
2177
+ url.searchParams.set("access_token", this.accessToken);
2178
+ const response = await fetch(url, { signal: AbortSignal.timeout(15e3) });
2179
+ if (response.status >= 500) {
2180
+ throw new Error(`Meta Graph API returned ${response.status}`);
2181
+ }
2182
+ const body = await response.json();
2183
+ if (!response.ok) {
2184
+ throw new Error(`Meta Graph API error: ${body.error?.message ?? response.status}`);
2185
+ }
2186
+ return body.data ?? body;
2187
+ }
2188
+ async getSpend(params) {
2189
+ const { since, until } = params;
2190
+ const rows = await this.graphGet(`${this.accountId(params)}/insights`, {
2191
+ fields: "spend,impressions,clicks,account_currency",
2192
+ time_range: JSON.stringify({ since, until }),
2193
+ level: "account"
2194
+ });
2195
+ const row = Array.isArray(rows) ? rows[0] : void 0;
2196
+ return {
2197
+ spend: Number(row?.spend ?? 0),
2198
+ currency: row?.account_currency ?? "",
2199
+ impressions: Number(row?.impressions ?? 0),
2200
+ clicks: Number(row?.clicks ?? 0)
2201
+ };
2202
+ }
2203
+ async listCampaigns(params) {
2204
+ const { status } = params;
2205
+ const rows = await this.graphGet(`${this.accountId(params)}/campaigns`, {
2206
+ fields: "id,name,status,daily_budget",
2207
+ ...status ? { effective_status: JSON.stringify([status]) } : {}
2208
+ });
2209
+ return {
2210
+ campaigns: (Array.isArray(rows) ? rows : []).map((row) => ({
2211
+ id: row.id ?? "",
2212
+ name: row.name ?? "",
2213
+ status: row.status ?? "",
2214
+ // Meta reports budgets in minor units (cents).
2215
+ dailyBudget: Number(row.daily_budget ?? 0) / 100
2216
+ }))
2217
+ };
2218
+ }
2219
+ };
2220
+ registerIntegration("metaAds", MetaAdsIntegration);
2221
+
2222
+ // src/integrations/accounting/index.ts
2223
+ var AccountingIntegration = class extends BaseIntegration {
2224
+ constructor(config) {
2225
+ super(config);
2226
+ this.logger.info("Accounting integration initialized (generic CSV export)");
2227
+ }
2228
+ async execute(action, params) {
2229
+ const validation = this.validateParams(action, params);
2230
+ if (!validation.valid) {
2231
+ return {
2232
+ success: false,
2233
+ error: {
2234
+ name: "IntegrationError",
2235
+ message: "Validation failed",
2236
+ code: "VALIDATION_ERROR",
2237
+ details: validation.errors
2238
+ },
2239
+ metadata: this.createMetadata(action, 0)
2240
+ };
2241
+ }
2242
+ const startTime = Date.now();
2243
+ try {
2244
+ let data;
2245
+ switch (action) {
2246
+ case "exportInvoices":
2247
+ data = this.exportRows(params.invoices, INVOICE_COLUMNS, "invoices");
2248
+ break;
2249
+ case "exportJournal":
2250
+ data = this.exportRows(params.entries, JOURNAL_COLUMNS, "journal");
2251
+ break;
2252
+ default:
2253
+ throw new Error(`Unknown action: ${action}`);
2254
+ }
2255
+ return {
2256
+ success: true,
2257
+ data,
2258
+ metadata: this.createMetadata(action, Date.now() - startTime)
2259
+ };
2260
+ } catch (error) {
2261
+ return this.handleError(action, error);
2262
+ }
2263
+ }
2264
+ exportRows(rowsValue, columns, kind) {
2265
+ if (!Array.isArray(rowsValue)) {
2266
+ throw new Error(`${kind} export requires an array of rows`);
2267
+ }
2268
+ const lines = [columns.join(",")];
2269
+ for (const row of rowsValue) {
2270
+ if (row === null || typeof row !== "object" || Array.isArray(row) || row instanceof Date) {
2271
+ throw new Error(`${kind} export: every row must be an object`);
2272
+ }
2273
+ const record = row;
2274
+ lines.push(columns.map((column) => csvCell(record[column])).join(","));
2275
+ }
2276
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2277
+ return {
2278
+ content: lines.join("\r\n") + "\r\n",
2279
+ filename: `${kind}-export-${stamp}.csv`,
2280
+ count: rowsValue.length
2281
+ };
2282
+ }
2283
+ };
2284
+ var INVOICE_COLUMNS = ["id", "number", "customer", "issuedAt", "dueAt", "currency", "net", "tax", "gross", "status"];
2285
+ var JOURNAL_COLUMNS = ["date", "account", "description", "debit", "credit", "reference"];
2286
+ function csvCell(value) {
2287
+ if (value === void 0 || value === null) return "";
2288
+ const raw = value instanceof Date ? value.toISOString() : String(value);
2289
+ return /[",\r\n]/.test(raw) ? `"${raw.replace(/"/g, '""')}"` : raw;
2290
+ }
2291
+ registerIntegration("accounting", AccountingIntegration);
2292
+
2293
+ // src/integrations/banking/index.ts
2294
+ var GC_BASE = "https://bankaccountdata.gocardless.com/api/v2";
2295
+ var BankingIntegration = class extends BaseIntegration {
2296
+ constructor(config) {
2297
+ super(config);
2298
+ this.accessToken = null;
2299
+ this.accessTokenExpiresAt = 0;
2300
+ this.secretId = config.env.GOCARDLESS_SECRET_ID || "";
2301
+ this.secretKey = config.env.GOCARDLESS_SECRET_KEY || "";
2302
+ if (!this.secretId || !this.secretKey) {
2303
+ throw new Error("GOCARDLESS_SECRET_ID / GOCARDLESS_SECRET_KEY not configured");
2304
+ }
2305
+ this.logger.info("Banking integration initialized (GoCardless Bank Account Data)");
2306
+ }
2307
+ async execute(action, params) {
2308
+ const validation = this.validateParams(action, params);
2309
+ if (!validation.valid) {
2310
+ return {
2311
+ success: false,
2312
+ error: {
2313
+ name: "IntegrationError",
2314
+ message: "Validation failed",
2315
+ code: "VALIDATION_ERROR",
2316
+ details: validation.errors
2317
+ },
2318
+ metadata: this.createMetadata(action, 0)
2319
+ };
2320
+ }
2321
+ const startTime = Date.now();
2322
+ try {
2323
+ let data;
2324
+ switch (action) {
2325
+ case "createRequisition":
2326
+ data = await this.executeWithRetry(() => this.createRequisition(params));
2327
+ break;
2328
+ case "listAccounts":
2329
+ data = await this.executeWithRetry(() => this.listAccounts(params));
2330
+ break;
2331
+ case "listTransactions":
2332
+ data = await this.executeWithRetry(() => this.listTransactions(params));
2333
+ break;
2334
+ default:
2335
+ throw new Error(`Unknown action: ${action}`);
2336
+ }
2337
+ return {
2338
+ success: true,
2339
+ data,
2340
+ metadata: this.createMetadata(action, Date.now() - startTime)
2341
+ };
2342
+ } catch (error) {
2343
+ return this.handleError(action, error);
964
2344
  }
965
- throw new Error(`Unknown email provider: ${this.provider}`);
966
2345
  }
967
- async sendViaSendGrid(to, subject, body, from) {
968
- const msg = {
969
- to,
970
- from,
971
- subject,
972
- html: body
973
- };
974
- const response = await sgMail.send(msg);
975
- return {
976
- id: response[0].headers["x-message-id"],
977
- status: "sent"
978
- };
2346
+ async token() {
2347
+ if (this.accessToken && Date.now() < this.accessTokenExpiresAt - 6e4) {
2348
+ return this.accessToken;
2349
+ }
2350
+ const response = await fetch(`${GC_BASE}/token/new/`, {
2351
+ method: "POST",
2352
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2353
+ body: JSON.stringify({ secret_id: this.secretId, secret_key: this.secretKey }),
2354
+ signal: AbortSignal.timeout(15e3)
2355
+ });
2356
+ if (!response.ok) {
2357
+ throw new Error(`GoCardless token request failed: ${response.status}`);
2358
+ }
2359
+ const body = await response.json();
2360
+ this.accessToken = body.access;
2361
+ this.accessTokenExpiresAt = Date.now() + body.access_expires * 1e3;
2362
+ return this.accessToken;
979
2363
  }
980
- async sendViaResend(to, subject, body, from) {
981
- if (!this.resendClient) {
982
- throw new Error("Resend client not initialized");
2364
+ async gcRequest(path, init2) {
2365
+ const response = await fetch(`${GC_BASE}${path}`, {
2366
+ method: init2?.method ?? "GET",
2367
+ headers: {
2368
+ Accept: "application/json",
2369
+ "Content-Type": "application/json",
2370
+ Authorization: `Bearer ${await this.token()}`
2371
+ },
2372
+ body: init2?.body === void 0 ? void 0 : JSON.stringify(init2.body),
2373
+ signal: AbortSignal.timeout(2e4)
2374
+ });
2375
+ if (response.status >= 500) {
2376
+ throw new Error(`GoCardless returned ${response.status}`);
983
2377
  }
984
- const response = await this.resendClient.emails.send({
985
- from,
986
- to,
987
- subject,
988
- html: body
2378
+ const body = await response.json();
2379
+ if (!response.ok) {
2380
+ throw new Error(`GoCardless error ${response.status}: ${JSON.stringify(body).slice(0, 300)}`);
2381
+ }
2382
+ return body;
2383
+ }
2384
+ async createRequisition(params) {
2385
+ const { institutionId, redirectUrl, reference } = params;
2386
+ const body = await this.gcRequest("/requisitions/", {
2387
+ method: "POST",
2388
+ body: {
2389
+ institution_id: institutionId,
2390
+ redirect: redirectUrl,
2391
+ reference: reference || void 0
2392
+ }
989
2393
  });
2394
+ return { requisitionId: body.id ?? "", link: body.link ?? "" };
2395
+ }
2396
+ async listAccounts(params) {
2397
+ const { requisitionId } = params;
2398
+ const body = await this.gcRequest(`/requisitions/${requisitionId}/`);
2399
+ return { accounts: body.accounts ?? [] };
2400
+ }
2401
+ async listTransactions(params) {
2402
+ const { accountId, dateFrom, dateTo } = params;
2403
+ const query = new URLSearchParams();
2404
+ if (dateFrom) query.set("date_from", dateFrom);
2405
+ if (dateTo) query.set("date_to", dateTo);
2406
+ const suffix = query.size > 0 ? `?${query.toString()}` : "";
2407
+ const body = await this.gcRequest(`/accounts/${accountId}/transactions/${suffix}`);
990
2408
  return {
991
- id: response.data?.id,
992
- status: "sent"
2409
+ transactions: (body.transactions?.booked ?? []).map((tx) => ({
2410
+ id: tx.transactionId ?? tx.internalTransactionId ?? "",
2411
+ amount: Number(tx.transactionAmount?.amount ?? 0),
2412
+ currency: tx.transactionAmount?.currency ?? "",
2413
+ date: tx.bookingDate ?? "",
2414
+ description: tx.remittanceInformationUnstructured ?? "",
2415
+ counterparty: tx.creditorName ?? tx.debtorName ?? ""
2416
+ }))
993
2417
  };
994
2418
  }
995
2419
  };
996
- registerIntegration("email", EmailIntegration);
997
- var WebhookIntegration = class extends BaseIntegration {
2420
+ registerIntegration("banking", BankingIntegration);
2421
+
2422
+ // src/integrations/esign/index.ts
2423
+ var EsignIntegration = class extends BaseIntegration {
998
2424
  constructor(config) {
999
2425
  super(config);
1000
- this.signingSecret = config.env.WEBHOOK_SIGNING_SECRET || "";
1001
- this.timeoutMs = Number(config.env.WEBHOOK_TIMEOUT_MS) || 1e4;
1002
- this.logger.info("Webhook integration initialized");
2426
+ if (!config.env.DOCUSIGN_BASE_URL || !config.env.DOCUSIGN_ACCESS_TOKEN) {
2427
+ throw new Error("DOCUSIGN_BASE_URL / DOCUSIGN_ACCESS_TOKEN not configured");
2428
+ }
2429
+ this.logger.info("E-sign integration initialized (DocuSign)");
1003
2430
  }
1004
2431
  async execute(action, params) {
1005
2432
  const validation = this.validateParams(action, params);
@@ -1016,12 +2443,17 @@ var WebhookIntegration = class extends BaseIntegration {
1016
2443
  };
1017
2444
  }
1018
2445
  const startTime = Date.now();
1019
- let retries = 0;
1020
2446
  try {
1021
2447
  let data;
1022
2448
  switch (action) {
1023
- case "send":
1024
- data = await this.executeWithRetry(() => this.send(params));
2449
+ case "sendEnvelope":
2450
+ data = await this.executeWithRetry(() => this.sendEnvelope(params));
2451
+ break;
2452
+ case "getEnvelopeStatus":
2453
+ data = await this.executeWithRetry(() => this.getEnvelopeStatus(params));
2454
+ break;
2455
+ case "downloadDocument":
2456
+ data = await this.executeWithRetry(() => this.downloadDocument(params));
1025
2457
  break;
1026
2458
  default:
1027
2459
  throw new Error(`Unknown action: ${action}`);
@@ -1029,46 +2461,81 @@ var WebhookIntegration = class extends BaseIntegration {
1029
2461
  return {
1030
2462
  success: true,
1031
2463
  data,
1032
- metadata: this.createMetadata(action, Date.now() - startTime, retries)
2464
+ metadata: this.createMetadata(action, Date.now() - startTime)
1033
2465
  };
1034
2466
  } catch (error) {
1035
2467
  return this.handleError(action, error);
1036
2468
  }
1037
2469
  }
1038
- async send(params) {
1039
- const { url, event, payload, secret } = params;
1040
- const body = JSON.stringify({
1041
- event,
1042
- payload: payload ?? {},
1043
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2470
+ async dsRequest(path, init2) {
2471
+ const base = this.config.env.DOCUSIGN_BASE_URL.replace(/\/$/, "");
2472
+ const response = await fetch(`${base}${path}`, {
2473
+ method: init2?.method ?? "GET",
2474
+ headers: {
2475
+ Accept: init2?.raw ? "application/pdf" : "application/json",
2476
+ "Content-Type": "application/json",
2477
+ Authorization: `Bearer ${this.config.env.DOCUSIGN_ACCESS_TOKEN}`
2478
+ },
2479
+ body: init2?.body === void 0 ? void 0 : JSON.stringify(init2.body),
2480
+ signal: AbortSignal.timeout(3e4)
1044
2481
  });
1045
- const headers = {
1046
- "Content-Type": "application/json",
1047
- "X-Almadar-Event": event
1048
- };
1049
- const signingSecret = secret || this.signingSecret;
1050
- if (signingSecret) {
1051
- headers["X-Almadar-Signature"] = "sha256=" + createHmac("sha256", signingSecret).update(body).digest("hex");
2482
+ if (response.status >= 500) {
2483
+ throw new Error(`DocuSign returned ${response.status}`);
1052
2484
  }
1053
- this.logger.debug("Sending webhook", { url: String(url), event: String(event) });
1054
- const startTime = Date.now();
1055
- const response = await fetch(url, {
2485
+ if (!response.ok) {
2486
+ const detail = await response.text();
2487
+ throw new Error(`DocuSign error ${response.status}: ${detail.slice(0, 300)}`);
2488
+ }
2489
+ if (init2?.raw) {
2490
+ return Buffer.from(await response.arrayBuffer());
2491
+ }
2492
+ return response.json();
2493
+ }
2494
+ async sendEnvelope(params) {
2495
+ const { recipientEmail, recipientName, documentName, documentContent, emailSubject } = params;
2496
+ const rawContent = documentContent;
2497
+ const base64 = rawContent.startsWith("data:") ? rawContent.slice(rawContent.indexOf(",") + 1) : rawContent;
2498
+ const body = await this.dsRequest("/envelopes", {
1056
2499
  method: "POST",
1057
- headers,
1058
- body,
1059
- signal: AbortSignal.timeout(this.timeoutMs)
2500
+ body: {
2501
+ emailSubject: emailSubject || `Please sign: ${documentName}`,
2502
+ status: "sent",
2503
+ documents: [
2504
+ {
2505
+ documentBase64: base64,
2506
+ name: documentName,
2507
+ fileExtension: String(documentName).split(".").pop() || "pdf",
2508
+ documentId: "1"
2509
+ }
2510
+ ],
2511
+ recipients: {
2512
+ signers: [
2513
+ {
2514
+ email: recipientEmail,
2515
+ name: recipientName,
2516
+ recipientId: "1",
2517
+ routingOrder: "1"
2518
+ }
2519
+ ]
2520
+ }
2521
+ }
1060
2522
  });
1061
- if (response.status >= 500) {
1062
- throw new Error(`Webhook endpoint returned ${response.status}`);
1063
- }
1064
- return {
1065
- status: response.status,
1066
- ok: response.ok,
1067
- durationMs: Date.now() - startTime
1068
- };
2523
+ return { envelopeId: body.envelopeId ?? "", status: body.status ?? "sent" };
2524
+ }
2525
+ async getEnvelopeStatus(params) {
2526
+ const { envelopeId } = params;
2527
+ const body = await this.dsRequest(`/envelopes/${envelopeId}`);
2528
+ return { status: body.status ?? "", completedAt: body.completedDateTime ?? "" };
2529
+ }
2530
+ async downloadDocument(params) {
2531
+ const { envelopeId } = params;
2532
+ const bytes = await this.dsRequest(`/envelopes/${envelopeId}/documents/combined`, {
2533
+ raw: true
2534
+ });
2535
+ return { content: bytes.toString("base64"), documentName: `envelope-${envelopeId}.pdf` };
1069
2536
  }
1070
2537
  };
1071
- registerIntegration("webhook", WebhookIntegration);
2538
+ registerIntegration("esign", EsignIntegration);
1072
2539
  var LLMIntegration = class extends BaseIntegration {
1073
2540
  constructor(config) {
1074
2541
  super(config);
@@ -2639,25 +4106,63 @@ var OtelIntegration = class extends BaseIntegration {
2639
4106
  }
2640
4107
  };
2641
4108
  registerIntegration("otel", OtelIntegration);
2642
-
2643
- // src/integrations/oauth/index.ts
2644
4109
  var PROVIDER_AUTH_URLS = {
2645
4110
  google: "https://accounts.google.com/o/oauth2/v2/auth",
2646
4111
  github: "https://github.com/login/oauth/authorize",
2647
4112
  auth0: "https://auth.example.com/authorize"
2648
4113
  };
4114
+ var PROVIDER_ISSUERS = {
4115
+ google: "https://accounts.google.com"
4116
+ };
4117
+ var PENDING_GRANT_TTL_MS = 10 * 60 * 1e3;
4118
+ var InMemoryPendingGrantStore = class {
4119
+ constructor() {
4120
+ this.grants = /* @__PURE__ */ new Map();
4121
+ }
4122
+ async put(state, grant, ttlMs) {
4123
+ this.grants.set(state, { grant, expiresAt: Date.now() + ttlMs });
4124
+ }
4125
+ async take(state) {
4126
+ const entry = this.grants.get(state);
4127
+ if (!entry) return null;
4128
+ this.grants.delete(state);
4129
+ return entry.expiresAt >= Date.now() ? entry.grant : null;
4130
+ }
4131
+ async sweep() {
4132
+ const now = Date.now();
4133
+ for (const [state, entry] of this.grants) {
4134
+ if (entry.expiresAt < now) this.grants.delete(state);
4135
+ }
4136
+ }
4137
+ };
4138
+ var installedPendingGrantStore = null;
4139
+ function installPendingGrantStore(store) {
4140
+ installedPendingGrantStore = store;
4141
+ }
2649
4142
  var OAuthIntegration = class extends BaseIntegration {
2650
4143
  constructor(config) {
2651
4144
  super(config);
2652
- /** Maps state token -> provider for pending authorization flows */
4145
+ /** Maps state token -> provider for pending MOCK authorization flows */
2653
4146
  this.states = /* @__PURE__ */ new Map();
2654
- /** Maps access token -> token set */
4147
+ /** Maps access token -> token set (mock backend) */
2655
4148
  this.tokens = /* @__PURE__ */ new Map();
2656
- /** Maps refresh token -> access token for refresh lookups */
4149
+ /** Maps refresh token -> access token for refresh lookups (mock backend) */
2657
4150
  this.refreshIndex = /* @__PURE__ */ new Map();
2658
4151
  /** Maps access token -> mock user session */
2659
4152
  this.sessions = /* @__PURE__ */ new Map();
2660
- this.logger.info("OAuth integration initialized (mock backend)");
4153
+ /** Pending OIDC authorizations (real backend) — injectable, in-memory default. */
4154
+ this.fallbackPending = new InMemoryPendingGrantStore();
4155
+ /** Maps access token -> ID-token subject, for userinfo subject checks */
4156
+ this.subjects = /* @__PURE__ */ new Map();
4157
+ /** Discovered issuer configurations, keyed by issuer URL */
4158
+ this.discovered = /* @__PURE__ */ new Map();
4159
+ this.real = config.env.OAUTH_MODE !== "mock" && Boolean(config.env.OAUTH_CLIENT_ID) && Boolean(config.env.OAUTH_CLIENT_SECRET);
4160
+ this.logger.info(
4161
+ this.real ? "OAuth integration initialized (OIDC backend via openid-client)" : "OAuth integration initialized (mock backend)"
4162
+ );
4163
+ }
4164
+ pendingStore() {
4165
+ return installedPendingGrantStore ?? this.fallbackPending;
2661
4166
  }
2662
4167
  async execute(action, params) {
2663
4168
  const validation = this.validateParams(action, params);
@@ -2678,19 +4183,19 @@ var OAuthIntegration = class extends BaseIntegration {
2678
4183
  let data;
2679
4184
  switch (action) {
2680
4185
  case "authorize":
2681
- data = await this.executeWithRetry(() => this.authorize(params));
4186
+ data = await this.executeWithRetry(() => this.real ? this.oidcAuthorize(params) : this.authorize(params));
2682
4187
  break;
2683
4188
  case "token":
2684
- data = await this.executeWithRetry(() => this.token(params));
4189
+ data = await this.executeWithRetry(() => this.real ? this.oidcToken(params) : this.token(params));
2685
4190
  break;
2686
4191
  case "refresh":
2687
- data = await this.executeWithRetry(() => this.refresh(params));
4192
+ data = await this.executeWithRetry(() => this.real ? this.oidcRefresh(params) : this.refresh(params));
2688
4193
  break;
2689
4194
  case "revoke":
2690
- data = await this.executeWithRetry(() => this.revoke(params));
4195
+ data = await this.executeWithRetry(() => this.real ? this.oidcRevoke(params) : this.revoke(params));
2691
4196
  break;
2692
4197
  case "userinfo":
2693
- data = await this.executeWithRetry(() => this.userinfo(params));
4198
+ data = await this.executeWithRetry(() => this.real ? this.oidcUserinfo(params) : this.userinfo(params));
2694
4199
  break;
2695
4200
  default:
2696
4201
  throw new Error(`Unknown action: ${action}`);
@@ -2705,7 +4210,119 @@ var OAuthIntegration = class extends BaseIntegration {
2705
4210
  }
2706
4211
  }
2707
4212
  // ---------------------------------------------------------------------------
2708
- // Helpers
4213
+ // OIDC backend (openid-client)
4214
+ // ---------------------------------------------------------------------------
4215
+ issuerFor(provider) {
4216
+ const configured = this.config.env.OIDC_ISSUER_URL;
4217
+ if (configured) return configured;
4218
+ const issuer = PROVIDER_ISSUERS[provider];
4219
+ if (!issuer) {
4220
+ throw new Error(
4221
+ `Provider "${provider}" has no OIDC issuer \u2014 set OIDC_ISSUER_URL to an OIDC-compliant issuer, or use OAUTH_MODE=mock`
4222
+ );
4223
+ }
4224
+ return issuer;
4225
+ }
4226
+ async configurationFor(provider) {
4227
+ const issuer = this.issuerFor(provider);
4228
+ const cached = this.discovered.get(issuer);
4229
+ if (cached) return cached;
4230
+ const configuration = await oidc.discovery(
4231
+ new URL(issuer),
4232
+ this.config.env.OAUTH_CLIENT_ID,
4233
+ this.config.env.OAUTH_CLIENT_SECRET
4234
+ );
4235
+ this.discovered.set(issuer, configuration);
4236
+ return configuration;
4237
+ }
4238
+ async oidcAuthorize(params) {
4239
+ const provider = params.provider;
4240
+ const scopes = params.scopes;
4241
+ const redirectUri = params.redirectUri || this.config.env.OAUTH_REDIRECT_URI;
4242
+ const configuration = await this.configurationFor(provider);
4243
+ const state = oidc.randomState();
4244
+ const pkceVerifier = oidc.randomPKCECodeVerifier();
4245
+ const codeChallenge = await oidc.calculatePKCECodeChallenge(pkceVerifier);
4246
+ const parameters = {
4247
+ redirect_uri: redirectUri,
4248
+ scope: scopes.join(" "),
4249
+ state,
4250
+ code_challenge: codeChallenge,
4251
+ code_challenge_method: "S256"
4252
+ };
4253
+ if (provider === "google") {
4254
+ parameters.access_type = "offline";
4255
+ parameters.prompt = "consent";
4256
+ }
4257
+ const authUrl = oidc.buildAuthorizationUrl(configuration, parameters);
4258
+ await this.pendingStore().put(state, { provider, redirectUri, pkceVerifier }, PENDING_GRANT_TTL_MS);
4259
+ void this.pendingStore().sweep();
4260
+ return { authUrl: authUrl.toString(), state };
4261
+ }
4262
+ async oidcToken(params) {
4263
+ const code = params.code;
4264
+ const state = params.state;
4265
+ const pendingAuth = await this.pendingStore().take(state);
4266
+ if (!pendingAuth) {
4267
+ throw new Error(`Invalid or expired state token: ${state}`);
4268
+ }
4269
+ const configuration = await this.configurationFor(pendingAuth.provider);
4270
+ const callbackUrl = new URL(pendingAuth.redirectUri);
4271
+ callbackUrl.searchParams.set("code", code);
4272
+ callbackUrl.searchParams.set("state", state);
4273
+ const tokens = await oidc.authorizationCodeGrant(configuration, callbackUrl, {
4274
+ expectedState: state,
4275
+ pkceCodeVerifier: pendingAuth.pkceVerifier
4276
+ });
4277
+ const claims = tokens.claims();
4278
+ if (claims?.sub) {
4279
+ this.subjects.set(tokens.access_token, claims.sub);
4280
+ }
4281
+ return {
4282
+ accessToken: tokens.access_token,
4283
+ refreshToken: tokens.refresh_token ?? "",
4284
+ expiresIn: tokens.expires_in ?? 3600,
4285
+ tokenType: "bearer"
4286
+ };
4287
+ }
4288
+ async oidcRefresh(params) {
4289
+ const refreshToken = params.refreshToken;
4290
+ const configuration = await this.configurationFor("google");
4291
+ const tokens = await oidc.refreshTokenGrant(configuration, refreshToken);
4292
+ const claims = tokens.claims();
4293
+ if (claims?.sub) {
4294
+ this.subjects.set(tokens.access_token, claims.sub);
4295
+ }
4296
+ return {
4297
+ accessToken: tokens.access_token,
4298
+ expiresIn: tokens.expires_in ?? 3600
4299
+ };
4300
+ }
4301
+ async oidcRevoke(params) {
4302
+ const token = params.token;
4303
+ const configuration = await this.configurationFor("google");
4304
+ await oidc.tokenRevocation(configuration, token);
4305
+ this.subjects.delete(token);
4306
+ return { revoked: true };
4307
+ }
4308
+ async oidcUserinfo(params) {
4309
+ const accessToken = params.accessToken;
4310
+ const configuration = await this.configurationFor("google");
4311
+ const subject = this.subjects.get(accessToken);
4312
+ const info = await oidc.fetchUserInfo(
4313
+ configuration,
4314
+ accessToken,
4315
+ subject ?? oidc.skipSubjectCheck
4316
+ );
4317
+ return {
4318
+ sub: info.sub,
4319
+ email: typeof info.email === "string" ? info.email : "",
4320
+ name: typeof info.name === "string" ? info.name : "",
4321
+ picture: typeof info.picture === "string" ? info.picture : ""
4322
+ };
4323
+ }
4324
+ // ---------------------------------------------------------------------------
4325
+ // Mock backend helpers
2709
4326
  // ---------------------------------------------------------------------------
2710
4327
  /** Generate a random hex token of the given byte length. */
2711
4328
  generateToken(bytes = 32) {
@@ -2726,7 +4343,7 @@ var OAuthIntegration = class extends BaseIntegration {
2726
4343
  };
2727
4344
  }
2728
4345
  // ---------------------------------------------------------------------------
2729
- // Actions
4346
+ // Mock backend actions
2730
4347
  // ---------------------------------------------------------------------------
2731
4348
  async authorize(params) {
2732
4349
  const provider = params.provider;
@@ -2835,19 +4452,242 @@ var OAuthIntegration = class extends BaseIntegration {
2835
4452
  };
2836
4453
  registerIntegration("oauth", OAuthIntegration);
2837
4454
 
2838
- // src/integrations/storage/index.ts
4455
+ // src/integrations/credentials/index.ts
4456
+ var ENV_VAR_NAME = /^[A-Z][A-Z0-9_]*$/;
4457
+ var ROLE_GATED_ACTIONS = /* @__PURE__ */ new Set(["set", "remove", "test", "rotate"]);
4458
+ var DEFAULT_ADMIN_ROLES = ["admin", "owner"];
4459
+ function isProbeService(service) {
4460
+ return service in serviceProbes;
4461
+ }
4462
+ var CredentialsIntegration = class extends BaseIntegration {
4463
+ constructor(config) {
4464
+ super(config);
4465
+ this.logger.info("Credentials integration initialized (tenant credential store surface)");
4466
+ }
4467
+ async execute(action, params, context) {
4468
+ if (ROLE_GATED_ACTIONS.has(action)) {
4469
+ const allowed = this.adminRoles();
4470
+ if (!context?.role || !allowed.includes(context.role)) {
4471
+ return {
4472
+ success: false,
4473
+ error: new IntegrationError(
4474
+ `forbidden: "${action}" requires one of roles: ${allowed.join(", ")}`,
4475
+ "AUTH_ERROR"
4476
+ ),
4477
+ metadata: this.createMetadata(action, 0)
4478
+ };
4479
+ }
4480
+ }
4481
+ const validation = this.validateParams(action, params);
4482
+ if (!validation.valid) {
4483
+ return {
4484
+ success: false,
4485
+ error: {
4486
+ name: "IntegrationError",
4487
+ message: "Validation failed",
4488
+ code: "VALIDATION_ERROR",
4489
+ details: validation.errors
4490
+ },
4491
+ metadata: this.createMetadata(action, 0)
4492
+ };
4493
+ }
4494
+ const startTime = Date.now();
4495
+ try {
4496
+ let data;
4497
+ switch (action) {
4498
+ case "list":
4499
+ data = this.list(typeof params.service === "string" ? params.service : void 0);
4500
+ break;
4501
+ case "set":
4502
+ data = await this.set(params.service, params.envVar, params.value);
4503
+ break;
4504
+ case "remove":
4505
+ data = await this.remove(params.service, params.envVar);
4506
+ break;
4507
+ case "test":
4508
+ data = await this.test(params.service, context);
4509
+ break;
4510
+ case "rotate": {
4511
+ const store = getInstalledCredentialStore();
4512
+ if (!store) {
4513
+ throw new Error("No credential store is installed on this host");
4514
+ }
4515
+ data = await store.rotate();
4516
+ break;
4517
+ }
4518
+ default:
4519
+ throw new Error(`Unknown action: ${action}`);
4520
+ }
4521
+ return {
4522
+ success: true,
4523
+ data,
4524
+ metadata: this.createMetadata(action, Date.now() - startTime)
4525
+ };
4526
+ } catch (error) {
4527
+ return this.handleError(action, error);
4528
+ }
4529
+ }
4530
+ adminRoles() {
4531
+ const raw = this.config.env["ALMADAR_CREDENTIAL_ADMIN_ROLES"] || process.env["ALMADAR_CREDENTIAL_ADMIN_ROLES"] || "";
4532
+ const roles = raw.split(",").map((r) => r.trim()).filter((r) => r.length > 0);
4533
+ return roles.length > 0 ? roles : DEFAULT_ADMIN_ROLES;
4534
+ }
4535
+ declaredFor(service) {
4536
+ return serviceCredentials[service] ?? [];
4537
+ }
4538
+ assertSettable(service, envVar) {
4539
+ if (service === "database") {
4540
+ if (!ENV_VAR_NAME.test(envVar)) {
4541
+ throw new Error(`"${envVar}" is not a well-formed connection reference (expected an env-var name)`);
4542
+ }
4543
+ return;
4544
+ }
4545
+ const declared = this.declaredFor(service);
4546
+ if (declared.length === 0) {
4547
+ throw new Error(`Service "${service}" declares no credentials`);
4548
+ }
4549
+ if (!declared.some((c) => c.envVar === envVar)) {
4550
+ const valid = declared.map((c) => c.envVar).join(", ");
4551
+ throw new Error(`"${envVar}" is not a declared credential of "${service}" (declared: ${valid})`);
4552
+ }
4553
+ }
4554
+ storeFirst() {
4555
+ const flag = this.config.env["ALMADAR_CREDENTIALS_SOURCE"] || process.env["ALMADAR_CREDENTIALS_SOURCE"];
4556
+ return flag === "store";
4557
+ }
4558
+ list(serviceFilter) {
4559
+ const store = getInstalledCredentialStore();
4560
+ const stored = new Map((store?.entries() ?? []).map((e) => [`${e.service}\0${e.envVar}`, e]));
4561
+ const entries = [];
4562
+ for (const [service, declared] of Object.entries(serviceCredentials)) {
4563
+ if (serviceFilter && service !== serviceFilter) continue;
4564
+ for (const { envVar, required, description } of declared) {
4565
+ const fromStore = stored.get(`${service}\0${envVar}`);
4566
+ stored.delete(`${service}\0${envVar}`);
4567
+ const fromEnv = this.storeFirst() ? void 0 : process.env[envVar];
4568
+ const source = fromStore ? "store" : fromEnv ? "env" : "none";
4569
+ entries.push({
4570
+ service,
4571
+ envVar,
4572
+ required,
4573
+ description,
4574
+ configured: source !== "none",
4575
+ source,
4576
+ last4: fromStore ? fromStore.last4 : fromEnv ? fromEnv.slice(-4) : ""
4577
+ });
4578
+ }
4579
+ }
4580
+ for (const e of stored.values()) {
4581
+ if (serviceFilter && e.service !== serviceFilter) continue;
4582
+ entries.push({
4583
+ service: e.service,
4584
+ envVar: e.envVar,
4585
+ required: false,
4586
+ description: "Stored connection reference",
4587
+ configured: true,
4588
+ source: "store",
4589
+ last4: e.last4
4590
+ });
4591
+ }
4592
+ return { enabled: store?.enabled ?? false, entries };
4593
+ }
4594
+ async set(service, envVar, value) {
4595
+ const store = getInstalledCredentialStore();
4596
+ if (!store) {
4597
+ throw new Error("No credential store is installed on this host \u2014 set credentials via the environment");
4598
+ }
4599
+ this.assertSettable(service, envVar);
4600
+ const entry = await store.set(service, envVar, value);
4601
+ return { saved: true, service, envVar, last4: entry.last4 };
4602
+ }
4603
+ async remove(service, envVar) {
4604
+ const store = getInstalledCredentialStore();
4605
+ if (!store) {
4606
+ throw new Error("No credential store is installed on this host");
4607
+ }
4608
+ this.assertSettable(service, envVar);
4609
+ return { removed: await store.remove(envVar) };
4610
+ }
4611
+ async test(service, context) {
4612
+ const declared = this.declaredFor(service);
4613
+ const missing = declared.filter((c) => c.required && !resolveCredentialRef(c.envVar)).map((c) => c.envVar);
4614
+ const configured = missing.length === 0;
4615
+ if (!configured) {
4616
+ return { service, configured, missing, probed: false, ok: false, message: `Missing required credentials: ${missing.join(", ")}` };
4617
+ }
4618
+ const factory = getActiveFactory();
4619
+ const probe = isProbeService(service) ? serviceProbes[service] : void 0;
4620
+ if (!factory || !probe) {
4621
+ return { service, configured, missing, probed: false, ok: true, message: "Credentials present (no live probe declared for this service)" };
4622
+ }
4623
+ if (!factory.isConfigured(service)) {
4624
+ 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" };
4625
+ }
4626
+ const result = await factory.execute(service, probe.action, probe.params, context);
4627
+ if (!result.success) {
4628
+ return { service, configured, missing, probed: true, ok: false, message: result.error?.message ?? `Probe ${probe.action} failed` };
4629
+ }
4630
+ const echoed = result.data !== null && typeof result.data === "object" && "_mock" in result.data;
4631
+ if (echoed) {
4632
+ return { service, configured, missing, probed: false, ok: false, message: "Probe was mock-echoed \u2014 the service is not actually configured" };
4633
+ }
4634
+ return { service, configured, missing, probed: true, ok: true, message: `Probe ${probe.action} succeeded` };
4635
+ }
4636
+ };
4637
+ registerIntegration("credentials", CredentialsIntegration);
4638
+ function isParamRecord2(value) {
4639
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
4640
+ }
4641
+ function toFilePayload(value) {
4642
+ if (!isParamRecord2(value)) return null;
4643
+ const { name, size, type, content } = value;
4644
+ if (typeof name !== "string") return null;
4645
+ return {
4646
+ name,
4647
+ size: typeof size === "number" ? size : 0,
4648
+ type: typeof type === "string" ? type : "application/octet-stream",
4649
+ content: typeof content === "string" ? content : void 0
4650
+ };
4651
+ }
4652
+ function decodeContent2(content) {
4653
+ const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
4654
+ if (dataUrlMatch) {
4655
+ const [, mime, isB64, body] = dataUrlMatch;
4656
+ const bytes = isB64 ? Buffer.from(body, "base64") : Buffer.from(decodeURIComponent(body), "utf8");
4657
+ return { bytes, contentType: mime || null };
4658
+ }
4659
+ return { bytes: Buffer.from(content, "utf8"), contentType: null };
4660
+ }
2839
4661
  var StorageIntegration = class extends BaseIntegration {
2840
4662
  constructor(config) {
2841
4663
  super(config);
2842
4664
  this.objects = /* @__PURE__ */ new Map();
2843
- const storageUrl = config.env.STORAGE_URL;
2844
- if (storageUrl) {
2845
- this.logger.warn(
2846
- "STORAGE_URL is configured but real storage client is not yet implemented. Falling back to in-memory store.",
2847
- { storageUrl }
2848
- );
4665
+ this.s3 = null;
4666
+ this.defaultBucket = config.env.STORAGE_BUCKET || "";
4667
+ this.publicUrlBase = config.env.STORAGE_PUBLIC_URL_BASE || "";
4668
+ const accessKeyId = config.env.STORAGE_ACCESS_KEY_ID || "";
4669
+ const secretAccessKey = config.env.STORAGE_SECRET_ACCESS_KEY || "";
4670
+ if (accessKeyId && secretAccessKey) {
4671
+ const endpoint = config.env.STORAGE_ENDPOINT || void 0;
4672
+ this.s3 = new S3Client({
4673
+ region: config.env.STORAGE_REGION || "us-east-1",
4674
+ endpoint,
4675
+ // Path-style is what MinIO/R2-style endpoints expect.
4676
+ forcePathStyle: Boolean(endpoint),
4677
+ credentials: { accessKeyId, secretAccessKey }
4678
+ });
4679
+ this.logger.info("Storage integration initialized (S3 backend)", {
4680
+ endpoint: endpoint ?? "aws",
4681
+ bucket: this.defaultBucket
4682
+ });
4683
+ } else {
4684
+ if (process.env.NODE_ENV === "production") {
4685
+ throw new Error(
4686
+ "Storage credentials missing in production (STORAGE_ACCESS_KEY_ID / STORAGE_SECRET_ACCESS_KEY) \u2014 refusing the in-memory fallback. See SECRETS.md."
4687
+ );
4688
+ }
4689
+ this.logger.warn("Storage integration initialized (in-memory backend \u2014 dev only, nothing persists)");
2849
4690
  }
2850
- this.logger.info("Storage integration initialized (in-memory backend)");
2851
4691
  }
2852
4692
  async execute(action, params) {
2853
4693
  const validation = this.validateParams(action, params);
@@ -2897,107 +4737,194 @@ var StorageIntegration = class extends BaseIntegration {
2897
4737
  // ---------------------------------------------------------------------------
2898
4738
  // Helpers
2899
4739
  // ---------------------------------------------------------------------------
2900
- /** Build a composite key from bucket and object key. */
4740
+ bucketOf(params) {
4741
+ return params.bucket || this.defaultBucket;
4742
+ }
2901
4743
  compositeKey(bucket, key) {
2902
4744
  return `${bucket}/${key}`;
2903
4745
  }
2904
- /** Generate a deterministic etag from content. */
2905
- generateEtag(content) {
2906
- const raw = typeof content === "string" ? content : JSON.stringify(content);
2907
- let hash = 0;
2908
- for (let i = 0; i < raw.length; i++) {
2909
- const ch = raw.charCodeAt(i);
2910
- hash = (hash << 5) - hash + ch | 0;
4746
+ generateEtag(bytes) {
4747
+ return `"${createHash("md5").update(bytes).digest("hex")}"`;
4748
+ }
4749
+ /** Resolve the upload inputs from either admitted shape. */
4750
+ resolveUpload(params) {
4751
+ const file = toFilePayload(params.file);
4752
+ if (file) {
4753
+ const maxSize = typeof params.maxSize === "number" ? params.maxSize : 0;
4754
+ if (maxSize > 0 && file.size > maxSize) {
4755
+ throw new Error(`Upload rejected: ${file.name} is ${file.size} bytes (max ${maxSize})`);
4756
+ }
4757
+ if (!file.content) {
4758
+ throw new Error(
4759
+ `Upload rejected: file payload for '${file.name}' carries no content \u2014 the uploader must include the base64 data URL`
4760
+ );
4761
+ }
4762
+ const { bytes: bytes2, contentType: contentType2 } = decodeContent2(file.content);
4763
+ const safeName = file.name.replace(/[^A-Za-z0-9._-]/g, "_");
4764
+ return {
4765
+ key: `${Date.now()}-${safeName}`,
4766
+ bytes: bytes2,
4767
+ contentType: contentType2 ?? file.type,
4768
+ acl: params.acl === "public" ? "public-read" : void 0
4769
+ };
4770
+ }
4771
+ const key = params.key;
4772
+ const content = params.content;
4773
+ if (!key || content === void 0 || content === null) {
4774
+ throw new Error("upload requires either `file` (with content) or the `key` + `content` pair");
2911
4775
  }
2912
- return `"${Math.abs(hash).toString(16).padStart(8, "0")}"`;
4776
+ const raw = typeof content === "string" ? content : JSON.stringify(content);
4777
+ const { bytes, contentType } = decodeContent2(raw);
4778
+ return {
4779
+ key,
4780
+ bytes,
4781
+ contentType: params.contentType || contentType || "application/octet-stream",
4782
+ acl: params.acl === "public" ? "public-read" : void 0
4783
+ };
2913
4784
  }
2914
- /** Compute the byte size of content. */
2915
- computeSize(content) {
2916
- if (typeof content === "string") {
2917
- return new TextEncoder().encode(content).byteLength;
4785
+ publicUrl(bucket, key) {
4786
+ if (this.publicUrlBase) {
4787
+ return `${this.publicUrlBase.replace(/\/$/, "")}/${key}`;
4788
+ }
4789
+ const endpoint = this.config.env.STORAGE_ENDPOINT;
4790
+ if (endpoint) {
4791
+ return `${endpoint.replace(/\/$/, "")}/${bucket}/${key}`;
2918
4792
  }
2919
- return JSON.stringify(content).length;
4793
+ const region = this.config.env.STORAGE_REGION || "us-east-1";
4794
+ return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
2920
4795
  }
2921
4796
  // ---------------------------------------------------------------------------
2922
4797
  // Actions
2923
4798
  // ---------------------------------------------------------------------------
2924
4799
  async upload(params) {
2925
- const bucket = params.bucket;
2926
- const key = params.key;
2927
- const content = params.content;
2928
- const contentType = params.contentType ?? "application/octet-stream";
2929
- const metadata = params.metadata ?? {};
2930
- this.logger.debug("Storage UPLOAD", { bucket, key, contentType });
2931
- const size = this.computeSize(content);
2932
- const etag = this.generateEtag(content);
2933
- const obj = {
2934
- content,
2935
- contentType,
2936
- size,
2937
- metadata,
2938
- lastModified: Date.now(),
2939
- etag
2940
- };
2941
- this.objects.set(this.compositeKey(bucket, key), obj);
2942
- return { key, bucket, size, etag };
4800
+ const bucket = this.bucketOf(params);
4801
+ const { key, bytes, contentType, acl } = this.resolveUpload(params);
4802
+ const etag = this.generateEtag(bytes);
4803
+ this.logger.debug("Storage UPLOAD", { bucket, key, contentType, size: bytes.length });
4804
+ if (this.s3) {
4805
+ await this.s3.send(
4806
+ new PutObjectCommand({
4807
+ Bucket: bucket,
4808
+ Key: key,
4809
+ Body: bytes,
4810
+ ContentType: contentType,
4811
+ ACL: acl
4812
+ })
4813
+ );
4814
+ } else {
4815
+ this.objects.set(this.compositeKey(bucket, key), {
4816
+ content: bytes.toString("base64"),
4817
+ contentType,
4818
+ size: bytes.length,
4819
+ metadata: params.metadata ?? {},
4820
+ lastModified: Date.now(),
4821
+ etag
4822
+ });
4823
+ }
4824
+ const url = acl === "public-read" ? this.publicUrl(bucket, key) : (await this.signUrl(bucket, key, "get", 3600)).url;
4825
+ return { key, bucket, size: bytes.length, etag, id: key, url };
2943
4826
  }
2944
4827
  async download(params) {
2945
- const bucket = params.bucket;
4828
+ const bucket = this.bucketOf(params);
2946
4829
  const key = params.key;
2947
4830
  this.logger.debug("Storage DOWNLOAD", { bucket, key });
4831
+ if (this.s3) {
4832
+ const response = await this.s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
4833
+ const bytes = Buffer.from(await response.Body.transformToByteArray());
4834
+ return {
4835
+ content: bytes.toString("base64"),
4836
+ contentType: response.ContentType ?? "application/octet-stream",
4837
+ size: bytes.length,
4838
+ metadata: {}
4839
+ };
4840
+ }
2948
4841
  const obj = this.objects.get(this.compositeKey(bucket, key));
2949
4842
  if (!obj) {
2950
4843
  throw new Error(`Object not found: ${bucket}/${key}`);
2951
4844
  }
2952
4845
  return {
2953
- content: obj.content,
4846
+ content: String(obj.content),
2954
4847
  contentType: obj.contentType,
2955
4848
  size: obj.size,
2956
4849
  metadata: obj.metadata
2957
4850
  };
2958
4851
  }
2959
4852
  async list(params) {
2960
- const bucket = params.bucket;
4853
+ const bucket = this.bucketOf(params);
2961
4854
  const prefix = params.prefix ?? "";
2962
4855
  const maxKeys = params.maxKeys ?? 1e3;
4856
+ const continuationToken = params.continuationToken;
2963
4857
  this.logger.debug("Storage LIST", { bucket, prefix, maxKeys });
4858
+ if (this.s3) {
4859
+ const response = await this.s3.send(
4860
+ new ListObjectsV2Command({
4861
+ Bucket: bucket,
4862
+ Prefix: prefix || void 0,
4863
+ MaxKeys: maxKeys,
4864
+ ContinuationToken: continuationToken
4865
+ })
4866
+ );
4867
+ return {
4868
+ keys: (response.Contents ?? []).map((entry) => ({
4869
+ key: entry.Key ?? "",
4870
+ size: entry.Size ?? 0,
4871
+ lastModified: entry.LastModified?.getTime() ?? 0
4872
+ })),
4873
+ truncated: Boolean(response.IsTruncated),
4874
+ ...response.IsTruncated && response.NextContinuationToken !== void 0 ? { nextToken: response.NextContinuationToken } : {}
4875
+ };
4876
+ }
2964
4877
  const bucketPrefix = `${bucket}/`;
2965
4878
  const fullPrefix = `${bucket}/${prefix}`;
2966
4879
  const results = [];
2967
4880
  for (const [compositeKey, obj] of this.objects) {
2968
4881
  if (!compositeKey.startsWith(fullPrefix)) continue;
2969
- const objectKey = compositeKey.slice(bucketPrefix.length);
2970
4882
  results.push({
2971
- key: objectKey,
4883
+ key: compositeKey.slice(bucketPrefix.length),
2972
4884
  size: obj.size,
2973
4885
  lastModified: obj.lastModified
2974
4886
  });
2975
4887
  }
2976
4888
  results.sort((a, b) => a.key.localeCompare(b.key));
2977
- const truncated = results.length > maxKeys;
4889
+ const offset = continuationToken !== void 0 ? Number.parseInt(continuationToken, 10) || 0 : 0;
4890
+ const page = results.slice(offset, offset + maxKeys);
4891
+ const truncated = offset + maxKeys < results.length;
2978
4892
  return {
2979
- keys: results.slice(0, maxKeys),
2980
- truncated
4893
+ keys: page,
4894
+ truncated,
4895
+ ...truncated ? { nextToken: String(offset + maxKeys) } : {}
2981
4896
  };
2982
4897
  }
2983
4898
  async deleteObject(params) {
2984
- const bucket = params.bucket;
4899
+ const bucket = this.bucketOf(params);
2985
4900
  const key = params.key;
2986
4901
  this.logger.debug("Storage DELETE", { bucket, key });
4902
+ if (this.s3) {
4903
+ await this.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
4904
+ return { deleted: true };
4905
+ }
2987
4906
  const existed = this.objects.has(this.compositeKey(bucket, key));
2988
4907
  this.objects.delete(this.compositeKey(bucket, key));
2989
4908
  return { deleted: existed };
2990
4909
  }
4910
+ async signUrl(bucket, key, operation, expiresIn) {
4911
+ const expiresAt = Date.now() + expiresIn * 1e3;
4912
+ if (this.s3) {
4913
+ const command = operation === "put" ? new PutObjectCommand({ Bucket: bucket, Key: key }) : new GetObjectCommand({ Bucket: bucket, Key: key });
4914
+ const url2 = await getSignedUrl(this.s3, command, { expiresIn });
4915
+ return { url: url2, expiresAt };
4916
+ }
4917
+ const token = createHash("sha256").update(`${bucket}/${key}/${expiresAt}`).digest("hex").slice(0, 16);
4918
+ 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}`;
4919
+ return { url, expiresAt };
4920
+ }
2991
4921
  async getSignedUrl(params) {
2992
- const bucket = params.bucket;
4922
+ const bucket = this.bucketOf(params);
2993
4923
  const key = params.key;
2994
4924
  const expiresIn = params.expiresIn ?? 3600;
2995
4925
  const operation = params.operation ?? "get";
2996
4926
  this.logger.debug("Storage GET_SIGNED_URL", { bucket, key, expiresIn, operation });
2997
- const expiresAt = Date.now() + expiresIn * 1e3;
2998
- const token = Math.random().toString(36).slice(2, 18);
2999
- const url = `https://storage.mock.local/${bucket}/${key}?X-Amz-Algorithm=MOCK-HMAC-SHA256&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host&X-Amz-Signature=${token}&operation=${operation}`;
3000
- return { url, expiresAt };
4927
+ return this.signUrl(bucket, key, operation, expiresIn);
3001
4928
  }
3002
4929
  };
3003
4930
  registerIntegration("storage", StorageIntegration);
@@ -3504,10 +5431,10 @@ var DatabaseIntegration = class extends BaseIntegration {
3504
5431
  }
3505
5432
  /** Resolve (and cache) the driver for a connection reference. */
3506
5433
  driverFor(connectionRef) {
3507
- const connectionString = process.env[connectionRef];
5434
+ const connectionString = resolveCredentialRef(connectionRef);
3508
5435
  if (!connectionString) {
3509
5436
  throw new IntegrationError(
3510
- `Connection reference "${connectionRef}" is not set in the environment`,
5437
+ `Connection reference "${connectionRef}" is not set in the credential store or environment`,
3511
5438
  "AUTH_ERROR"
3512
5439
  );
3513
5440
  }
@@ -3759,6 +5686,6 @@ var ArxivIntegration = class extends BaseIntegration {
3759
5686
  };
3760
5687
  registerIntegration("arxiv", ArxivIntegration);
3761
5688
 
3762
- export { ArxivIntegration, BaseIntegration, CLIIntegration, ConsoleLogger, DatabaseIntegration, DeepAgentIntegration, DockerIntegration, EmailIntegration, GitHubIntegration, IconifyIntegration, IntegrationError, IntegrationFactory, LLMIntegration, MLIntegration, OAuthIntegration, OtelIntegration, QueueIntegration, RedisIntegration, StorageIntegration, StripeIntegration, TwilioIntegration, WebhookIntegration, WikimediaIntegration, YouTubeIntegration, assertReadOnlySelect, getIntegration, getIntegrationFactory, getRegisteredIntegrations, isKnownIntegration, registerIntegration, resetIntegrationFactory, validateParams, verifyAndParseStripeEvent, withRetry };
5689
+ export { AccountingIntegration, ArxivIntegration, BankingIntegration, BaseIntegration, CLIIntegration, CREDENTIALS_FILE_ENV, CREDENTIAL_ENTITY_TYPE, CREDENTIAL_MASTER_KEY_ENV, CalendarIntegration, ConsoleLogger, CredentialStore, CredentialsIntegration, DatabaseIntegration, DeepAgentIntegration, DockerIntegration, DriveIntegration, EmailIntegration, EsignIntegration, FileCredentialPersistence, GitHubIntegration, IconifyIntegration, InMemoryPendingGrantStore, IntegrationError, IntegrationFactory, LLMIntegration, MLIntegration, MetaAdsIntegration, OAuthIntegration, OtelIntegration, PushIntegration, QueueIntegration, RedisIntegration, StorageIntegration, StripeIntegration, TwilioIntegration, WebhookIntegration, WikimediaIntegration, YouTubeIntegration, allHookDeclarations, assertReadOnlySelect, getActiveFactory, getInstalledCredentialStore, getIntegration, getIntegrationFactory, getRegisteredIntegrations, googleCalendarHookProvider, installActiveFactory, installCredentialStore, installPendingGrantStore, isKnownIntegration, parseCalendarPushNotification, registerIntegration, registeredHookProviders, resetIntegrationFactory, resolveCredentialRef, serviceHooks, uninstallCredentialStore, validateParams, verifyAndParseStripeEvent, withRetry };
3763
5690
  //# sourceMappingURL=index.js.map
3764
5691
  //# sourceMappingURL=index.js.map