@almadar/integrations 2.24.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseIntegration-MA-b4fh8.d.ts → BaseIntegration-C_5q54DM.d.ts} +64 -3
- package/dist/index.d.ts +377 -22
- package/dist/index.js +2091 -164
- package/dist/index.js.map +1 -1
- package/dist/integrations/github/index.d.ts +1 -1
- package/dist/integrations/github/index.js +11 -0
- package/dist/integrations/github/index.js.map +1 -1
- package/dist/mocks/index.d.ts +1 -2
- package/dist/mocks/index.js +51 -16
- package/dist/mocks/index.js.map +1 -1
- package/dist/runtime/index.d.ts +35 -6
- package/dist/runtime/index.js +1877 -135
- package/dist/runtime/index.js.map +1 -1
- package/dist/{contracts-Dv9PM_Cz.d.ts → store-tehIm2rt.d.ts} +541 -6
- package/package.json +12 -7
- package/dist/factory-DTdVeyAi.d.ts +0 -41
package/dist/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
|
|
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
|
-
|
|
241
|
-
|
|
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(
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
|
981
|
-
|
|
982
|
-
|
|
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
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
|
|
992
|
-
|
|
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("
|
|
997
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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 "
|
|
1024
|
-
data = await this.executeWithRetry(() => this.
|
|
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
|
|
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
|
|
1039
|
-
const
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
"X-Almadar-Event": event
|
|
1048
|
-
};
|
|
1049
|
-
const signingSecret = secret || this.signingSecret;
|
|
1050
|
-
if (signingSecret) {
|
|
1051
|
-
headers["X-Almadar-Signature"] = "sha256=" + createHmac("sha256", signingSecret).update(body).digest("hex");
|
|
2482
|
+
if (response.status >= 500) {
|
|
2483
|
+
throw new Error(`DocuSign returned ${response.status}`);
|
|
1052
2484
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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/
|
|
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
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
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
|
-
|
|
4740
|
+
bucketOf(params) {
|
|
4741
|
+
return params.bucket || this.defaultBucket;
|
|
4742
|
+
}
|
|
2901
4743
|
compositeKey(bucket, key) {
|
|
2902
4744
|
return `${bucket}/${key}`;
|
|
2903
4745
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
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
|
-
|
|
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
|
|
2926
|
-
const key = params
|
|
2927
|
-
const
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|