wizards 0.1.9
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/LICENSE +110 -0
- package/README.md +170 -0
- package/apps/runtime/dist/agents/architect.js +453 -0
- package/apps/runtime/dist/agents/project-assistant.js +354 -0
- package/apps/runtime/dist/agents/subscription.js +206 -0
- package/apps/runtime/dist/agents/thought.js +21 -0
- package/apps/runtime/dist/agents/website.js +197 -0
- package/apps/runtime/dist/app.js +153 -0
- package/apps/runtime/dist/auth/account.js +121 -0
- package/apps/runtime/dist/auth/index.js +329 -0
- package/apps/runtime/dist/auth/keys.js +55 -0
- package/apps/runtime/dist/auth/local-ticket.js +23 -0
- package/apps/runtime/dist/authoring/example.js +73 -0
- package/apps/runtime/dist/authoring/guide.js +259 -0
- package/apps/runtime/dist/authoring/ops.js +117 -0
- package/apps/runtime/dist/cli/autostart.js +134 -0
- package/apps/runtime/dist/cli/clients.js +44 -0
- package/apps/runtime/dist/cli/environment.js +81 -0
- package/apps/runtime/dist/cli/exec.js +45 -0
- package/apps/runtime/dist/cli/home.js +74 -0
- package/apps/runtime/dist/cli/machine.js +145 -0
- package/apps/runtime/dist/cli/main.js +125 -0
- package/apps/runtime/dist/cli/setup.js +273 -0
- package/apps/runtime/dist/cli/start.js +190 -0
- package/apps/runtime/dist/cli/status.js +120 -0
- package/apps/runtime/dist/cli/ui.js +12 -0
- package/apps/runtime/dist/cli/update.js +58 -0
- package/apps/runtime/dist/connectors/builtin.js +70 -0
- package/apps/runtime/dist/connectors/external.js +400 -0
- package/apps/runtime/dist/connectors/gmail.js +107 -0
- package/apps/runtime/dist/connectors/imap.js +217 -0
- package/apps/runtime/dist/connectors/index.js +266 -0
- package/apps/runtime/dist/connectors/mail.js +72 -0
- package/apps/runtime/dist/connectors/outlook.js +90 -0
- package/apps/runtime/dist/credits/credits.js +103 -0
- package/apps/runtime/dist/credits/estimate.js +132 -0
- package/apps/runtime/dist/db/adopt.js +149 -0
- package/apps/runtime/dist/db/client.js +146 -0
- package/apps/runtime/dist/db/control-schema.js +70 -0
- package/apps/runtime/dist/db/migrations/control/0000_last_absorbing_man.sql +47 -0
- package/apps/runtime/dist/db/migrations/control/0001_gorgeous_nicolaos.sql +41 -0
- package/apps/runtime/dist/db/migrations/control/0002_secret_penance.sql +1 -0
- package/apps/runtime/dist/db/migrations/control/0003_nasty_mordo.sql +3 -0
- package/apps/runtime/dist/db/migrations/control/0004_breezy_eddie_brock.sql +6 -0
- package/apps/runtime/dist/db/migrations/control/meta/0000_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0001_snapshot.json +599 -0
- package/apps/runtime/dist/db/migrations/control/meta/0002_snapshot.json +607 -0
- package/apps/runtime/dist/db/migrations/control/meta/0003_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0004_snapshot.json +350 -0
- package/apps/runtime/dist/db/migrations/control/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/migrations/tenant/0000_eminent_freak.sql +188 -0
- package/apps/runtime/dist/db/migrations/tenant/0001_salty_master_chief.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0002_cloudy_firebird.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0003_brown_warbound.sql +52 -0
- package/apps/runtime/dist/db/migrations/tenant/0004_romantic_nocturne.sql +6 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0000_snapshot.json +1323 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0001_snapshot.json +1330 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0002_snapshot.json +1337 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0003_snapshot.json +1605 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0004_snapshot.json +1645 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/schema.js +278 -0
- package/apps/runtime/dist/documents/parse.js +230 -0
- package/apps/runtime/dist/engenty/connections-external/build-connector.js +193 -0
- package/apps/runtime/dist/engenty/connections-external/errors.js +11 -0
- package/apps/runtime/dist/engenty/connections-external/import-service.js +270 -0
- package/apps/runtime/dist/engenty/connections-external/importer/classify.js +34 -0
- package/apps/runtime/dist/engenty/connections-external/importer/map-auth.js +185 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-mcp.js +63 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-openapi.js +130 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/http-invoker.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/mcp-client.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/net/guarded-fetch.js +119 -0
- package/apps/runtime/dist/engenty/connections-external/oauth-dcr.js +88 -0
- package/apps/runtime/dist/engenty/connections-external/registry-client.js +336 -0
- package/apps/runtime/dist/engenty/connections-external/registry-source.js +82 -0
- package/apps/runtime/dist/engenty/connections-external/types.js +7 -0
- package/apps/runtime/dist/engenty/connections-github/connector.js +209 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +359 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/contacts.js +94 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/drive.js +290 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/gmail.js +567 -0
- package/apps/runtime/dist/engenty/connections-google/definitions.js +11 -0
- package/apps/runtime/dist/engenty/connections-google/shared.js +73 -0
- package/apps/runtime/dist/engenty/connections-hubspot/connector.js +140 -0
- package/apps/runtime/dist/engenty/connections-microsoft/action.js +17 -0
- package/apps/runtime/dist/engenty/connections-microsoft/graph.js +114 -0
- package/apps/runtime/dist/engenty/connections-microsoft/onedrive.js +286 -0
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +464 -0
- package/apps/runtime/dist/engenty/connections-s3/s3.js +328 -0
- package/apps/runtime/dist/engenty/connections-sdk/files-capability.js +86 -0
- package/apps/runtime/dist/engenty/connections-sdk/oauth2.js +150 -0
- package/apps/runtime/dist/engenty/connections-sdk/registry.js +89 -0
- package/apps/runtime/dist/engenty/connections-sdk/storage-capability.js +84 -0
- package/apps/runtime/dist/engenty/connections-sdk/types.js +30 -0
- package/apps/runtime/dist/engenty/connections-slack/connector.js +360 -0
- package/apps/runtime/dist/engenty/csv-import/csv-matrix.js +178 -0
- package/apps/runtime/dist/engenty/csv-import/import-sources.js +87 -0
- package/apps/runtime/dist/engenty/csv-import/parse-csv.js +129 -0
- package/apps/runtime/dist/engenty/csv-import/serialize-csv.js +20 -0
- package/apps/runtime/dist/engenty/csv-import/types.js +1 -0
- package/apps/runtime/dist/engenty/csv-import/xlsx-workbook.js +159 -0
- package/apps/runtime/dist/engenty/doc-converter/interface.js +9 -0
- package/apps/runtime/dist/engenty/doc-converter/page-break.js +92 -0
- package/apps/runtime/dist/engenty/doc-converter/providers/local/index.js +72 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/classifier.js +30 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/index.js +4 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/invoice.js +73 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/receipt.js +63 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/shared.js +40 -0
- package/apps/runtime/dist/engenty/search-index/chunks.js +164 -0
- package/apps/runtime/dist/engenty/search-index/contracts.js +9 -0
- package/apps/runtime/dist/engenty/shims/connections-sdk.js +39 -0
- package/apps/runtime/dist/engenty/shims/plugin-sdk.js +1 -0
- package/apps/runtime/dist/engenty/shims/web-ingest.js +3 -0
- package/apps/runtime/dist/engenty/web-ingest/ssrf.js +277 -0
- package/apps/runtime/dist/engine/asks.js +60 -0
- package/apps/runtime/dist/engine/events.js +46 -0
- package/apps/runtime/dist/engine/input.js +170 -0
- package/apps/runtime/dist/engine/prepare.js +51 -0
- package/apps/runtime/dist/engine/requirements.js +124 -0
- package/apps/runtime/dist/engine/resources.js +89 -0
- package/apps/runtime/dist/engine/runner.js +560 -0
- package/apps/runtime/dist/engine/steps.js +598 -0
- package/apps/runtime/dist/engine/template.js +183 -0
- package/apps/runtime/dist/engine/types.js +3 -0
- package/apps/runtime/dist/env.js +224 -0
- package/apps/runtime/dist/files/blobs.js +22 -0
- package/apps/runtime/dist/files/objects.js +86 -0
- package/apps/runtime/dist/files/storage.js +95 -0
- package/apps/runtime/dist/geocode.js +58 -0
- package/apps/runtime/dist/harness/claude.js +138 -0
- package/apps/runtime/dist/harness/codex.js +228 -0
- package/apps/runtime/dist/harness/cursor.js +93 -0
- package/apps/runtime/dist/harness/env.js +124 -0
- package/apps/runtime/dist/harness/gemini.js +126 -0
- package/apps/runtime/dist/harness/index.js +99 -0
- package/apps/runtime/dist/harness/model.js +267 -0
- package/apps/runtime/dist/harness/prompt.js +121 -0
- package/apps/runtime/dist/harness/terminal.js +193 -0
- package/apps/runtime/dist/harness/types.js +7 -0
- package/apps/runtime/dist/index.js +76 -0
- package/apps/runtime/dist/limits.js +57 -0
- package/apps/runtime/dist/link-preview.js +59 -0
- package/apps/runtime/dist/manage.js +151 -0
- package/apps/runtime/dist/mcp/auth.js +102 -0
- package/apps/runtime/dist/mcp/bridge.js +59 -0
- package/apps/runtime/dist/mcp/handler.js +42 -0
- package/apps/runtime/dist/mcp/scopes.js +5 -0
- package/apps/runtime/dist/mcp/tools.js +401 -0
- package/apps/runtime/dist/media/ffmpeg.js +121 -0
- package/apps/runtime/dist/media/generate.js +120 -0
- package/apps/runtime/dist/media/marking.js +121 -0
- package/apps/runtime/dist/media/transcribe.js +30 -0
- package/apps/runtime/dist/model-errors.js +3 -0
- package/apps/runtime/dist/models.js +489 -0
- package/apps/runtime/dist/plugin.js +61 -0
- package/apps/runtime/dist/render/chromium.js +85 -0
- package/apps/runtime/dist/render/convert.js +161 -0
- package/apps/runtime/dist/render/downloads.js +277 -0
- package/apps/runtime/dist/render/guard.js +25 -0
- package/apps/runtime/dist/routes/api.js +74 -0
- package/apps/runtime/dist/routes/delivery.js +52 -0
- package/apps/runtime/dist/routes/internal.js +49 -0
- package/apps/runtime/dist/routes/marketplace.js +36 -0
- package/apps/runtime/dist/routes/runs.js +805 -0
- package/apps/runtime/dist/routes/studio.js +702 -0
- package/apps/runtime/dist/routes/wizard-stream.js +30 -0
- package/apps/runtime/dist/running.js +39 -0
- package/apps/runtime/dist/sandbox/agentos.js +191 -0
- package/apps/runtime/dist/sandbox/docker.js +77 -0
- package/apps/runtime/dist/sandbox/index.js +30 -0
- package/apps/runtime/dist/sandbox/types.js +1 -0
- package/apps/runtime/dist/secrets/crypto.js +28 -0
- package/apps/runtime/dist/secrets/signing.js +34 -0
- package/apps/runtime/dist/secrets/vault.js +59 -0
- package/apps/runtime/dist/services/architect.js +92 -0
- package/apps/runtime/dist/services/brand.js +20 -0
- package/apps/runtime/dist/services/cloud.js +52 -0
- package/apps/runtime/dist/services/draft-events.js +27 -0
- package/apps/runtime/dist/services/errors.js +24 -0
- package/apps/runtime/dist/services/files.js +129 -0
- package/apps/runtime/dist/services/links.js +16 -0
- package/apps/runtime/dist/services/marketplace.js +292 -0
- package/apps/runtime/dist/services/package.js +202 -0
- package/apps/runtime/dist/services/profile.js +23 -0
- package/apps/runtime/dist/services/project-files.js +266 -0
- package/apps/runtime/dist/services/project-index.js +109 -0
- package/apps/runtime/dist/services/projects.js +181 -0
- package/apps/runtime/dist/services/runs.js +224 -0
- package/apps/runtime/dist/services/shares.js +129 -0
- package/apps/runtime/dist/services/widgets.js +34 -0
- package/apps/runtime/dist/services/wizards.js +342 -0
- package/apps/runtime/dist/settings.js +16 -0
- package/apps/runtime/dist/store/index.js +279 -0
- package/apps/runtime/dist/tenants/control.js +106 -0
- package/apps/runtime/dist/tenants/tenant.js +26 -0
- package/apps/runtime/dist/tools/browser.js +380 -0
- package/apps/runtime/dist/tools/connector.js +204 -0
- package/apps/runtime/dist/tools/index.js +234 -0
- package/apps/runtime/dist/tools/mail.js +104 -0
- package/apps/runtime/dist/tools/net-guard.js +59 -0
- package/apps/runtime/dist/tools/project.js +53 -0
- package/apps/runtime/dist/tools/shared.js +42 -0
- package/apps/runtime/dist/tools/store.js +238 -0
- package/apps/runtime/dist/update.js +118 -0
- package/apps/runtime/dist/widgets/bundle.js +99 -0
- package/apps/runtime/dist/widgets/render.js +312 -0
- package/apps/runtime/dist/widgets/runtime.js +84 -0
- package/apps/runtime/dist/widgets/script.js +30 -0
- package/apps/runtime/dist/widgets/step.js +192 -0
- package/apps/web/dist/ai-labels/ai-black.svg +20 -0
- package/apps/web/dist/ai-labels/ai-generated-black.svg +31 -0
- package/apps/web/dist/ai-labels/ai-generated-white.svg +32 -0
- package/apps/web/dist/ai-labels/ai-modified-black.svg +30 -0
- package/apps/web/dist/ai-labels/ai-modified-white.svg +31 -0
- package/apps/web/dist/ai-labels/ai-white.svg +21 -0
- package/apps/web/dist/assets/ChangelogDialog-O00vEnbm.js +1 -0
- package/apps/web/dist/assets/ChatPanel-CH9ry0Hp.js +1 -0
- package/apps/web/dist/assets/CreditsDialog-DRzecwUw.js +1 -0
- package/apps/web/dist/assets/EditorPage-BRnBf02Q.js +2 -0
- package/apps/web/dist/assets/FlowPreview-DDOj8IAH.js +1 -0
- package/apps/web/dist/assets/HomePage-CAGTTG3N.js +1 -0
- package/apps/web/dist/assets/ImportWizard-0RRrAnX3.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-BrP-ENHg.css +1 -0
- package/apps/web/dist/assets/LocalRuntime-C-TBYrzs.js +16 -0
- package/apps/web/dist/assets/NewWizardPage-kI90h3_9.js +2 -0
- package/apps/web/dist/assets/PublicRunner-CKRDeIVd.js +1 -0
- package/apps/web/dist/assets/RunnerView-Rc4H8wYH.js +2 -0
- package/apps/web/dist/assets/SettingsPage-CESMuZxq.js +2 -0
- package/apps/web/dist/assets/SetupPage-D1rrYU14.js +1 -0
- package/apps/web/dist/assets/SharePage-COTAmOgh.js +1 -0
- package/apps/web/dist/assets/api-BY28cjGY.js +10 -0
- package/apps/web/dist/assets/arrow-left-UpjOBfQp.js +1 -0
- package/apps/web/dist/assets/arrow-right-CucQycEy.js +1 -0
- package/apps/web/dist/assets/changelog-p3wsaDDS.js +1 -0
- package/apps/web/dist/assets/check-Bjx41xbB.js +1 -0
- package/apps/web/dist/assets/createLucideIcon-DBu2Ovxj.js +1 -0
- package/apps/web/dist/assets/definition-cy7uvY5L.js +55 -0
- package/apps/web/dist/assets/engenty-CWIFpgKq.js +1 -0
- package/apps/web/dist/assets/esm-D1OEhjTK.js +7 -0
- package/apps/web/dist/assets/external-link-CRg6DVdO.js +1 -0
- package/apps/web/dist/assets/fluffy-engenty-CJ3wCQgT.js +456 -0
- package/apps/web/dist/assets/index-CQ_q9iMO.css +1 -0
- package/apps/web/dist/assets/index-D0gxh90t.js +10 -0
- package/apps/web/dist/assets/jsx-runtime-Dk72oS4N.js +1 -0
- package/apps/web/dist/assets/meta-BbSdtTUa.js +1 -0
- package/apps/web/dist/assets/mic-CeqQ89Dj.js +1 -0
- package/apps/web/dist/assets/oss-credits-cPuZUFHD.js +1 -0
- package/apps/web/dist/assets/outputs-D6pxH6ve.js +69 -0
- package/apps/web/dist/assets/plus-HA210wXA.js +1 -0
- package/apps/web/dist/assets/preload-helper-CNEI7N0N.js +1 -0
- package/apps/web/dist/assets/qr-scanner-worker.min-CJaXo3yu.js +98 -0
- package/apps/web/dist/assets/qr-scanner.min-C_UI0gH-.js +1 -0
- package/apps/web/dist/assets/type-BzYgVWKL.js +1 -0
- package/apps/web/dist/assets/ui-Ddwl35A6.js +1 -0
- package/apps/web/dist/assets/upload-DsgLfQ3U.js +1 -0
- package/apps/web/dist/assets/video-Dk1fuTDl.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-Bf97fs8T.js +1 -0
- package/apps/web/dist/embed.js +102 -0
- package/apps/web/dist/favicon.svg +1 -0
- package/apps/web/dist/icons/apple-touch-icon.png +0 -0
- package/apps/web/dist/icons/icon-192.png +0 -0
- package/apps/web/dist/icons/icon-512.png +0 -0
- package/apps/web/dist/icons/icon-maskable-512.png +0 -0
- package/apps/web/dist/index.html +46 -0
- package/apps/web/dist/install.sh +532 -0
- package/apps/web/dist/manifest.webmanifest +20 -0
- package/apps/web/dist/sw.js +15 -0
- package/bin/wizards.mjs +13 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.d.ts +77 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.js +2 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1415 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.js +762 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.d.ts +5 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.js +132 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.d.ts +11 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.js +209 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +532 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.js +216 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.d.ts +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.d.ts +14 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.js +166 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.d.ts +33 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.js +99 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +35 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +86 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.d.ts +20 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +133 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +298 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.js +211 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.d.ts +7 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.js +40 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +74 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.js +96 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +219 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +1 -0
- package/node_modules/@engenty-wizards/shared/dist/store.d.ts +100 -0
- package/node_modules/@engenty-wizards/shared/dist/store.js +44 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.d.ts +16 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.js +51 -0
- package/node_modules/@engenty-wizards/shared/package.json +10 -0
- package/package.json +62 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/commands/wizard.md +8 -0
- package/plugin/skills/build-wizard/SKILL.md +27 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { defineConnector, } from "../shims/connections-sdk.js";
|
|
2
|
+
import { AwsClient } from "aws4fetch";
|
|
3
|
+
import { XMLParser } from "fast-xml-parser";
|
|
4
|
+
const PRESIGN_TTL_SECONDS = 300;
|
|
5
|
+
const xml = new XMLParser({ ignoreAttributes: true });
|
|
6
|
+
function parseCredentials(accessToken) {
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = JSON.parse(accessToken);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error("s3_credentials_invalid: stored credentials are not JSON");
|
|
13
|
+
}
|
|
14
|
+
if (!(parsed.access_key_id &&
|
|
15
|
+
parsed.secret_access_key &&
|
|
16
|
+
parsed.region &&
|
|
17
|
+
parsed.bucket)) {
|
|
18
|
+
throw new Error("s3_credentials_invalid: missing required fields");
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
function normalizedPrefix(creds) {
|
|
23
|
+
const prefix = (creds.prefix ?? "").replace(/^\/+/, "");
|
|
24
|
+
if (!prefix) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
return prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
28
|
+
}
|
|
29
|
+
function bucketUrl(creds) {
|
|
30
|
+
const endpoint = creds.endpoint?.replace(/\/+$/, "") ??
|
|
31
|
+
`https://s3.${creds.region}.amazonaws.com`;
|
|
32
|
+
return `${endpoint}/${creds.bucket}`;
|
|
33
|
+
}
|
|
34
|
+
function clientFor(creds) {
|
|
35
|
+
return new AwsClient({
|
|
36
|
+
accessKeyId: creds.access_key_id,
|
|
37
|
+
region: creds.region,
|
|
38
|
+
secretAccessKey: creds.secret_access_key,
|
|
39
|
+
service: "s3",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async function s3Fetch(ctx, creds, url, init) {
|
|
43
|
+
const client = clientFor(creds);
|
|
44
|
+
const signed = await client.sign(url, {
|
|
45
|
+
method: init?.method ?? "GET",
|
|
46
|
+
...(init?.body === undefined ? {} : { body: init.body }),
|
|
47
|
+
...(init?.headers ? { headers: init.headers } : {}),
|
|
48
|
+
});
|
|
49
|
+
const res = await ctx.fetchImpl(signed);
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const body = (await res.text()).slice(0, 400);
|
|
52
|
+
throw new Error(`s3_api_error (${res.status}): ${body}`);
|
|
53
|
+
}
|
|
54
|
+
return res;
|
|
55
|
+
}
|
|
56
|
+
/** Bucket URL for one object key, each path segment encoded. */
|
|
57
|
+
function objectUrl(creds, key) {
|
|
58
|
+
return `${bucketUrl(creds)}/${key
|
|
59
|
+
.split("/")
|
|
60
|
+
.map(encodeURIComponent)
|
|
61
|
+
.join("/")}`;
|
|
62
|
+
}
|
|
63
|
+
/** Compose a connection-relative file ref from a folder ref + file name. */
|
|
64
|
+
export function composeFileRef(folderRef, name) {
|
|
65
|
+
const folder = (folderRef ?? "").replace(/^\/+/, "");
|
|
66
|
+
if (!folder) {
|
|
67
|
+
return name;
|
|
68
|
+
}
|
|
69
|
+
return folder.endsWith("/") ? `${folder}${name}` : `${folder}/${name}`;
|
|
70
|
+
}
|
|
71
|
+
function asArray(value) {
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return Array.isArray(value) ? value : [value];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse a ListObjectsV2 response into normalized entries. `root` is the
|
|
79
|
+
* connection's configured prefix; refs are keys relative to it (folders carry a
|
|
80
|
+
* trailing slash so they round-trip as prefixes).
|
|
81
|
+
*/
|
|
82
|
+
export function parseListResponse(body, root) {
|
|
83
|
+
const doc = xml.parse(body);
|
|
84
|
+
const result = doc.ListBucketResult ?? {};
|
|
85
|
+
const entries = [];
|
|
86
|
+
for (const cp of asArray(result.CommonPrefixes)) {
|
|
87
|
+
const full = cp.Prefix ?? "";
|
|
88
|
+
if (!full.startsWith(root)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const rel = full.slice(root.length);
|
|
92
|
+
entries.push({
|
|
93
|
+
kind: "folder",
|
|
94
|
+
mime_type: null,
|
|
95
|
+
modified_at: null,
|
|
96
|
+
name: rel.replace(/\/$/, "").split("/").pop() ?? rel,
|
|
97
|
+
ref: rel,
|
|
98
|
+
size: null,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
for (const obj of asArray(result.Contents)) {
|
|
102
|
+
const full = obj.Key ?? "";
|
|
103
|
+
if (!full.startsWith(root)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const rel = full.slice(root.length);
|
|
107
|
+
// The prefix marker object of the folder itself has an empty relative key.
|
|
108
|
+
if (!rel || rel.endsWith("/")) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
entries.push({
|
|
112
|
+
kind: "file",
|
|
113
|
+
mime_type: null,
|
|
114
|
+
modified_at: obj.LastModified ?? null,
|
|
115
|
+
name: rel.split("/").pop() ?? rel,
|
|
116
|
+
ref: rel,
|
|
117
|
+
size: obj.Size === undefined ? null : Number(obj.Size),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
entries,
|
|
122
|
+
next_cursor: result.NextContinuationToken ?? null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Verify credentials with a 1-key list and resolve the account label. */
|
|
126
|
+
export async function verifyS3Credentials(raw, fetchImpl) {
|
|
127
|
+
const creds = parseCredentials(JSON.stringify(raw));
|
|
128
|
+
const root = normalizedPrefix(creds);
|
|
129
|
+
const url = new URL(bucketUrl(creds));
|
|
130
|
+
url.searchParams.set("list-type", "2");
|
|
131
|
+
url.searchParams.set("max-keys", "1");
|
|
132
|
+
if (root) {
|
|
133
|
+
url.searchParams.set("prefix", root);
|
|
134
|
+
}
|
|
135
|
+
const signed = await clientFor(creds).sign(url.toString(), {
|
|
136
|
+
method: "GET",
|
|
137
|
+
});
|
|
138
|
+
const res = await fetchImpl(signed);
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const body = (await res.text()).slice(0, 300);
|
|
141
|
+
throw new Error(`s3_verify_failed (${res.status}): ${body}`);
|
|
142
|
+
}
|
|
143
|
+
const label = root
|
|
144
|
+
? `${creds.bucket}/${root.replace(/\/$/, "")}`
|
|
145
|
+
: creds.bucket;
|
|
146
|
+
return { externalId: label, label };
|
|
147
|
+
}
|
|
148
|
+
export const s3Connector = defineConnector({
|
|
149
|
+
actions: [],
|
|
150
|
+
auth: {
|
|
151
|
+
kind: "api_key",
|
|
152
|
+
apiKey: {
|
|
153
|
+
fields: [
|
|
154
|
+
{ key: "access_key_id", label: "Access key ID" },
|
|
155
|
+
{ key: "secret_access_key", label: "Secret access key", secret: true },
|
|
156
|
+
{ key: "region", label: "Region", placeholder: "eu-central-1" },
|
|
157
|
+
{ key: "bucket", label: "Bucket" },
|
|
158
|
+
{
|
|
159
|
+
key: "endpoint",
|
|
160
|
+
label: "Endpoint",
|
|
161
|
+
placeholder: "https://… (only for R2/MinIO)",
|
|
162
|
+
required: false,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
key: "prefix",
|
|
166
|
+
label: "Key prefix",
|
|
167
|
+
placeholder: "optional root folder",
|
|
168
|
+
required: false,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
verify: verifyS3Credentials,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
description: "Browse, read, and write objects in an S3-compatible bucket (AWS S3, Cloudflare R2, MinIO). Writes require approval by default.",
|
|
175
|
+
files: {
|
|
176
|
+
rootLabel: (connection) => connection.external_account ?? "S3 bucket",
|
|
177
|
+
async list(ctx, input) {
|
|
178
|
+
const creds = parseCredentials(ctx.accessToken);
|
|
179
|
+
const root = normalizedPrefix(creds);
|
|
180
|
+
const folder = input.folder_ref ?? "";
|
|
181
|
+
const url = new URL(bucketUrl(creds));
|
|
182
|
+
url.searchParams.set("list-type", "2");
|
|
183
|
+
url.searchParams.set("delimiter", "/");
|
|
184
|
+
url.searchParams.set("prefix", `${root}${folder}`);
|
|
185
|
+
url.searchParams.set("max-keys", String(input.limit ?? 100));
|
|
186
|
+
if (input.cursor) {
|
|
187
|
+
url.searchParams.set("continuation-token", input.cursor);
|
|
188
|
+
}
|
|
189
|
+
const res = await s3Fetch(ctx, creds, url.toString());
|
|
190
|
+
return parseListResponse(await res.text(), root);
|
|
191
|
+
},
|
|
192
|
+
async read(ctx, input) {
|
|
193
|
+
const creds = parseCredentials(ctx.accessToken);
|
|
194
|
+
const root = normalizedPrefix(creds);
|
|
195
|
+
const key = `${root}${input.file_ref}`;
|
|
196
|
+
// HEAD first for size/mime, then presign a GET — no proxying, no cap.
|
|
197
|
+
const headUrl = `${bucketUrl(creds)}/${key
|
|
198
|
+
.split("/")
|
|
199
|
+
.map(encodeURIComponent)
|
|
200
|
+
.join("/")}`;
|
|
201
|
+
const head = await s3Fetch(ctx, creds, headUrl, { method: "HEAD" });
|
|
202
|
+
const size = Number(head.headers.get("content-length") ?? "0") || null;
|
|
203
|
+
const mime = head.headers.get("content-type");
|
|
204
|
+
const presigned = await clientFor(creds).sign(`${headUrl}?X-Amz-Expires=${PRESIGN_TTL_SECONDS}`, { aws: { signQuery: true }, method: "GET" });
|
|
205
|
+
return {
|
|
206
|
+
expires_at: new Date(Date.now() + PRESIGN_TTL_SECONDS * 1000).toISOString(),
|
|
207
|
+
kind: "url",
|
|
208
|
+
mime_type: mime,
|
|
209
|
+
name: input.file_ref.split("/").pop() ?? input.file_ref,
|
|
210
|
+
size,
|
|
211
|
+
url: presigned.url,
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
async search(ctx, input) {
|
|
215
|
+
const creds = parseCredentials(ctx.accessToken);
|
|
216
|
+
const root = normalizedPrefix(creds);
|
|
217
|
+
const scope = input.folder_ref ?? "";
|
|
218
|
+
const url = new URL(bucketUrl(creds));
|
|
219
|
+
url.searchParams.set("list-type", "2");
|
|
220
|
+
url.searchParams.set("prefix", `${root}${scope}`);
|
|
221
|
+
url.searchParams.set("max-keys", "1000");
|
|
222
|
+
const res = await s3Fetch(ctx, creds, url.toString());
|
|
223
|
+
const listing = parseListResponse(await res.text(), root);
|
|
224
|
+
const q = input.query.toLowerCase();
|
|
225
|
+
const limit = input.limit ?? 100;
|
|
226
|
+
return {
|
|
227
|
+
entries: listing.entries
|
|
228
|
+
.filter((entry) => entry.name.toLowerCase().includes(q))
|
|
229
|
+
.slice(0, limit),
|
|
230
|
+
next_cursor: null,
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
async stat(ctx, input) {
|
|
234
|
+
const creds = parseCredentials(ctx.accessToken);
|
|
235
|
+
const root = normalizedPrefix(creds);
|
|
236
|
+
if (!input.ref || input.ref.endsWith("/")) {
|
|
237
|
+
return {
|
|
238
|
+
kind: "folder",
|
|
239
|
+
mime_type: null,
|
|
240
|
+
modified_at: null,
|
|
241
|
+
name: input.ref.replace(/\/$/, "").split("/").pop() ?? "",
|
|
242
|
+
ref: input.ref,
|
|
243
|
+
size: null,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const key = `${root}${input.ref}`;
|
|
247
|
+
const headUrl = `${bucketUrl(creds)}/${key
|
|
248
|
+
.split("/")
|
|
249
|
+
.map(encodeURIComponent)
|
|
250
|
+
.join("/")}`;
|
|
251
|
+
const head = await s3Fetch(ctx, creds, headUrl, { method: "HEAD" });
|
|
252
|
+
return {
|
|
253
|
+
kind: "file",
|
|
254
|
+
mime_type: head.headers.get("content-type"),
|
|
255
|
+
modified_at: head.headers.get("last-modified")
|
|
256
|
+
? new Date(head.headers.get("last-modified")).toISOString()
|
|
257
|
+
: null,
|
|
258
|
+
name: input.ref.split("/").pop() ?? input.ref,
|
|
259
|
+
ref: input.ref,
|
|
260
|
+
size: Number(head.headers.get("content-length") ?? "0") || null,
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
icon: "🪣",
|
|
265
|
+
id: "s3",
|
|
266
|
+
moduleId: "connections-s3",
|
|
267
|
+
name: "S3 Bucket",
|
|
268
|
+
storage: {
|
|
269
|
+
async write(ctx, input) {
|
|
270
|
+
const creds = parseCredentials(ctx.accessToken);
|
|
271
|
+
const root = normalizedPrefix(creds);
|
|
272
|
+
const ref = composeFileRef(input.folder_ref, input.name);
|
|
273
|
+
const bytes = typeof input.content_base64 === "string"
|
|
274
|
+
? Buffer.from(input.content_base64, "base64")
|
|
275
|
+
: Buffer.from(input.content_text ?? "", "utf8");
|
|
276
|
+
const mime = input.mime_type ?? "application/octet-stream";
|
|
277
|
+
await s3Fetch(ctx, creds, objectUrl(creds, `${root}${ref}`), {
|
|
278
|
+
body: new Uint8Array(bytes),
|
|
279
|
+
headers: { "content-type": mime },
|
|
280
|
+
method: "PUT",
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
kind: "file",
|
|
284
|
+
mime_type: mime,
|
|
285
|
+
modified_at: new Date().toISOString(),
|
|
286
|
+
name: input.name,
|
|
287
|
+
ref,
|
|
288
|
+
size: bytes.length,
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
async delete(ctx, input) {
|
|
292
|
+
const creds = parseCredentials(ctx.accessToken);
|
|
293
|
+
const root = normalizedPrefix(creds);
|
|
294
|
+
await s3Fetch(ctx, creds, objectUrl(creds, `${root}${input.ref}`), {
|
|
295
|
+
method: "DELETE",
|
|
296
|
+
});
|
|
297
|
+
return { deleted: true, ref: input.ref };
|
|
298
|
+
},
|
|
299
|
+
async move(ctx, input) {
|
|
300
|
+
const creds = parseCredentials(ctx.accessToken);
|
|
301
|
+
const root = normalizedPrefix(creds);
|
|
302
|
+
const name = input.new_name ?? (input.ref.split("/").pop() || input.ref);
|
|
303
|
+
const toRef = composeFileRef(input.to_folder_ref, name);
|
|
304
|
+
// S3 has no native move: server-side copy, then delete the source.
|
|
305
|
+
await s3Fetch(ctx, creds, objectUrl(creds, `${root}${toRef}`), {
|
|
306
|
+
headers: {
|
|
307
|
+
"x-amz-copy-source": `/${creds.bucket}/${`${root}${input.ref}`
|
|
308
|
+
.split("/")
|
|
309
|
+
.map(encodeURIComponent)
|
|
310
|
+
.join("/")}`,
|
|
311
|
+
},
|
|
312
|
+
method: "PUT",
|
|
313
|
+
});
|
|
314
|
+
await s3Fetch(ctx, creds, objectUrl(creds, `${root}${input.ref}`), {
|
|
315
|
+
method: "DELETE",
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
kind: "file",
|
|
319
|
+
mime_type: null,
|
|
320
|
+
modified_at: new Date().toISOString(),
|
|
321
|
+
name,
|
|
322
|
+
ref: toRef,
|
|
323
|
+
size: null,
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
toolPrefix: "s3",
|
|
328
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Synthesize the read actions for a connector's files capability. Each is a
|
|
4
|
+
* normal `group: "read"` action, so `registerConnectorModule` projects it as a
|
|
5
|
+
* gateway operation (`<toolPrefix>_files_list`, …) and the policy gate, audit,
|
|
6
|
+
* approvals, and agent-tool wiring all apply with no extra code. The files
|
|
7
|
+
* module also drives connected-folder mounts through the same capability.
|
|
8
|
+
*/
|
|
9
|
+
export function filesCapabilityActions(files, providerScopes) {
|
|
10
|
+
const scopes = providerScopes ?? [];
|
|
11
|
+
const fileEntryDescription = "A file or folder. `ref` is the provider id to pass back as folder_ref/file_ref/ref.";
|
|
12
|
+
const listAction = {
|
|
13
|
+
description: "List one folder's immediate children. Pass folder_ref: null for the root; page with the returned next_cursor.",
|
|
14
|
+
group: "read",
|
|
15
|
+
id: "files_list",
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
cursor: z
|
|
18
|
+
.string()
|
|
19
|
+
.nullish()
|
|
20
|
+
.describe("Opaque pagination cursor from a prior next_cursor."),
|
|
21
|
+
folder_ref: z
|
|
22
|
+
.string()
|
|
23
|
+
.nullable()
|
|
24
|
+
.describe("Folder id to list; null lists the connection's root."),
|
|
25
|
+
limit: z
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.min(1)
|
|
29
|
+
.max(200)
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Max entries (default 100)."),
|
|
32
|
+
}),
|
|
33
|
+
providerScopes: scopes,
|
|
34
|
+
summary: "List a folder",
|
|
35
|
+
handler: (input, ctx) => files.list(ctx, input),
|
|
36
|
+
};
|
|
37
|
+
const readAction = {
|
|
38
|
+
description: `Read a file's contents. ${fileEntryDescription} Large or binary files come back base64-encoded or as a short-lived URL.`,
|
|
39
|
+
group: "read",
|
|
40
|
+
id: "files_read",
|
|
41
|
+
inputSchema: z.object({
|
|
42
|
+
file_ref: z.string().describe("File id to read (an entry's ref)."),
|
|
43
|
+
max_bytes: z
|
|
44
|
+
.number()
|
|
45
|
+
.int()
|
|
46
|
+
.min(1)
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Cap for proxied bytes; ignored for URL results."),
|
|
49
|
+
}),
|
|
50
|
+
providerScopes: scopes,
|
|
51
|
+
summary: "Read a file",
|
|
52
|
+
handler: (input, ctx) => files.read(ctx, input),
|
|
53
|
+
};
|
|
54
|
+
const statAction = {
|
|
55
|
+
description: `Get metadata for a single file or folder. ${fileEntryDescription}`,
|
|
56
|
+
group: "read",
|
|
57
|
+
id: "files_stat",
|
|
58
|
+
inputSchema: z.object({
|
|
59
|
+
ref: z.string().describe("File or folder id."),
|
|
60
|
+
}),
|
|
61
|
+
providerScopes: scopes,
|
|
62
|
+
summary: "Stat a file or folder",
|
|
63
|
+
handler: (input, ctx) => files.stat(ctx, input),
|
|
64
|
+
};
|
|
65
|
+
const actions = [listAction, readAction, statAction];
|
|
66
|
+
if (files.search) {
|
|
67
|
+
const search = files.search.bind(files);
|
|
68
|
+
actions.push({
|
|
69
|
+
description: "Search files by name. Optionally scope to a folder subtree with folder_ref.",
|
|
70
|
+
group: "read",
|
|
71
|
+
id: "files_search",
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
folder_ref: z
|
|
74
|
+
.string()
|
|
75
|
+
.nullish()
|
|
76
|
+
.describe("Restrict the search to this folder subtree."),
|
|
77
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
78
|
+
query: z.string().min(1).describe("Case-insensitive name query."),
|
|
79
|
+
}),
|
|
80
|
+
providerScopes: scopes,
|
|
81
|
+
summary: "Search files by name",
|
|
82
|
+
handler: (input, ctx) => search(ctx, input),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return actions;
|
|
86
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
async function readClientEnv(key, resolveEnv) {
|
|
3
|
+
if (!key) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
const resolved = await resolveEnv?.(key);
|
|
7
|
+
return resolved ?? process.env[key];
|
|
8
|
+
}
|
|
9
|
+
/** RFC 7636 S256 PKCE pair for public (and confidential) authorization-code clients. */
|
|
10
|
+
export function createOAuth2Pkce() {
|
|
11
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
12
|
+
const codeChallenge = createHash("sha256")
|
|
13
|
+
.update(codeVerifier)
|
|
14
|
+
.digest("base64url");
|
|
15
|
+
return {
|
|
16
|
+
codeChallenge,
|
|
17
|
+
codeChallengeMethod: "S256",
|
|
18
|
+
codeVerifier,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function resolveOAuth2Env(config, resolveEnv) {
|
|
22
|
+
const clientId = await readClientEnv(config.clientIdEnv, resolveEnv);
|
|
23
|
+
const clientSecret = await readClientEnv(config.clientSecretEnv, resolveEnv);
|
|
24
|
+
if (!(clientId && clientSecret)) {
|
|
25
|
+
throw new Error(`OAuth client credentials missing: set ${config.clientIdEnv ?? "<clientIdEnv>"} and ${config.clientSecretEnv ?? "<clientSecretEnv>"}`);
|
|
26
|
+
}
|
|
27
|
+
return { clientId, clientSecret };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the OAuth client for a connector. Precedence: a DB-backed
|
|
31
|
+
* `resolveClientCredentials` (imported connectors) wins; otherwise the injected
|
|
32
|
+
* `resolveEnv` (tenant → platform settings) with a process.env fallback.
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveOAuth2Credentials(config, resolveEnv) {
|
|
35
|
+
if (config.resolveClientCredentials) {
|
|
36
|
+
const creds = await config.resolveClientCredentials();
|
|
37
|
+
if (!creds.clientId?.trim()) {
|
|
38
|
+
throw new Error("OAuth client credentials missing: no client_id");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
clientId: creds.clientId,
|
|
42
|
+
clientSecret: creds.clientSecret ?? "",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return resolveOAuth2Env(config, resolveEnv);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* True when the connector's OAuth client can start a connect flow. Env-backed
|
|
49
|
+
* connectors still need both id and secret; imported / DCR clients only need a
|
|
50
|
+
* client id (public clients omit the secret). Never throws.
|
|
51
|
+
*/
|
|
52
|
+
export async function hasOAuth2ClientCredentials(config, resolveEnv) {
|
|
53
|
+
if (config.resolveClientCredentials) {
|
|
54
|
+
try {
|
|
55
|
+
const creds = await config.resolveClientCredentials();
|
|
56
|
+
return Boolean(creds.clientId?.trim());
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const clientId = await readClientEnv(config.clientIdEnv, resolveEnv);
|
|
63
|
+
const clientSecret = await readClientEnv(config.clientSecretEnv, resolveEnv);
|
|
64
|
+
return Boolean(clientId && clientSecret);
|
|
65
|
+
}
|
|
66
|
+
export async function buildAuthorizationUrl(params) {
|
|
67
|
+
if (params.connector.auth.kind !== "oauth2") {
|
|
68
|
+
throw new Error("buildAuthorizationUrl requires an oauth2 connector");
|
|
69
|
+
}
|
|
70
|
+
const { oauth2 } = params.connector.auth;
|
|
71
|
+
const env = await resolveOAuth2Credentials(oauth2, params.resolveEnv);
|
|
72
|
+
const url = new URL(oauth2.authUrl);
|
|
73
|
+
url.searchParams.set("client_id", env.clientId);
|
|
74
|
+
url.searchParams.set("redirect_uri", params.redirectUri);
|
|
75
|
+
url.searchParams.set("response_type", "code");
|
|
76
|
+
url.searchParams.set("scope", params.scopes.join(oauth2.scopeSeparator ?? " "));
|
|
77
|
+
url.searchParams.set("state", params.state);
|
|
78
|
+
if (params.pkce) {
|
|
79
|
+
url.searchParams.set("code_challenge", params.pkce.codeChallenge);
|
|
80
|
+
url.searchParams.set("code_challenge_method", params.pkce.codeChallengeMethod);
|
|
81
|
+
}
|
|
82
|
+
for (const [key, value] of Object.entries(oauth2.extraAuthParams ?? {})) {
|
|
83
|
+
url.searchParams.set(key, value);
|
|
84
|
+
}
|
|
85
|
+
return url.toString();
|
|
86
|
+
}
|
|
87
|
+
function parseTokenResponse(data, separator) {
|
|
88
|
+
const nested = data.authed_user?.access_token ? data.authed_user : null;
|
|
89
|
+
const accessToken = nested?.access_token ?? data.access_token;
|
|
90
|
+
if (!accessToken) {
|
|
91
|
+
throw new Error(`Token endpoint returned no access token${data.error ? `: ${data.error} ${data.error_description ?? ""}` : ""}`.trim());
|
|
92
|
+
}
|
|
93
|
+
const scopeRaw = nested?.scope ?? data.scope ?? "";
|
|
94
|
+
return {
|
|
95
|
+
accessToken,
|
|
96
|
+
expiresAt: data.expires_in
|
|
97
|
+
? new Date(Date.now() + data.expires_in * 1000)
|
|
98
|
+
: null,
|
|
99
|
+
grantedScopes: scopeRaw
|
|
100
|
+
.split(separator === "," ? "," : /[\s,]+/u)
|
|
101
|
+
.map((s) => s.trim())
|
|
102
|
+
.filter(Boolean),
|
|
103
|
+
refreshToken: data.refresh_token ?? null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function postTokenEndpoint(config, body, fetchImpl, resolveEnv) {
|
|
107
|
+
const env = await resolveOAuth2Credentials(config, resolveEnv);
|
|
108
|
+
const form = {
|
|
109
|
+
client_id: env.clientId,
|
|
110
|
+
...body,
|
|
111
|
+
};
|
|
112
|
+
// Public clients omit client_secret; sending an empty value breaks some AS.
|
|
113
|
+
if (env.clientSecret) {
|
|
114
|
+
form.client_secret = env.clientSecret;
|
|
115
|
+
}
|
|
116
|
+
const response = await fetchImpl(config.tokenUrl, {
|
|
117
|
+
body: new URLSearchParams(form),
|
|
118
|
+
// Some providers (GitHub) default the token response to form-encoding and
|
|
119
|
+
// only return JSON when the request opts in via `Accept: application/json`;
|
|
120
|
+
// `tokenRequestHeaders` lets a connector add that header (parseTokenResponse
|
|
121
|
+
// always reads JSON). Connector-supplied headers cannot override content-type.
|
|
122
|
+
headers: {
|
|
123
|
+
...config.tokenRequestHeaders,
|
|
124
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
125
|
+
},
|
|
126
|
+
method: "POST",
|
|
127
|
+
});
|
|
128
|
+
const data = (await response.json());
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error(`Token endpoint failed (${response.status}): ${data.error ?? "unknown"} ${data.error_description ?? ""}`.trim());
|
|
131
|
+
}
|
|
132
|
+
return parseTokenResponse(data, config.scopeSeparator ?? " ");
|
|
133
|
+
}
|
|
134
|
+
export function exchangeAuthorizationCode(params) {
|
|
135
|
+
const body = {
|
|
136
|
+
code: params.code,
|
|
137
|
+
grant_type: "authorization_code",
|
|
138
|
+
redirect_uri: params.redirectUri,
|
|
139
|
+
};
|
|
140
|
+
if (params.codeVerifier) {
|
|
141
|
+
body.code_verifier = params.codeVerifier;
|
|
142
|
+
}
|
|
143
|
+
return postTokenEndpoint(params.config, body, params.fetchImpl ?? fetch, params.resolveEnv);
|
|
144
|
+
}
|
|
145
|
+
export function refreshAccessToken(params) {
|
|
146
|
+
return postTokenEndpoint(params.config, {
|
|
147
|
+
grant_type: "refresh_token",
|
|
148
|
+
refresh_token: params.refreshToken,
|
|
149
|
+
}, params.fetchImpl ?? fetch, params.resolveEnv);
|
|
150
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide connector registry. Connector modules register at plugin load;
|
|
3
|
+
* the connections module reads it lazily (OAuth routes, management ops, UI
|
|
4
|
+
* catalog), so registration order between modules does not matter.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: core loads every plugin through its OWN jiti instance (fresh
|
|
7
|
+
* module cache per plugin — see apps/core/src/plugins/loader.ts), so a plain
|
|
8
|
+
* module-level Map would give each module a private copy. The registry
|
|
9
|
+
* therefore lives on `globalThis` under a `Symbol.for` key, which is shared
|
|
10
|
+
* across all module-cache copies in the process.
|
|
11
|
+
*
|
|
12
|
+
* Imported (integrations.sh / MCP / OpenAPI) definitions carry `tenantId` and
|
|
13
|
+
* are stored under `${tenantId}::${id}` so one tenant's import is not listed
|
|
14
|
+
* or executed as another's. Builtins have no tenantId and use `id` as the key.
|
|
15
|
+
*/
|
|
16
|
+
const REGISTRY_KEY = Symbol.for("engenty.connections.connector-registry");
|
|
17
|
+
function registry() {
|
|
18
|
+
const host = globalThis;
|
|
19
|
+
host[REGISTRY_KEY] ??= new Map();
|
|
20
|
+
return host[REGISTRY_KEY];
|
|
21
|
+
}
|
|
22
|
+
function registryKey(def) {
|
|
23
|
+
return def.tenantId ? `${def.tenantId}::${def.id}` : def.id;
|
|
24
|
+
}
|
|
25
|
+
export function registerConnectorDefinition(def) {
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
for (const action of def.actions) {
|
|
28
|
+
if (seen.has(action.id)) {
|
|
29
|
+
throw new Error(`Connector "${def.id}" duplicates action "${action.id}"`);
|
|
30
|
+
}
|
|
31
|
+
seen.add(action.id);
|
|
32
|
+
}
|
|
33
|
+
// Idempotent on purpose: dev-reload re-runs plugin factories; the latest
|
|
34
|
+
// definition wins.
|
|
35
|
+
registry().set(registryKey(def), def);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Remove a definition (imported-connector delete). Already-registered gateway
|
|
39
|
+
* operations cannot be unregistered — they dead-end at policy resolution once
|
|
40
|
+
* the definition is gone; a restart fully clears them.
|
|
41
|
+
*/
|
|
42
|
+
export function removeConnectorDefinition(id, tenantId) {
|
|
43
|
+
registry().delete(tenantId ? `${tenantId}::${id}` : id);
|
|
44
|
+
}
|
|
45
|
+
export function getConnectorDefinition(id, tenantId) {
|
|
46
|
+
const map = registry();
|
|
47
|
+
if (tenantId) {
|
|
48
|
+
const scoped = map.get(`${tenantId}::${id}`);
|
|
49
|
+
if (scoped) {
|
|
50
|
+
return scoped;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const builtin = map.get(id);
|
|
54
|
+
if (builtin && !builtin.tenantId) {
|
|
55
|
+
return builtin;
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
/** Builtins plus this tenant's imports. Omit tenantId to list builtins only. */
|
|
60
|
+
export function listConnectorDefinitions(tenantId) {
|
|
61
|
+
return [...registry().values()]
|
|
62
|
+
.filter((def) => !def.tenantId || def.tenantId === tenantId)
|
|
63
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
64
|
+
}
|
|
65
|
+
function connectorsForResolve(tenantId) {
|
|
66
|
+
if (!tenantId) {
|
|
67
|
+
return [...registry().values()].filter((def) => !def.tenantId);
|
|
68
|
+
}
|
|
69
|
+
return listConnectorDefinitions(tenantId);
|
|
70
|
+
}
|
|
71
|
+
/** Find the connector + action that own a projected operation id. */
|
|
72
|
+
export function resolveConnectorOperation(operationId, tenantId) {
|
|
73
|
+
for (const connector of connectorsForResolve(tenantId)) {
|
|
74
|
+
const prefix = `${connector.toolPrefix}_`;
|
|
75
|
+
if (!operationId.startsWith(prefix)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const actionId = operationId.slice(prefix.length);
|
|
79
|
+
const action = connector.actions.find((a) => a.id === actionId);
|
|
80
|
+
if (action) {
|
|
81
|
+
return { action, connector };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/** Test-only: reset the registry between vitest cases. */
|
|
87
|
+
export function __resetConnectorRegistryForTests() {
|
|
88
|
+
registry().clear();
|
|
89
|
+
}
|