@almadar/integrations 2.25.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,6 +1,8 @@
1
1
  import { createLogger } from '@almadar/logger';
2
2
  import { integratorsRegistry } from '@almadar/core/patterns';
3
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';
4
6
  import Stripe from 'stripe';
5
7
  import { google } from 'googleapis';
6
8
  import twilio from 'twilio';
@@ -11,8 +13,6 @@ import { Readable } from 'stream';
11
13
  import { getAvailableProvider, LLMClient, EmbeddingClient } from '@almadar/llm';
12
14
  import { z } from 'zod';
13
15
  import { execSync, spawn } from 'child_process';
14
- import { mkdtempSync, writeFileSync, rmSync, promises } from 'fs';
15
- import { join } from 'path';
16
16
  import { tmpdir } from 'os';
17
17
  import * as oidc from 'openid-client';
18
18
  import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand } from '@aws-sdk/client-s3';
@@ -38,6 +38,143 @@ var IntegrationError = class extends Error {
38
38
  };
39
39
  }
40
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
+ };
41
178
  var ConsoleLogger = class {
42
179
  constructor(_level = "info") {
43
180
  this.log = createLogger("almadar:integrations");
@@ -55,6 +192,7 @@ var ConsoleLogger = class {
55
192
  this.log.error(message, meta);
56
193
  }
57
194
  };
195
+ var RESERVED_ENVELOPE_KEYS = /* @__PURE__ */ new Set(["emit", "onSuccess", "onError", "timeout"]);
58
196
  function validateParams(integration, action, params) {
59
197
  const typedRegistry = integratorsRegistry;
60
198
  const registry = typedRegistry.integrators[integration];
@@ -77,6 +215,16 @@ function validateParams(integration, action, params) {
77
215
  };
78
216
  }
79
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
+ }
80
228
  for (const paramDef of actionDef.params) {
81
229
  if (paramDef.required && !(paramDef.name in params)) {
82
230
  errors.push({
@@ -272,7 +420,7 @@ var IntegrationFactory = class {
272
420
  */
273
421
  async execute(integration, action, params, context) {
274
422
  const instance = this.get(integration, context?.principal);
275
- return await instance.execute(action, params);
423
+ return await instance.execute(action, params, context);
276
424
  }
277
425
  /**
278
426
  * Check if integration is configured
@@ -329,24 +477,31 @@ function resetIntegrationFactory() {
329
477
  }
330
478
  var CREDENTIAL_ENTITY_TYPE = "AlmadarIntegrationCredential";
331
479
  var CREDENTIAL_MASTER_KEY_ENV = "ALMADAR_CREDENTIAL_MASTER_KEY";
480
+ var CREDENTIAL_PREVIOUS_MASTER_KEY_ENV = "ALMADAR_CREDENTIAL_MASTER_KEY_PREVIOUS";
332
481
  var ENCRYPTION_ALGORITHM = "aes-256-gcm";
333
482
  function isNonEmptyString(v) {
334
483
  return typeof v === "string" && v.length > 0;
335
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}$/;
336
489
  var CredentialStore = class {
337
- constructor(adapter, masterKey) {
490
+ constructor(adapter, masterKey, previousMasterKey) {
338
491
  this.byEnvVar = /* @__PURE__ */ new Map();
339
492
  this.listeners = /* @__PURE__ */ new Set();
340
493
  this.warmed = false;
341
494
  this.adapter = adapter;
342
495
  const key = masterKey ?? process.env[CREDENTIAL_MASTER_KEY_ENV];
343
- const valid = typeof key === "string" && /^[0-9a-fA-F]{64}$/.test(key);
496
+ const valid = typeof key === "string" && MASTER_KEY_SHAPE.test(key);
344
497
  if (!valid && process.env.NODE_ENV === "production") {
345
498
  throw new Error(
346
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'))"`
347
500
  );
348
501
  }
349
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;
350
505
  }
351
506
  /** True when a valid master key is present (writes and decryption enabled). */
352
507
  get enabled() {
@@ -368,15 +523,17 @@ var CredentialStore = class {
368
523
  const cipher = createCipheriv(ENCRYPTION_ALGORITHM, Buffer.from(this.masterKeyHex, "hex"), iv);
369
524
  let ciphertext = cipher.update(plaintext, "utf8", "hex");
370
525
  ciphertext += cipher.final("hex");
371
- return { ciphertext, iv: iv.toString("hex"), authTag: cipher.getAuthTag().toString("hex") };
526
+ return {
527
+ ciphertext,
528
+ iv: iv.toString("hex"),
529
+ authTag: cipher.getAuthTag().toString("hex"),
530
+ keyId: keyIdOf(this.masterKeyHex)
531
+ };
372
532
  }
373
- decrypt(ciphertext, ivHex, authTagHex) {
374
- if (!this.masterKeyHex) {
375
- throw new Error(`${CREDENTIAL_MASTER_KEY_ENV} is not configured \u2014 cannot decrypt credentials`);
376
- }
533
+ decryptWithKey(keyHex, ciphertext, ivHex, authTagHex) {
377
534
  const decipher = createDecipheriv(
378
535
  ENCRYPTION_ALGORITHM,
379
- Buffer.from(this.masterKeyHex, "hex"),
536
+ Buffer.from(keyHex, "hex"),
380
537
  Buffer.from(ivHex, "hex")
381
538
  );
382
539
  decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
@@ -384,6 +541,32 @@ var CredentialStore = class {
384
541
  plaintext += decipher.final("utf8");
385
542
  return plaintext;
386
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
+ }
387
570
  /**
388
571
  * Load and decrypt every stored row into memory. Returns the number of
389
572
  * resolvable credentials. Without a master key nothing decrypts (rows are
@@ -399,7 +582,13 @@ var CredentialStore = class {
399
582
  if (!isNonEmptyString(id) || !isNonEmptyString(service) || !isNonEmptyString(envVar) || !isNonEmptyString(ciphertext) || !isNonEmptyString(iv) || !isNonEmptyString(authTag)) {
400
583
  continue;
401
584
  }
402
- const value = this.decrypt(ciphertext, iv, authTag);
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;
403
592
  this.byEnvVar.set(envVar, {
404
593
  id,
405
594
  service,
@@ -428,10 +617,10 @@ var CredentialStore = class {
428
617
  /** Upsert one credential (encrypts, persists, re-warms the entry, notifies). */
429
618
  async set(service, envVar, value) {
430
619
  if (!this.warmed) await this.warm();
431
- const { ciphertext, iv, authTag } = this.encrypt(value);
620
+ const { ciphertext, iv, authTag, keyId } = this.encrypt(value);
432
621
  const updatedAt = Date.now();
433
622
  const existing = this.byEnvVar.get(envVar);
434
- const data = { service, envVar, ciphertext, iv, authTag, updatedAt };
623
+ const data = { service, envVar, ciphertext, iv, authTag, keyId, updatedAt };
435
624
  let id;
436
625
  if (existing) {
437
626
  id = existing.id;
@@ -445,6 +634,58 @@ var CredentialStore = class {
445
634
  const { service: s, envVar: e, last4, updatedAt: u } = entry;
446
635
  return { service: s, envVar: e, last4, updatedAt: u };
447
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
+ }
448
689
  /** Delete one credential row; resolution falls back to env afterwards. */
449
690
  async remove(envVar) {
450
691
  if (!this.warmed) await this.warm();
@@ -478,6 +719,58 @@ function uninstallCredentialStore() {
478
719
  function resolveCredentialRef(ref, env = process.env) {
479
720
  return installedStore?.resolve(ref) ?? env[ref];
480
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
+ };
481
774
  var STRIPE_API_VERSION = "2025-02-24.acacia";
482
775
  function priceToTier(priceId, prices) {
483
776
  if (priceId === prices.solo) return "solo";
@@ -1043,10 +1336,10 @@ var TwilioIntegration = class extends BaseIntegration {
1043
1336
  }
1044
1337
  }
1045
1338
  async sendSMS(params) {
1046
- const { to, body } = params;
1339
+ const { to, body, from } = params;
1047
1340
  this.logger.debug("Sending SMS", { to: String(to ?? "") });
1048
1341
  const message = await this.client.messages.create({
1049
- from: this.phoneNumber,
1342
+ from: from || this.phoneNumber,
1050
1343
  to,
1051
1344
  body
1052
1345
  });
@@ -1125,31 +1418,35 @@ var EmailIntegration = class extends BaseIntegration {
1125
1418
  }
1126
1419
  }
1127
1420
  async send(params) {
1128
- const { to, subject, body, from } = params;
1421
+ const { to, subject, body, from, htmlBody, replyTo, templateId } = params;
1129
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
+ };
1130
1432
  if (this.provider === "sendgrid") {
1131
- return await this.sendViaSendGrid(
1132
- to,
1133
- subject,
1134
- body,
1135
- from || this.fromEmail
1136
- );
1433
+ return await this.sendViaSendGrid(message);
1137
1434
  } else if (this.provider === "resend") {
1138
- return await this.sendViaResend(
1139
- to,
1140
- subject,
1141
- body,
1142
- from || this.fromEmail
1143
- );
1435
+ return await this.sendViaResend(message);
1144
1436
  }
1145
1437
  throw new Error(`Unknown email provider: ${this.provider}`);
1146
1438
  }
1147
- async sendViaSendGrid(to, subject, body, from) {
1439
+ async sendViaSendGrid(message) {
1148
1440
  const msg = {
1149
- to,
1150
- from,
1151
- subject,
1152
- html: body
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 } : {}
1153
1450
  };
1154
1451
  const response = await sgMail.send(msg);
1155
1452
  return {
@@ -1157,15 +1454,20 @@ var EmailIntegration = class extends BaseIntegration {
1157
1454
  status: "sent"
1158
1455
  };
1159
1456
  }
1160
- async sendViaResend(to, subject, body, from) {
1457
+ async sendViaResend(message) {
1161
1458
  if (!this.resendClient) {
1162
1459
  throw new Error("Resend client not initialized");
1163
1460
  }
1461
+ if (message.templateId) {
1462
+ throw new Error("templateId is not supported by the resend provider \u2014 use sendgrid or drop templateId");
1463
+ }
1164
1464
  const response = await this.resendClient.emails.send({
1165
- from,
1166
- to,
1167
- subject,
1168
- html: body
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 } : {}
1169
1471
  });
1170
1472
  return {
1171
1473
  id: response.data?.id,
@@ -1595,6 +1897,27 @@ var CalendarIntegration = class extends BaseIntegration {
1595
1897
  }
1596
1898
  };
1597
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
+ }
1598
1921
  function decodeContent(content) {
1599
1922
  const dataUrlMatch = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(content);
1600
1923
  if (dataUrlMatch) {
@@ -1608,20 +1931,53 @@ var DriveIntegration = class extends BaseIntegration {
1608
1931
  constructor(config) {
1609
1932
  super(config);
1610
1933
  const rawKey = config.env.GOOGLE_DRIVE_SA_KEY;
1611
- if (!rawKey) {
1612
- throw new Error("GOOGLE_DRIVE_SA_KEY not configured");
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;
1613
1957
  }
1614
- const keyJson = rawKey.trim().startsWith("{") ? rawKey : Buffer.from(rawKey, "base64").toString("utf8");
1615
- const key = JSON.parse(keyJson);
1616
- const subject = config.env.GOOGLE_DRIVE_SUBJECT || void 0;
1617
- const auth = new google.auth.JWT({
1618
- email: key.client_email,
1619
- key: key.private_key,
1620
- scopes: ["https://www.googleapis.com/auth/drive"],
1621
- subject
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)
1622
1968
  });
1623
- this.client = google.drive({ version: "v3", auth });
1624
- this.logger.info("Drive integration initialized", { delegated: Boolean(subject) });
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;
1625
1981
  }
1626
1982
  async execute(action, params) {
1627
1983
  const validation = this.validateParams(action, params);
@@ -1673,7 +2029,7 @@ var DriveIntegration = class extends BaseIntegration {
1673
2029
  const clauses = ["trashed = false"];
1674
2030
  if (folderId) clauses.push(`'${String(folderId).replace(/'/g, "\\'")}' in parents`);
1675
2031
  if (query) clauses.push(String(query));
1676
- const response = await this.client.files.list({
2032
+ const response = await this.readClient().files.list({
1677
2033
  q: clauses.join(" and "),
1678
2034
  pageSize: maxResults || 100,
1679
2035
  fields: "files(id, name, mimeType, size, modifiedTime, webViewLink)"
@@ -1691,11 +2047,11 @@ var DriveIntegration = class extends BaseIntegration {
1691
2047
  }
1692
2048
  async getFile(params) {
1693
2049
  const fileId = params.fileId;
1694
- const meta = await this.client.files.get({
2050
+ const meta = await this.readClient().files.get({
1695
2051
  fileId,
1696
2052
  fields: "id, name, mimeType, size"
1697
2053
  });
1698
- const content = await this.client.files.get(
2054
+ const content = await this.readClient().files.get(
1699
2055
  { fileId, alt: "media" },
1700
2056
  { responseType: "arraybuffer" }
1701
2057
  );
@@ -1711,10 +2067,11 @@ var DriveIntegration = class extends BaseIntegration {
1711
2067
  async uploadFile(params) {
1712
2068
  const { name, content, mimeType, folderId } = params;
1713
2069
  const { bytes, contentType } = decodeContent(content);
1714
- const response = await this.client.files.create({
2070
+ const parent = folderId || this.defaultFolderId;
2071
+ const response = await this.writeClient().files.create({
1715
2072
  requestBody: {
1716
2073
  name,
1717
- parents: folderId ? [folderId] : void 0
2074
+ parents: parent ? [parent] : void 0
1718
2075
  },
1719
2076
  media: {
1720
2077
  mimeType: mimeType || contentType || "application/octet-stream",
@@ -1730,11 +2087,12 @@ var DriveIntegration = class extends BaseIntegration {
1730
2087
  }
1731
2088
  async createFolder(params) {
1732
2089
  const { name, parentId } = params;
1733
- const response = await this.client.files.create({
2090
+ const parent = parentId || this.defaultFolderId;
2091
+ const response = await this.writeClient().files.create({
1734
2092
  requestBody: {
1735
2093
  name,
1736
2094
  mimeType: "application/vnd.google-apps.folder",
1737
- parents: parentId ? [parentId] : void 0
2095
+ parents: parent ? [parent] : void 0
1738
2096
  },
1739
2097
  fields: "id, name"
1740
2098
  });
@@ -1742,7 +2100,7 @@ var DriveIntegration = class extends BaseIntegration {
1742
2100
  }
1743
2101
  async shareFile(params) {
1744
2102
  const { fileId, email, role } = params;
1745
- const response = await this.client.permissions.create({
2103
+ const response = await this.readClient().permissions.create({
1746
2104
  fileId,
1747
2105
  requestBody: {
1748
2106
  type: "user",
@@ -3756,6 +4114,31 @@ var PROVIDER_AUTH_URLS = {
3756
4114
  var PROVIDER_ISSUERS = {
3757
4115
  google: "https://accounts.google.com"
3758
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
+ }
3759
4142
  var OAuthIntegration = class extends BaseIntegration {
3760
4143
  constructor(config) {
3761
4144
  super(config);
@@ -3767,8 +4150,8 @@ var OAuthIntegration = class extends BaseIntegration {
3767
4150
  this.refreshIndex = /* @__PURE__ */ new Map();
3768
4151
  /** Maps access token -> mock user session */
3769
4152
  this.sessions = /* @__PURE__ */ new Map();
3770
- /** Maps state -> pending OIDC authorization (real backend) */
3771
- this.pending = /* @__PURE__ */ new Map();
4153
+ /** Pending OIDC authorizations (real backend) — injectable, in-memory default. */
4154
+ this.fallbackPending = new InMemoryPendingGrantStore();
3772
4155
  /** Maps access token -> ID-token subject, for userinfo subject checks */
3773
4156
  this.subjects = /* @__PURE__ */ new Map();
3774
4157
  /** Discovered issuer configurations, keyed by issuer URL */
@@ -3778,6 +4161,9 @@ var OAuthIntegration = class extends BaseIntegration {
3778
4161
  this.real ? "OAuth integration initialized (OIDC backend via openid-client)" : "OAuth integration initialized (mock backend)"
3779
4162
  );
3780
4163
  }
4164
+ pendingStore() {
4165
+ return installedPendingGrantStore ?? this.fallbackPending;
4166
+ }
3781
4167
  async execute(action, params) {
3782
4168
  const validation = this.validateParams(action, params);
3783
4169
  if (!validation.valid) {
@@ -3869,17 +4255,17 @@ var OAuthIntegration = class extends BaseIntegration {
3869
4255
  parameters.prompt = "consent";
3870
4256
  }
3871
4257
  const authUrl = oidc.buildAuthorizationUrl(configuration, parameters);
3872
- this.pending.set(state, { provider, redirectUri, pkceVerifier });
4258
+ await this.pendingStore().put(state, { provider, redirectUri, pkceVerifier }, PENDING_GRANT_TTL_MS);
4259
+ void this.pendingStore().sweep();
3873
4260
  return { authUrl: authUrl.toString(), state };
3874
4261
  }
3875
4262
  async oidcToken(params) {
3876
4263
  const code = params.code;
3877
4264
  const state = params.state;
3878
- const pendingAuth = this.pending.get(state);
4265
+ const pendingAuth = await this.pendingStore().take(state);
3879
4266
  if (!pendingAuth) {
3880
4267
  throw new Error(`Invalid or expired state token: ${state}`);
3881
4268
  }
3882
- this.pending.delete(state);
3883
4269
  const configuration = await this.configurationFor(pendingAuth.provider);
3884
4270
  const callbackUrl = new URL(pendingAuth.redirectUri);
3885
4271
  callbackUrl.searchParams.set("code", code);
@@ -4066,131 +4452,10 @@ var OAuthIntegration = class extends BaseIntegration {
4066
4452
  };
4067
4453
  registerIntegration("oauth", OAuthIntegration);
4068
4454
 
4069
- // src/contracts.ts
4070
- var serviceCredentials = {
4071
- stripe: [
4072
- { envVar: "STRIPE_SECRET_KEY", required: true, description: "Stripe secret API key" },
4073
- { envVar: "STRIPE_WEBHOOK_SECRET", required: false, description: "Webhook endpoint signing secret" }
4074
- ],
4075
- youtube: [
4076
- { envVar: "YOUTUBE_API_KEY", required: true, description: "YouTube Data API key" }
4077
- ],
4078
- twilio: [
4079
- { envVar: "TWILIO_ACCOUNT_SID", required: true, description: "Twilio account SID" },
4080
- { envVar: "TWILIO_AUTH_TOKEN", required: true, description: "Twilio auth token" },
4081
- { envVar: "TWILIO_PHONE_NUMBER", required: false, description: "Default sender phone number" }
4082
- ],
4083
- email: [
4084
- { envVar: "SENDGRID_API_KEY", required: false, description: "SendGrid API key (use this OR RESEND_API_KEY)" },
4085
- { envVar: "RESEND_API_KEY", required: false, description: "Resend API key (use this OR SENDGRID_API_KEY)" },
4086
- { envVar: "FROM_EMAIL", required: false, description: "Default sender email address" }
4087
- ],
4088
- webhook: [
4089
- { envVar: "WEBHOOK_SIGNING_SECRET", required: false, description: "Default HMAC-SHA256 signing secret (per-call secret overrides)" },
4090
- { envVar: "WEBHOOK_TIMEOUT_MS", required: false, description: "Per-request timeout (ms, default 10000)" }
4091
- ],
4092
- push: [
4093
- { envVar: "VAPID_PUBLIC_KEY", required: true, description: "VAPID public key (web-push generate-vapid-keys)" },
4094
- { envVar: "VAPID_PRIVATE_KEY", required: true, description: "VAPID private key" },
4095
- { envVar: "VAPID_SUBJECT", required: true, description: "VAPID subject (mailto: or https: contact URI)" }
4096
- ],
4097
- calendar: [
4098
- { envVar: "GOOGLE_CALENDAR_SA_KEY", required: true, description: "Google service-account key JSON (raw or base64) with calendar scope; store in Secret Manager, bind as env" },
4099
- { envVar: "GOOGLE_CALENDAR_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" },
4100
- { envVar: "GOOGLE_CALENDAR_ID", required: false, description: "Default calendar id (default: primary)" }
4101
- ],
4102
- drive: [
4103
- { envVar: "GOOGLE_DRIVE_SA_KEY", required: true, description: "Google service-account key JSON (raw or base64) with drive scope; store in Secret Manager, bind as env" },
4104
- { envVar: "GOOGLE_DRIVE_SUBJECT", required: false, description: "Workspace user to impersonate (domain-wide delegation); empty = act as the service account" }
4105
- ],
4106
- metaAds: [
4107
- { envVar: "META_ACCESS_TOKEN", required: true, description: "Meta Graph API access token (Marketing API, ads_read)" },
4108
- { envVar: "META_AD_ACCOUNT_ID", required: false, description: "Default ad account id (act_\u2026); per-call accountId overrides" }
4109
- ],
4110
- accounting: [],
4111
- banking: [
4112
- { envVar: "GOCARDLESS_SECRET_ID", required: true, description: "GoCardless Bank Account Data secret id" },
4113
- { envVar: "GOCARDLESS_SECRET_KEY", required: true, description: "GoCardless Bank Account Data secret key" }
4114
- ],
4115
- esign: [
4116
- { envVar: "DOCUSIGN_BASE_URL", required: true, description: "DocuSign REST base (e.g. https://demo.docusign.net/restapi/v2.1/accounts/<accountId>)" },
4117
- { envVar: "DOCUSIGN_ACCESS_TOKEN", required: true, description: "DocuSign OAuth access token (JWT grant rotation is the deployment concern)" }
4118
- ],
4119
- llm: [
4120
- { envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key (use this OR OPENAI_API_KEY)" },
4121
- { envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key (use this OR ANTHROPIC_API_KEY)" }
4122
- ],
4123
- "llm-integration": [
4124
- { envVar: "ANTHROPIC_API_KEY", required: false, description: "Anthropic API key" },
4125
- { envVar: "OPENAI_API_KEY", required: false, description: "OpenAI API key" }
4126
- ],
4127
- ml: [
4128
- { envVar: "MASAR_URL", required: true, description: "Base URL of the deployed model-serving orbital" },
4129
- { envVar: "MASAR_ML_TRAIT", required: true, description: "Kebab-case trait name the serving orbital exposes its /events route under" }
4130
- ],
4131
- deepagent: [
4132
- { envVar: "DEEPAGENT_API_URL", required: true, description: "DeepAgent server URL" },
4133
- { envVar: "DEEPAGENT_API_KEY", required: false, description: "DeepAgent API key" }
4134
- ],
4135
- github: [
4136
- { envVar: "GITHUB_TOKEN", required: true, description: "GitHub personal access token" },
4137
- { envVar: "GITHUB_OWNER", required: false, description: "Default repository owner" },
4138
- { envVar: "GITHUB_REPO", required: false, description: "Default repository name" }
4139
- ],
4140
- docker: [
4141
- { envVar: "DOCKER_HOST", required: false, description: "Docker daemon host URL" }
4142
- ],
4143
- storage: [
4144
- { envVar: "STORAGE_ACCESS_KEY_ID", required: true, description: "S3-compatible access key id" },
4145
- { envVar: "STORAGE_SECRET_ACCESS_KEY", required: true, description: "S3-compatible secret access key" },
4146
- { envVar: "STORAGE_BUCKET", required: true, description: "Default bucket name" },
4147
- { envVar: "STORAGE_REGION", required: false, description: "Region (default us-east-1)" },
4148
- { envVar: "STORAGE_ENDPOINT", required: false, description: "Custom S3-compatible endpoint (R2/MinIO/\u2026); empty = AWS S3" },
4149
- { envVar: "STORAGE_PUBLIC_URL_BASE", required: false, description: "Base URL for public-acl object links; empty = endpoint-derived" }
4150
- ],
4151
- queue: [
4152
- { envVar: "QUEUE_URL", required: true, description: "Message queue connection URL" }
4153
- ],
4154
- redis: [
4155
- { envVar: "REDIS_URL", required: true, description: "Redis connection URL" }
4156
- ],
4157
- oauth: [
4158
- { envVar: "OAUTH_CLIENT_ID", required: true, description: "OIDC client ID" },
4159
- { envVar: "OAUTH_CLIENT_SECRET", required: true, description: "OIDC client secret" },
4160
- { envVar: "OAUTH_REDIRECT_URI", required: false, description: "OIDC redirect URI (default per-call param)" },
4161
- { envVar: "OIDC_ISSUER_URL", required: false, description: "OIDC issuer URL (default https://accounts.google.com)" }
4162
- ],
4163
- credentials: [
4164
- { envVar: "ALMADAR_CREDENTIAL_MASTER_KEY", required: false, description: "AES-256 master key (64-char hex) enabling the hosted credential store \u2014 hold it alone in the platform secret store" }
4165
- ],
4166
- otel: [
4167
- { envVar: "OTEL_EXPORTER_OTLP_ENDPOINT", required: true, description: "OpenTelemetry collector endpoint" },
4168
- { envVar: "OTEL_SERVICE_NAME", required: false, description: "Service name for traces" }
4169
- ],
4170
- cli: [],
4171
- // No fixed env vars — connection strings are resolved per-query from the
4172
- // caller-supplied connectionRef, so credentials cannot be declared statically.
4173
- database: [],
4174
- wikimedia: [
4175
- { envVar: "WIKIMEDIA_USER_AGENT", required: false, description: "Descriptive User-Agent for the Wikipedia API" },
4176
- { envVar: "WIKIMEDIA_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
4177
- ],
4178
- iconify: [
4179
- { envVar: "ICONIFY_USER_AGENT", required: false, description: "User-Agent for the Iconify API" },
4180
- { envVar: "ICONIFY_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
4181
- ],
4182
- arxiv: [
4183
- { envVar: "ARXIV_TIMEOUT_MS", required: false, description: "Per-request timeout (ms)" }
4184
- ]
4185
- };
4186
- var serviceProbes = {
4187
- calendar: { action: "listEvents", params: { maxResults: 1 } },
4188
- drive: { action: "listFiles", params: { maxResults: 1 } },
4189
- metaAds: { action: "listCampaigns", params: {} }
4190
- };
4191
-
4192
4455
  // src/integrations/credentials/index.ts
4193
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"];
4194
4459
  function isProbeService(service) {
4195
4460
  return service in serviceProbes;
4196
4461
  }
@@ -4199,7 +4464,20 @@ var CredentialsIntegration = class extends BaseIntegration {
4199
4464
  super(config);
4200
4465
  this.logger.info("Credentials integration initialized (tenant credential store surface)");
4201
4466
  }
4202
- async execute(action, params) {
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
+ }
4203
4481
  const validation = this.validateParams(action, params);
4204
4482
  if (!validation.valid) {
4205
4483
  return {
@@ -4227,8 +4505,16 @@ var CredentialsIntegration = class extends BaseIntegration {
4227
4505
  data = await this.remove(params.service, params.envVar);
4228
4506
  break;
4229
4507
  case "test":
4230
- data = await this.test(params.service);
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();
4231
4516
  break;
4517
+ }
4232
4518
  default:
4233
4519
  throw new Error(`Unknown action: ${action}`);
4234
4520
  }
@@ -4241,6 +4527,11 @@ var CredentialsIntegration = class extends BaseIntegration {
4241
4527
  return this.handleError(action, error);
4242
4528
  }
4243
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
+ }
4244
4535
  declaredFor(service) {
4245
4536
  return serviceCredentials[service] ?? [];
4246
4537
  }
@@ -4260,6 +4551,10 @@ var CredentialsIntegration = class extends BaseIntegration {
4260
4551
  throw new Error(`"${envVar}" is not a declared credential of "${service}" (declared: ${valid})`);
4261
4552
  }
4262
4553
  }
4554
+ storeFirst() {
4555
+ const flag = this.config.env["ALMADAR_CREDENTIALS_SOURCE"] || process.env["ALMADAR_CREDENTIALS_SOURCE"];
4556
+ return flag === "store";
4557
+ }
4263
4558
  list(serviceFilter) {
4264
4559
  const store = getInstalledCredentialStore();
4265
4560
  const stored = new Map((store?.entries() ?? []).map((e) => [`${e.service}\0${e.envVar}`, e]));
@@ -4269,7 +4564,7 @@ var CredentialsIntegration = class extends BaseIntegration {
4269
4564
  for (const { envVar, required, description } of declared) {
4270
4565
  const fromStore = stored.get(`${service}\0${envVar}`);
4271
4566
  stored.delete(`${service}\0${envVar}`);
4272
- const fromEnv = process.env[envVar];
4567
+ const fromEnv = this.storeFirst() ? void 0 : process.env[envVar];
4273
4568
  const source = fromStore ? "store" : fromEnv ? "env" : "none";
4274
4569
  entries.push({
4275
4570
  service,
@@ -4313,7 +4608,7 @@ var CredentialsIntegration = class extends BaseIntegration {
4313
4608
  this.assertSettable(service, envVar);
4314
4609
  return { removed: await store.remove(envVar) };
4315
4610
  }
4316
- async test(service) {
4611
+ async test(service, context) {
4317
4612
  const declared = this.declaredFor(service);
4318
4613
  const missing = declared.filter((c) => c.required && !resolveCredentialRef(c.envVar)).map((c) => c.envVar);
4319
4614
  const configured = missing.length === 0;
@@ -4328,7 +4623,7 @@ var CredentialsIntegration = class extends BaseIntegration {
4328
4623
  if (!factory.isConfigured(service)) {
4329
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" };
4330
4625
  }
4331
- const result = await factory.execute(service, probe.action, probe.params);
4626
+ const result = await factory.execute(service, probe.action, probe.params, context);
4332
4627
  if (!result.success) {
4333
4628
  return { service, configured, missing, probed: true, ok: false, message: result.error?.message ?? `Probe ${probe.action} failed` };
4334
4629
  }
@@ -4558,10 +4853,16 @@ var StorageIntegration = class extends BaseIntegration {
4558
4853
  const bucket = this.bucketOf(params);
4559
4854
  const prefix = params.prefix ?? "";
4560
4855
  const maxKeys = params.maxKeys ?? 1e3;
4856
+ const continuationToken = params.continuationToken;
4561
4857
  this.logger.debug("Storage LIST", { bucket, prefix, maxKeys });
4562
4858
  if (this.s3) {
4563
4859
  const response = await this.s3.send(
4564
- new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix || void 0, MaxKeys: maxKeys })
4860
+ new ListObjectsV2Command({
4861
+ Bucket: bucket,
4862
+ Prefix: prefix || void 0,
4863
+ MaxKeys: maxKeys,
4864
+ ContinuationToken: continuationToken
4865
+ })
4565
4866
  );
4566
4867
  return {
4567
4868
  keys: (response.Contents ?? []).map((entry) => ({
@@ -4569,9 +4870,8 @@ var StorageIntegration = class extends BaseIntegration {
4569
4870
  size: entry.Size ?? 0,
4570
4871
  lastModified: entry.LastModified?.getTime() ?? 0
4571
4872
  })),
4572
- // Continuation tokens are unrepresentable in the result type (ledger
4573
- // I-11) surface the clamp honestly.
4574
- truncated: Boolean(response.IsTruncated)
4873
+ truncated: Boolean(response.IsTruncated),
4874
+ ...response.IsTruncated && response.NextContinuationToken !== void 0 ? { nextToken: response.NextContinuationToken } : {}
4575
4875
  };
4576
4876
  }
4577
4877
  const bucketPrefix = `${bucket}/`;
@@ -4586,7 +4886,14 @@ var StorageIntegration = class extends BaseIntegration {
4586
4886
  });
4587
4887
  }
4588
4888
  results.sort((a, b) => a.key.localeCompare(b.key));
4589
- return { keys: results.slice(0, maxKeys), 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;
4892
+ return {
4893
+ keys: page,
4894
+ truncated,
4895
+ ...truncated ? { nextToken: String(offset + maxKeys) } : {}
4896
+ };
4590
4897
  }
4591
4898
  async deleteObject(params) {
4592
4899
  const bucket = this.bucketOf(params);
@@ -5379,6 +5686,6 @@ var ArxivIntegration = class extends BaseIntegration {
5379
5686
  };
5380
5687
  registerIntegration("arxiv", ArxivIntegration);
5381
5688
 
5382
- export { AccountingIntegration, ArxivIntegration, BankingIntegration, BaseIntegration, CLIIntegration, CREDENTIAL_ENTITY_TYPE, CREDENTIAL_MASTER_KEY_ENV, CalendarIntegration, ConsoleLogger, CredentialStore, CredentialsIntegration, DatabaseIntegration, DeepAgentIntegration, DockerIntegration, DriveIntegration, EmailIntegration, EsignIntegration, GitHubIntegration, IconifyIntegration, IntegrationError, IntegrationFactory, LLMIntegration, MLIntegration, MetaAdsIntegration, OAuthIntegration, OtelIntegration, PushIntegration, QueueIntegration, RedisIntegration, StorageIntegration, StripeIntegration, TwilioIntegration, WebhookIntegration, WikimediaIntegration, YouTubeIntegration, assertReadOnlySelect, getActiveFactory, getInstalledCredentialStore, getIntegration, getIntegrationFactory, getRegisteredIntegrations, googleCalendarHookProvider, installActiveFactory, installCredentialStore, isKnownIntegration, parseCalendarPushNotification, registerIntegration, resetIntegrationFactory, resolveCredentialRef, uninstallCredentialStore, validateParams, verifyAndParseStripeEvent, withRetry };
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 };
5383
5690
  //# sourceMappingURL=index.js.map
5384
5691
  //# sourceMappingURL=index.js.map