@oomol-lab/open-connector 1.7.0 → 1.8.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/README.md +4 -0
- package/assets/open-connector/catalog/apps/airtable.json +134 -0
- package/assets/open-connector/catalog/apps/aisa.json +916 -76
- package/assets/open-connector/catalog/apps/cal.json +24 -6
- package/assets/open-connector/catalog/apps/discordbot.json +2 -2
- package/assets/open-connector/catalog/apps/enormail.json +411 -0
- package/assets/open-connector/catalog/apps/feishu_app_bot.json +158 -0
- package/assets/open-connector/catalog/apps/github.json +275 -0
- package/assets/open-connector/catalog/apps/gitlab.json +125 -0
- package/assets/open-connector/catalog/apps/gmail.json +151 -0
- package/assets/open-connector/catalog/apps/googlecalendar.json +206 -0
- package/assets/open-connector/catalog/apps/googledrive.json +479 -0
- package/assets/open-connector/catalog/apps/googlesheets.json +144 -0
- package/assets/open-connector/catalog/apps/hithink_finance.json +6 -3
- package/assets/open-connector/catalog/apps/indexed.json +1115 -0
- package/assets/open-connector/catalog/apps/linear.json +116 -0
- package/assets/open-connector/catalog/apps/notion.json +142 -0
- package/assets/open-connector/catalog/apps/one_drive.json +185 -0
- package/assets/open-connector/catalog/apps/outlook.json +182 -0
- package/assets/open-connector/catalog/apps/shopify.json +282 -0
- package/assets/open-connector/catalog/apps/slack.json +182 -0
- package/assets/open-connector/catalog/apps/spotify.json +46 -43
- package/assets/open-connector/catalog/apps/stripe.json +96 -0
- package/assets/open-connector/catalog/apps/telegram.json +138 -0
- package/assets/open-connector/catalog/apps/weixin_store.json +543 -2
- package/assets/open-connector/catalog/apps/woocommerce.json +127 -0
- package/assets/open-connector/catalog/apps/zendesk.json +152 -0
- package/assets/open-connector/catalog/apps/zerotier.json +9 -9
- package/assets/open-connector/catalog/apps-index.json +1 -1
- package/assets/open-connector/migrations/0014_saas_project.sql +55 -0
- package/assets/open-connector/migrations/0015_saas_cleanup_runtime.sql +8 -0
- package/assets/open-connector/migrations/0016_trigger_policy.sql +1 -0
- package/assets/open-connector/migrations/0017_trigger_subscriptions.sql +17 -0
- package/assets/open-connector/migrations/postgresql/0014_saas_project.sql +55 -0
- package/assets/open-connector/migrations/postgresql/0015_saas_cleanup_runtime.sql +8 -0
- package/assets/open-connector/migrations/postgresql/0016_trigger_policy.sql +1 -0
- package/assets/open-connector/migrations/postgresql/0017_trigger_subscriptions.sql +17 -0
- package/package.json +2 -2
- package/src/connection-service.d.ts +40 -7
- package/src/connection-service.js +60 -18
- package/src/core/action-policy.d.ts +8 -1
- package/src/core/action-policy.js +51 -0
- package/src/core/cast.d.ts +3 -0
- package/src/core/cast.js +4 -1
- package/src/core/guarded-fetch.d.ts +5 -0
- package/src/core/guarded-fetch.js +13 -4
- package/src/core/remote-http.d.ts +33 -0
- package/src/core/remote-http.js +104 -0
- package/src/core/request.d.ts +1 -0
- package/src/core/request.js +7 -0
- package/src/core/types.d.ts +4 -0
- package/src/marketplace/marketplace-service.d.ts +0 -1
- package/src/marketplace/marketplace-service.js +80 -92
- package/src/mcp.js +19 -5
- package/src/oauth/oauth-flow-service.d.ts +8 -1
- package/src/oauth/oauth-flow-service.js +10 -4
- package/src/providers/action-contracts.generated.d.ts +4 -2
- package/src/providers/airtable/executors.d.ts +3 -0
- package/src/providers/airtable/executors.js +2 -0
- package/src/providers/airtable/trigger-on-record-changed.d.ts +2 -0
- package/src/providers/airtable/trigger-on-record-changed.definition.d.ts +4 -0
- package/src/providers/airtable/trigger-on-record-changed.definition.js +54 -0
- package/src/providers/airtable/trigger-on-record-changed.js +235 -0
- package/src/providers/aisa/executors.js +196 -22
- package/src/providers/cal/actions.js +5 -1
- package/src/providers/enormail/executors.d.ts +4 -0
- package/src/providers/enormail/executors.js +97 -0
- package/src/providers/feishu_app_bot/executors.d.ts +3 -0
- package/src/providers/feishu_app_bot/executors.js +2 -0
- package/src/providers/feishu_app_bot/trigger-config.d.ts +1 -0
- package/src/providers/feishu_app_bot/trigger-config.js +10 -0
- package/src/providers/feishu_app_bot/trigger-on-event.d.ts +2 -0
- package/src/providers/feishu_app_bot/trigger-on-event.definition.d.ts +4 -0
- package/src/providers/feishu_app_bot/trigger-on-event.definition.js +63 -0
- package/src/providers/feishu_app_bot/trigger-on-event.js +29 -0
- package/src/providers/feishu_app_bot/trigger-subscriptions.d.ts +5 -0
- package/src/providers/feishu_app_bot/trigger-subscriptions.js +70 -0
- package/src/providers/github/executors.d.ts +3 -0
- package/src/providers/github/executors.js +6 -0
- package/src/providers/github/trigger-on-repo-event.d.ts +2 -0
- package/src/providers/github/trigger-on-repo-event.definition.d.ts +4 -0
- package/src/providers/github/trigger-on-repo-event.definition.js +94 -0
- package/src/providers/github/trigger-on-repo-event.js +168 -0
- package/src/providers/github/trigger-watch-pull-request.d.ts +2 -0
- package/src/providers/github/trigger-watch-pull-request.definition.d.ts +4 -0
- package/src/providers/github/trigger-watch-pull-request.definition.js +49 -0
- package/src/providers/github/trigger-watch-pull-request.js +106 -0
- package/src/providers/gitlab/executors.d.ts +3 -0
- package/src/providers/gitlab/executors.js +2 -0
- package/src/providers/gitlab/trigger-on-project-event.d.ts +2 -0
- package/src/providers/gitlab/trigger-on-project-event.definition.d.ts +5 -0
- package/src/providers/gitlab/trigger-on-project-event.definition.js +63 -0
- package/src/providers/gitlab/trigger-on-project-event.js +186 -0
- package/src/providers/gmail/executors.d.ts +3 -0
- package/src/providers/gmail/executors.js +2 -0
- package/src/providers/gmail/trigger-on-message-received.d.ts +2 -0
- package/src/providers/gmail/trigger-on-message-received.definition.d.ts +4 -0
- package/src/providers/gmail/trigger-on-message-received.definition.js +74 -0
- package/src/providers/gmail/trigger-on-message-received.js +232 -0
- package/src/providers/googlecalendar/executors.d.ts +3 -0
- package/src/providers/googlecalendar/executors.js +2 -0
- package/src/providers/googlecalendar/trigger-on-event-changed.d.ts +2 -0
- package/src/providers/googlecalendar/trigger-on-event-changed.definition.d.ts +4 -0
- package/src/providers/googlecalendar/trigger-on-event-changed.definition.js +74 -0
- package/src/providers/googlecalendar/trigger-on-event-changed.js +233 -0
- package/src/providers/googledrive/executors.d.ts +3 -0
- package/src/providers/googledrive/executors.js +7 -0
- package/src/providers/googledrive/trigger-changes.d.ts +12 -0
- package/src/providers/googledrive/trigger-changes.definition.d.ts +7 -0
- package/src/providers/googledrive/trigger-changes.definition.js +101 -0
- package/src/providers/googledrive/trigger-changes.js +452 -0
- package/src/providers/googledrive/trigger-on-file-change.d.ts +2 -0
- package/src/providers/googledrive/trigger-on-file-change.definition.d.ts +4 -0
- package/src/providers/googledrive/trigger-on-file-change.definition.js +77 -0
- package/src/providers/googledrive/trigger-on-file-change.js +258 -0
- package/src/providers/googlesheets/executors.d.ts +3 -0
- package/src/providers/googlesheets/executors.js +2 -0
- package/src/providers/googlesheets/trigger-on-row-added.d.ts +2 -0
- package/src/providers/googlesheets/trigger-on-row-added.definition.d.ts +4 -0
- package/src/providers/googlesheets/trigger-on-row-added.definition.js +54 -0
- package/src/providers/googlesheets/trigger-on-row-added.js +234 -0
- package/src/providers/hithink_finance/runtime.js +1 -1
- package/src/providers/indexed/executors.d.ts +4 -0
- package/src/providers/indexed/executors.js +16 -0
- package/src/providers/indexed/runtime.d.ts +26 -0
- package/src/providers/indexed/runtime.js +129 -0
- package/src/providers/linear/executors.d.ts +3 -0
- package/src/providers/linear/executors.js +2 -0
- package/src/providers/linear/trigger-on-issue-changed.d.ts +2 -0
- package/src/providers/linear/trigger-on-issue-changed.definition.d.ts +5 -0
- package/src/providers/linear/trigger-on-issue-changed.definition.js +43 -0
- package/src/providers/linear/trigger-on-issue-changed.js +215 -0
- package/src/providers/notion/executors.d.ts +3 -0
- package/src/providers/notion/executors.js +2 -0
- package/src/providers/notion/trigger-on-database-page-event.d.ts +2 -0
- package/src/providers/notion/trigger-on-database-page-event.definition.d.ts +4 -0
- package/src/providers/notion/trigger-on-database-page-event.definition.js +63 -0
- package/src/providers/notion/trigger-on-database-page-event.js +293 -0
- package/src/providers/one_drive/executors.d.ts +3 -0
- package/src/providers/one_drive/executors.js +2 -0
- package/src/providers/one_drive/trigger-on-item-changed.d.ts +2 -0
- package/src/providers/one_drive/trigger-on-item-changed.definition.d.ts +4 -0
- package/src/providers/one_drive/trigger-on-item-changed.definition.js +63 -0
- package/src/providers/one_drive/trigger-on-item-changed.js +214 -0
- package/src/providers/outlook/executors.d.ts +3 -0
- package/src/providers/outlook/executors.js +2 -0
- package/src/providers/outlook/trigger-on-message-received.d.ts +2 -0
- package/src/providers/outlook/trigger-on-message-received.definition.d.ts +4 -0
- package/src/providers/outlook/trigger-on-message-received.definition.js +93 -0
- package/src/providers/outlook/trigger-on-message-received.js +226 -0
- package/src/providers/outlook_calendar/executors.js +6 -0
- package/src/providers/provider-loader.d.ts +5 -0
- package/src/providers/provider-loader.js +4 -0
- package/src/providers/registry.generated.js +2 -0
- package/src/providers/shopify/executors.d.ts +3 -0
- package/src/providers/shopify/executors.js +2 -0
- package/src/providers/shopify/trigger-on-shop-event.d.ts +2 -0
- package/src/providers/shopify/trigger-on-shop-event.definition.d.ts +4 -0
- package/src/providers/shopify/trigger-on-shop-event.definition.js +214 -0
- package/src/providers/shopify/trigger-on-shop-event.js +165 -0
- package/src/providers/slack/executors.d.ts +3 -0
- package/src/providers/slack/executors.js +2 -0
- package/src/providers/slack/trigger-on-message-posted.d.ts +2 -0
- package/src/providers/slack/trigger-on-message-posted.definition.d.ts +4 -0
- package/src/providers/slack/trigger-on-message-posted.definition.js +80 -0
- package/src/providers/slack/trigger-on-message-posted.js +176 -0
- package/src/providers/spotify/runtime.js +15 -4
- package/src/providers/stripe/executors.d.ts +3 -0
- package/src/providers/stripe/executors.js +2 -0
- package/src/providers/stripe/trigger-on-event.d.ts +2 -0
- package/src/providers/stripe/trigger-on-event.definition.d.ts +4 -0
- package/src/providers/stripe/trigger-on-event.definition.js +43 -0
- package/src/providers/stripe/trigger-on-event.js +212 -0
- package/src/providers/telegram/executors.d.ts +3 -0
- package/src/providers/telegram/executors.js +2 -0
- package/src/providers/telegram/trigger-on-update.d.ts +2 -0
- package/src/providers/telegram/trigger-on-update.definition.d.ts +5 -0
- package/src/providers/telegram/trigger-on-update.definition.js +78 -0
- package/src/providers/telegram/trigger-on-update.js +141 -0
- package/src/providers/weixin_store/access-token.d.ts +2 -1
- package/src/providers/weixin_store/access-token.js +15 -4
- package/src/providers/weixin_store/executors.js +97 -12
- package/src/providers/woocommerce/executors.d.ts +3 -0
- package/src/providers/woocommerce/executors.js +2 -0
- package/src/providers/woocommerce/trigger-on-store-event.d.ts +2 -0
- package/src/providers/woocommerce/trigger-on-store-event.definition.d.ts +4 -0
- package/src/providers/woocommerce/trigger-on-store-event.definition.js +55 -0
- package/src/providers/woocommerce/trigger-on-store-event.js +179 -0
- package/src/providers/zendesk/executors.d.ts +3 -0
- package/src/providers/zendesk/executors.js +2 -0
- package/src/providers/zendesk/trigger-on-event.d.ts +2 -0
- package/src/providers/zendesk/trigger-on-event.definition.d.ts +5 -0
- package/src/providers/zendesk/trigger-on-event.definition.js +101 -0
- package/src/providers/zendesk/trigger-on-event.js +188 -0
- package/src/saas/saas-cleanup-service.d.ts +26 -0
- package/src/saas/saas-cleanup-service.js +124 -0
- package/src/saas/saas-client.d.ts +86 -0
- package/src/saas/saas-client.js +369 -0
- package/src/saas/saas-execution-service.d.ts +15 -0
- package/src/saas/saas-execution-service.js +21 -0
- package/src/saas/saas-oauth-service.d.ts +25 -0
- package/src/saas/saas-oauth-service.js +197 -0
- package/src/saas/saas-project-service.d.ts +61 -0
- package/src/saas/saas-project-service.js +211 -0
- package/src/server/actions/action-idempotency.d.ts +1 -0
- package/src/server/actions/action-idempotency.js +1 -0
- package/src/server/actions/action-runner.d.ts +7 -0
- package/src/server/actions/action-runner.js +43 -7
- package/src/server/actions/run-log-summary.js +9 -0
- package/src/server/api/auth.d.ts +1 -0
- package/src/server/api/auth.js +13 -2
- package/src/server/api/cache-policy.js +4 -1
- package/src/server/api/connection-input.d.ts +7 -1
- package/src/server/api/connection-input.js +7 -1
- package/src/server/api/connection-routes.d.ts +3 -1
- package/src/server/api/connection-routes.js +21 -3
- package/src/server/api/http-utils.d.ts +4 -3
- package/src/server/api/oauth-completion-page.d.ts +124 -0
- package/src/server/api/oauth-completion-page.js +113 -18
- package/src/server/api/openapi.js +112 -14
- package/src/server/api/policy-input.js +6 -0
- package/src/server/api/runtime-api.d.ts +10 -2
- package/src/server/api/runtime-api.js +17 -4
- package/src/server/api/saas-completion-page.d.ts +2 -0
- package/src/server/api/saas-completion-page.js +103 -0
- package/src/server/connect-app.d.ts +5 -0
- package/src/server/connect-app.js +50 -0
- package/src/server/connect-server.d.ts +12 -0
- package/src/server/connect-server.js +147 -13
- package/src/server/connector-runtime.d.ts +3 -1
- package/src/server/connector-runtime.js +5 -2
- package/src/server/files/transit-files.js +16 -7
- package/src/server/proxy/proxy-runner.d.ts +5 -1
- package/src/server/proxy/proxy-runner.js +72 -26
- package/src/server/storage/connection-request-store.d.ts +41 -1
- package/src/server/storage/connection-request-store.js +245 -20
- package/src/server/storage/connection-store.d.ts +16 -0
- package/src/server/storage/connection-store.js +119 -0
- package/src/server/storage/node-runtime-database.js +3 -3
- package/src/server/storage/{postgres-migrations.d.ts → postgres/migrations.d.ts} +2 -2
- package/src/server/storage/{postgres-migrations.js → postgres/migrations.js} +1 -1
- package/src/server/storage/postgres/runtime-store.d.ts +43 -0
- package/src/server/storage/{postgres-runtime-store.js → postgres/runtime-store.js} +59 -94
- package/src/server/storage/runtime-database.d.ts +4 -0
- package/src/server/storage/runtime-sql.d.ts +1 -1
- package/src/server/storage/runtime-sql.js +2 -1
- package/src/server/storage/runtime-store.d.ts +1 -0
- package/src/server/storage/runtime-token-service.d.ts +2 -0
- package/src/server/storage/runtime-token-service.js +3 -0
- package/src/server/storage/saas-project-store.d.ts +77 -0
- package/src/server/storage/saas-project-store.js +273 -0
- package/src/server/storage/{sqlite-runtime-store.d.ts → sqlite/runtime-store.d.ts} +21 -25
- package/src/server/storage/{sqlite-runtime-store.js → sqlite/runtime-store.js} +100 -115
- package/src/server/storage/trigger-store.d.ts +16 -0
- package/src/server/storage/trigger-store.js +96 -0
- package/src/triggers/common/config.d.ts +2 -0
- package/src/triggers/common/config.js +17 -0
- package/src/triggers/common/configOptions.d.ts +13 -0
- package/src/triggers/common/configOptions.js +1 -0
- package/src/triggers/common/encoding.d.ts +3 -0
- package/src/triggers/common/encoding.js +19 -0
- package/src/triggers/common/integration.d.ts +115 -0
- package/src/triggers/common/integration.js +31 -0
- package/src/triggers/common/json.d.ts +6 -0
- package/src/triggers/common/json.js +91 -0
- package/src/triggers/common/poll.d.ts +41 -0
- package/src/triggers/common/poll.js +15 -0
- package/src/triggers/common/proxy.d.ts +14 -0
- package/src/triggers/common/proxy.js +1 -0
- package/src/triggers/common/types.d.ts +42 -0
- package/src/triggers/common/types.js +1 -0
- package/src/triggers/maintenance.d.ts +40 -0
- package/src/triggers/maintenance.js +120 -0
- package/src/triggers/metadata.d.ts +8 -0
- package/src/triggers/metadata.js +1 -0
- package/src/triggers/request.d.ts +33 -0
- package/src/triggers/request.js +52 -0
- package/src/triggers/resources.d.ts +12 -0
- package/src/triggers/resources.js +121 -0
- package/src/triggers/signature.d.ts +4 -0
- package/src/triggers/signature.js +48 -0
- package/src/triggers/store.d.ts +29 -0
- package/src/triggers/store.js +1 -0
- package/src/triggers/subscriptions.d.ts +22 -0
- package/src/triggers/subscriptions.js +220 -0
- package/src/triggers/trigger-runner.d.ts +34 -0
- package/src/triggers/trigger-runner.js +169 -0
- package/src/server/storage/postgres-runtime-store.d.ts +0 -39
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { optionalRecord, optionalString } from "../core/cast.js";
|
|
2
|
+
import { isFailureEnvelope, isSuccessEnvelope, readRemoteJson, RemoteHttpError, requestRemote, } from "../core/remote-http.js";
|
|
3
|
+
import { assertPublicHttpUrl, encodePathSegment } from "../core/request.js";
|
|
4
|
+
export class SaasError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
retryAfter;
|
|
8
|
+
reason;
|
|
9
|
+
connectionRequestId;
|
|
10
|
+
remoteExecutionId;
|
|
11
|
+
constructor(code, message, status = 400, retryAfter, reason) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.retryAfter = retryAfter;
|
|
16
|
+
this.reason = reason;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const executionErrors = {
|
|
20
|
+
invalid_input: { status: 400, message: "SaaS rejected the action or proxy input. Check the remote schema." },
|
|
21
|
+
scope_missing: { status: 403, message: "The SaaS account is missing required scopes." },
|
|
22
|
+
policy_denied: { status: 403, message: "SaaS execution policy denied this request." },
|
|
23
|
+
credential_expired: { status: 409, message: "The SaaS account requires reauthorization." },
|
|
24
|
+
app_not_ready: { status: 409, message: "The SaaS account is not ready for execution." },
|
|
25
|
+
connected_account_not_found: { status: 404, message: "The selected SaaS account was not found." },
|
|
26
|
+
provider_config_not_found: { status: 404, message: "The selected SaaS provider configuration was not found." },
|
|
27
|
+
provider_not_found: { status: 404, message: "The requested SaaS provider was not found." },
|
|
28
|
+
provider_not_configured: { status: 503, message: "The SaaS provider is not configured." },
|
|
29
|
+
proxy_not_supported: { status: 501, message: "This SaaS connection does not support proxy execution." },
|
|
30
|
+
proxy_upstream_error: { status: 502, message: "The SaaS proxy upstream request failed." },
|
|
31
|
+
proxy_upstream_timeout: { status: 504, message: "The SaaS proxy upstream request timed out." },
|
|
32
|
+
proxy_response_too_large: { status: 502, message: "The SaaS proxy response exceeded its size limit." },
|
|
33
|
+
rate_limited: { status: 429, message: "SaaS execution was rate limited." },
|
|
34
|
+
insufficient_credit: { status: 402, message: "SaaS execution has insufficient credit." },
|
|
35
|
+
provider_error: { status: 502, message: "The SaaS provider request failed." },
|
|
36
|
+
};
|
|
37
|
+
const remoteFailureMessages = {
|
|
38
|
+
invalid_url: "Connect blocked the SaaS address because the URL or its DNS result is not allowed. Check proxy Fake-IP and DNS settings: the SaaS hostname must resolve to a public IP address. Trusted-host overrides are not supported for SaaS requests.",
|
|
39
|
+
dns: "Connect could not resolve the SaaS hostname. Check DNS and network settings on the machine running Connect.",
|
|
40
|
+
network: "Connect could not complete the SaaS network request. Check connectivity, proxy settings and TLS certificates on the machine running Connect.",
|
|
41
|
+
redirect: "SaaS returned an HTTP redirect. Redirects are blocked to protect the project key. Check the SaaS address.",
|
|
42
|
+
invalid_json: "SaaS returned a response that is not valid UTF-8 JSON. Check the SaaS endpoint and any intervening proxy or login page.",
|
|
43
|
+
too_large: "SaaS returned a response exceeding the 4 MiB limit for project and account requests.",
|
|
44
|
+
};
|
|
45
|
+
/** Reject values that the project proxy protocol cannot represent before any remote request. */
|
|
46
|
+
export async function parseSaasProxyRequest(input) {
|
|
47
|
+
const { proxyInputSchema } = await getSchemas();
|
|
48
|
+
try {
|
|
49
|
+
const result = proxyInputSchema.safeParse(input);
|
|
50
|
+
if (result.success) {
|
|
51
|
+
const headers = new Headers(result.data.headers);
|
|
52
|
+
if (["authorization", "proxy-authorization", "cookie", "set-cookie", "host", "x-api-key"].some((name) => headers.has(name)))
|
|
53
|
+
throw new Error("Authentication headers are not supported.");
|
|
54
|
+
const contentType = headers.get("content-type");
|
|
55
|
+
if (contentType && !supportsProxyContentType(contentType))
|
|
56
|
+
throw new Error("Only JSON and text bodies are supported.");
|
|
57
|
+
return result.data;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Cyclic or excessively nested non-HTTP inputs cannot be serialized as JSON.
|
|
62
|
+
}
|
|
63
|
+
throw new SaasError("invalid_input", "SaaS proxy supports only GET/POST/PUT/PATCH/DELETE, primitive query values, non-authentication string headers and UTF-8 JSON or text bodies; unknown fields are not allowed.");
|
|
64
|
+
}
|
|
65
|
+
/** The project API never supplies credentials to the local provider runtime. */
|
|
66
|
+
export class SaasClient {
|
|
67
|
+
fetcher;
|
|
68
|
+
constructor(fetcher) {
|
|
69
|
+
this.fetcher = fetcher;
|
|
70
|
+
}
|
|
71
|
+
async discover(project, signal) {
|
|
72
|
+
const { discoverySchema } = await getSchemas();
|
|
73
|
+
const discovery = await this.request(project, "/oauth/provider-configs", discoverySchema, { signal });
|
|
74
|
+
if (new Set(discovery.providerConfigs.map((config) => config.id)).size !== discovery.providerConfigs.length)
|
|
75
|
+
throw new SaasError("oauth_source_protocol_error", "SaaS discovery contains duplicate configuration IDs.", 502);
|
|
76
|
+
return discovery;
|
|
77
|
+
}
|
|
78
|
+
async getAccount(project, selector, service, signal) {
|
|
79
|
+
const { accountSchema } = await getSchemas();
|
|
80
|
+
const account = await this.request(project, `/connected-accounts/${encodePathSegment(selector.connectedAccountId)}`, accountSchema, { signal });
|
|
81
|
+
if (account.projectId !== project.projectId ||
|
|
82
|
+
account.providerConfigId !== selector.providerConfigId ||
|
|
83
|
+
account.externalUserId !== selector.externalUserId ||
|
|
84
|
+
account.connectedAccountId !== selector.connectedAccountId ||
|
|
85
|
+
account.service !== service)
|
|
86
|
+
throw new SaasError("oauth_source_mismatch", "SaaS account does not match the requested identity.", 409);
|
|
87
|
+
return account;
|
|
88
|
+
}
|
|
89
|
+
async getRequest(project, id, signal) {
|
|
90
|
+
const { requestSchema } = await getSchemas();
|
|
91
|
+
const request = await this.request(project, `/connection-requests/${encodePathSegment(id)}`, requestSchema, {
|
|
92
|
+
signal,
|
|
93
|
+
});
|
|
94
|
+
if (request.projectId !== project.projectId || request.id !== id)
|
|
95
|
+
throw new SaasError("oauth_source_mismatch", "SaaS authorization request identity does not match.", 409);
|
|
96
|
+
return request;
|
|
97
|
+
}
|
|
98
|
+
async createLink(project, input, signal) {
|
|
99
|
+
const { requestSchema } = await getSchemas();
|
|
100
|
+
const result = await this.request(project, "/connected-accounts/link", requestSchema, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
signal,
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
providerConfigId: input.providerConfigId,
|
|
105
|
+
userId: input.externalUserId,
|
|
106
|
+
alias: input.alias,
|
|
107
|
+
returnUri: input.returnUri,
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
if (result.projectId !== project.projectId ||
|
|
111
|
+
result.providerConfigId !== input.providerConfigId ||
|
|
112
|
+
result.externalUserId !== input.externalUserId ||
|
|
113
|
+
result.alias !== input.alias)
|
|
114
|
+
throw new SaasError("oauth_source_mismatch", "SaaS authorization request identity does not match.", 409);
|
|
115
|
+
const authorizationUrl = new URL(result.authorizationUrl);
|
|
116
|
+
if (authorizationUrl.protocol !== "https:" ||
|
|
117
|
+
authorizationUrl.username ||
|
|
118
|
+
authorizationUrl.password ||
|
|
119
|
+
authorizationUrl.origin !== new URL(project.baseUrl).origin)
|
|
120
|
+
throw new SaasError("oauth_source_protocol_error", "SaaS authorization URL must use the configured origin.", 502);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
async deleteAccount(project, selector, signal) {
|
|
124
|
+
const { z, identifier } = await getSchemas();
|
|
125
|
+
const query = new URLSearchParams({ providerConfigId: selector.providerConfigId, userId: selector.externalUserId });
|
|
126
|
+
const result = await this.request(project, `/connected-accounts/${encodePathSegment(selector.connectedAccountId)}?${query}`, z.strictObject({ connectedAccountId: identifier, deleted: z.literal(true) }), { method: "DELETE", signal });
|
|
127
|
+
if (result.connectedAccountId !== selector.connectedAccountId)
|
|
128
|
+
throw new SaasError("oauth_source_mismatch", "SaaS deletion result identity does not match.", 409);
|
|
129
|
+
}
|
|
130
|
+
async executeAction(project, selector, actionId, input, signal) {
|
|
131
|
+
const { z, actionResultSchema } = await getSchemas();
|
|
132
|
+
const envelope = await this.requestEnvelope(project, `/actions/${encodePathSegment(actionId)}`, z.object({ success: z.literal(true), data: actionResultSchema }), {
|
|
133
|
+
method: "POST",
|
|
134
|
+
signal,
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
providerConfigId: selector.providerConfigId,
|
|
137
|
+
userId: selector.externalUserId,
|
|
138
|
+
connectedAccountId: selector.connectedAccountId,
|
|
139
|
+
input,
|
|
140
|
+
}),
|
|
141
|
+
}, true);
|
|
142
|
+
if (envelope.data.actionId !== actionId || !("output" in envelope.data))
|
|
143
|
+
throw new SaasError("oauth_source_protocol_error", "SaaS returned an incompatible action result.", 502);
|
|
144
|
+
return envelope.data;
|
|
145
|
+
}
|
|
146
|
+
async executeProxy(project, selector, service, request, signal) {
|
|
147
|
+
const { proxyResultSchema } = await getSchemas();
|
|
148
|
+
const envelope = await this.requestEnvelope(project, `/proxy/${encodePathSegment(service)}`, proxyResultSchema, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
signal,
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
providerConfigId: selector.providerConfigId,
|
|
153
|
+
userId: selector.externalUserId,
|
|
154
|
+
connectedAccountId: selector.connectedAccountId,
|
|
155
|
+
request: {
|
|
156
|
+
endpoint: request.endpoint,
|
|
157
|
+
method: request.method,
|
|
158
|
+
query: request.query,
|
|
159
|
+
headers: request.headers,
|
|
160
|
+
body: request.body,
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
}, true);
|
|
164
|
+
if (envelope.meta.service !== service || !("data" in envelope.data))
|
|
165
|
+
throw new SaasError("oauth_source_protocol_error", "SaaS returned an incompatible proxy result.", 502);
|
|
166
|
+
let contentType;
|
|
167
|
+
try {
|
|
168
|
+
contentType = new Headers(envelope.data.headers).get("content-type");
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
throw new SaasError("oauth_source_protocol_error", "SaaS returned invalid proxy headers.", 502);
|
|
172
|
+
}
|
|
173
|
+
if (contentType && !supportsProxyContentType(contentType))
|
|
174
|
+
throw new SaasError("oauth_source_protocol_error", "SaaS proxy supports only JSON and UTF-8 text responses.", 502);
|
|
175
|
+
return { executionId: envelope.meta.executionId, response: envelope.data };
|
|
176
|
+
}
|
|
177
|
+
async request(project, path, schema, init) {
|
|
178
|
+
const { z } = await getSchemas();
|
|
179
|
+
const envelope = await this.requestEnvelope(project, path, z.object({ success: z.literal(true), data: schema }), init);
|
|
180
|
+
return envelope.data;
|
|
181
|
+
}
|
|
182
|
+
async requestEnvelope(project, path, schema, init, execution = false) {
|
|
183
|
+
const { executionFailureSchema, executionIdentifier } = await getSchemas();
|
|
184
|
+
try {
|
|
185
|
+
return await requestRemote({
|
|
186
|
+
url: `${project.baseUrl}/v1/saas${path}`,
|
|
187
|
+
label: "SaaS",
|
|
188
|
+
timeoutMs: execution ? 300_000 : 30_000,
|
|
189
|
+
fetcher: this.fetcher,
|
|
190
|
+
init: { ...init, headers: { authorization: `Bearer ${project.apiKey}`, "content-type": "application/json" } },
|
|
191
|
+
}, async (response, signal) => {
|
|
192
|
+
const json = await readRemoteJson(response, {
|
|
193
|
+
label: "SaaS response",
|
|
194
|
+
maxBytes: (execution ? 64 : 4) * 1024 * 1024,
|
|
195
|
+
signal,
|
|
196
|
+
}).catch((error) => {
|
|
197
|
+
if (!response.ok && error instanceof RemoteHttpError && error.kind === "invalid_json")
|
|
198
|
+
return undefined;
|
|
199
|
+
throw error;
|
|
200
|
+
});
|
|
201
|
+
const failure = execution && !response.ok ? executionFailureSchema.safeParse(json) : undefined;
|
|
202
|
+
if (execution && (failure?.success || isFailureEnvelope(json))) {
|
|
203
|
+
const errorCode = failure?.success
|
|
204
|
+
? failure.data.errorCode
|
|
205
|
+
: optionalString(optionalRecord(json)?.errorCode);
|
|
206
|
+
const known = Object.hasOwn(executionErrors, errorCode) ? executionErrors[errorCode] : undefined;
|
|
207
|
+
const error = new SaasError(known ? errorCode : "oauth_source_unavailable", known?.message ?? "SaaS execution failed.", known?.status ?? 502, readRetryAfter(response));
|
|
208
|
+
const remoteId = executionIdentifier.safeParse(failure?.success ? failure.data.executionId : optionalRecord(optionalRecord(json)?.meta)?.executionId);
|
|
209
|
+
if (remoteId.success)
|
|
210
|
+
error.remoteExecutionId = remoteId.data;
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
if (response.status === 401 || response.status === 403)
|
|
215
|
+
throw new SaasError("oauth_source_unauthorized", "SaaS project key was rejected. Update the project configuration.", 503);
|
|
216
|
+
if (response.status === 404)
|
|
217
|
+
throw new SaasError("oauth_source_not_found", "SaaS resource was not found.", 404);
|
|
218
|
+
if (response.status === 429) {
|
|
219
|
+
const retryAfter = readRetryAfter(response);
|
|
220
|
+
throw new SaasError("oauth_source_rate_limited", "SaaS request was rate limited.", 429, retryAfter);
|
|
221
|
+
}
|
|
222
|
+
throw new SaasError("oauth_source_unavailable", `SaaS returned HTTP ${response.status}. Check the SaaS service logs.`, 502);
|
|
223
|
+
}
|
|
224
|
+
if (!isSuccessEnvelope(json))
|
|
225
|
+
throw new SaasError("oauth_source_protocol_error", "SaaS returned an invalid response envelope.", 502);
|
|
226
|
+
const result = schema.safeParse(json);
|
|
227
|
+
if (!result.success)
|
|
228
|
+
throw new SaasError("oauth_source_protocol_error", "SaaS returned fields that do not match the expected API contract. Check that the deployed SaaS version supports this Connect version.", 502);
|
|
229
|
+
return result.data;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
init.signal?.throwIfAborted();
|
|
234
|
+
if (error instanceof SaasError)
|
|
235
|
+
throw error;
|
|
236
|
+
if (error instanceof Error && error.name === "TimeoutError")
|
|
237
|
+
throw new SaasError("oauth_source_unavailable", "SaaS request timed out.", 504, undefined, "timeout");
|
|
238
|
+
if (execution && error instanceof RemoteHttpError && error.kind === "too_large")
|
|
239
|
+
throw new SaasError("oauth_source_response_too_large", "SaaS execution response exceeds the 64 MiB JSON limit.", 502);
|
|
240
|
+
if (error instanceof RemoteHttpError)
|
|
241
|
+
throw new SaasError("oauth_source_unavailable", remoteFailureMessages[error.kind], 502, undefined, error.kind);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export function normalizeSaasBaseUrl(value) {
|
|
247
|
+
const url = assertPublicHttpUrl(value, {
|
|
248
|
+
fieldName: "SaaS baseUrl",
|
|
249
|
+
createError: () => new SaasError("invalid_input", "SaaS baseUrl must be a public HTTPS origin."),
|
|
250
|
+
});
|
|
251
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/")
|
|
252
|
+
throw new SaasError("invalid_input", "SaaS baseUrl must be a public HTTPS origin.");
|
|
253
|
+
return url.origin;
|
|
254
|
+
}
|
|
255
|
+
function readRetryAfter(response) {
|
|
256
|
+
const header = optionalString(response.headers.get("retry-after"));
|
|
257
|
+
return header && (/^\d+$/.test(header) || Number.isFinite(Date.parse(header))) ? header : undefined;
|
|
258
|
+
}
|
|
259
|
+
function supportsProxyContentType(value) {
|
|
260
|
+
const [mediaType, ...parameters] = value
|
|
261
|
+
.toLowerCase()
|
|
262
|
+
.split(";")
|
|
263
|
+
.map((part) => part.trim());
|
|
264
|
+
if (!(mediaType.startsWith("text/") || mediaType === "application/json" || mediaType.endsWith("+json")))
|
|
265
|
+
return false;
|
|
266
|
+
return parameters
|
|
267
|
+
.filter((part) => /^charset\s*=/.test(part))
|
|
268
|
+
.every((part) => {
|
|
269
|
+
const charset = part
|
|
270
|
+
.slice(part.indexOf("=") + 1)
|
|
271
|
+
.trim()
|
|
272
|
+
.replace(/^"|"$/g, "");
|
|
273
|
+
return charset === "utf-8" || charset === "utf8" || charset === "us-ascii";
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
let schemasPromise;
|
|
277
|
+
function getSchemas() {
|
|
278
|
+
return (schemasPromise ??= createSchemas());
|
|
279
|
+
}
|
|
280
|
+
async function createSchemas() {
|
|
281
|
+
const { z } = await import("zod");
|
|
282
|
+
const identifier = z.string().min(1);
|
|
283
|
+
const providerConfigSchema = z.strictObject({
|
|
284
|
+
id: identifier,
|
|
285
|
+
service: identifier,
|
|
286
|
+
displayName: z.string(),
|
|
287
|
+
callbackUrl: z.url({ protocol: /^https?$/ }),
|
|
288
|
+
effectiveScopes: z.array(identifier),
|
|
289
|
+
actionIds: z.array(identifier),
|
|
290
|
+
proxyAvailable: z.boolean(),
|
|
291
|
+
});
|
|
292
|
+
const discoverySchema = z.strictObject({
|
|
293
|
+
projectId: identifier,
|
|
294
|
+
providerConfigs: z.array(providerConfigSchema),
|
|
295
|
+
});
|
|
296
|
+
const accountSchema = z.strictObject({
|
|
297
|
+
projectId: identifier,
|
|
298
|
+
providerConfigId: identifier,
|
|
299
|
+
service: identifier,
|
|
300
|
+
externalUserId: identifier,
|
|
301
|
+
connectedAccountId: identifier,
|
|
302
|
+
alias: z.string().nullable(),
|
|
303
|
+
status: z.enum(["active", "reauth_required", "error", "disconnected"]),
|
|
304
|
+
providerAccountId: z.string().nullable(),
|
|
305
|
+
accountLabel: z.string().nullable(),
|
|
306
|
+
scopes: z.array(z.string()),
|
|
307
|
+
});
|
|
308
|
+
const requestSchema = z
|
|
309
|
+
.strictObject({
|
|
310
|
+
id: identifier,
|
|
311
|
+
status: z.enum(["initiated", "connected", "failed", "expired"]),
|
|
312
|
+
projectId: identifier,
|
|
313
|
+
providerConfigId: identifier,
|
|
314
|
+
externalUserId: identifier,
|
|
315
|
+
service: identifier,
|
|
316
|
+
alias: z.string().nullable(),
|
|
317
|
+
authorizationUrl: z.url({ protocol: /^https$/ }),
|
|
318
|
+
connectedAccountId: identifier.nullable(),
|
|
319
|
+
errorCode: z.string().nullable(),
|
|
320
|
+
errorMessage: z.string().nullable(),
|
|
321
|
+
expiresAt: z.iso.datetime(),
|
|
322
|
+
createdAt: z.number().finite(),
|
|
323
|
+
updatedAt: z.number().finite(),
|
|
324
|
+
})
|
|
325
|
+
.refine((value) => value.status !== "connected" || value.connectedAccountId !== null);
|
|
326
|
+
const executionIdentifier = z
|
|
327
|
+
.string()
|
|
328
|
+
.min(1)
|
|
329
|
+
.max(256)
|
|
330
|
+
.regex(/^[a-zA-Z0-9_.:-]+$/);
|
|
331
|
+
const actionResultSchema = z.strictObject({
|
|
332
|
+
executionId: executionIdentifier,
|
|
333
|
+
actionId: identifier,
|
|
334
|
+
output: z.unknown(),
|
|
335
|
+
});
|
|
336
|
+
const proxyResultSchema = z.object({
|
|
337
|
+
success: z.literal(true),
|
|
338
|
+
data: z.strictObject({
|
|
339
|
+
status: z.number().int().min(200).max(599),
|
|
340
|
+
headers: z.record(z.string(), z.string()),
|
|
341
|
+
data: z.unknown(),
|
|
342
|
+
}),
|
|
343
|
+
meta: z.object({ executionId: executionIdentifier, service: identifier }),
|
|
344
|
+
});
|
|
345
|
+
const proxyInputSchema = z.strictObject({
|
|
346
|
+
endpoint: z.string().min(1),
|
|
347
|
+
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
348
|
+
query: z.record(z.string(), z.union([z.string(), z.number().finite(), z.boolean(), z.null()])).optional(),
|
|
349
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
350
|
+
body: z.json().optional(),
|
|
351
|
+
});
|
|
352
|
+
const executionFailureSchema = z.object({
|
|
353
|
+
errorCode: identifier,
|
|
354
|
+
errorMessage: z.string(),
|
|
355
|
+
executionId: executionIdentifier.optional(),
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
z,
|
|
359
|
+
identifier,
|
|
360
|
+
discoverySchema,
|
|
361
|
+
accountSchema,
|
|
362
|
+
requestSchema,
|
|
363
|
+
actionResultSchema,
|
|
364
|
+
proxyResultSchema,
|
|
365
|
+
proxyInputSchema,
|
|
366
|
+
executionFailureSchema,
|
|
367
|
+
executionIdentifier,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SaasConnectionReference } from "../connection-service.ts";
|
|
2
|
+
import type { SaasActionResult, SaasProxyResult } from "./saas-client.ts";
|
|
3
|
+
import type { SaasProjectService } from "./saas-project-service.ts";
|
|
4
|
+
import { SaasClient } from "./saas-client.ts";
|
|
5
|
+
export interface SaasExecutionServiceOptions {
|
|
6
|
+
projects: SaasProjectService;
|
|
7
|
+
client: SaasClient;
|
|
8
|
+
}
|
|
9
|
+
/** Resolves capabilities for the selected connection without fetching provider credentials or profiles. */
|
|
10
|
+
export declare class SaasExecutionService {
|
|
11
|
+
private readonly options;
|
|
12
|
+
constructor(options: SaasExecutionServiceOptions);
|
|
13
|
+
executeAction(reference: SaasConnectionReference, service: string, actionId: string, input: unknown, signal?: AbortSignal): Promise<SaasActionResult>;
|
|
14
|
+
executeProxy(reference: SaasConnectionReference, service: string, input: unknown, signal?: AbortSignal): Promise<SaasProxyResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { parseSaasProxyRequest, SaasClient, SaasError } from "./saas-client.js";
|
|
2
|
+
/** Resolves capabilities for the selected connection without fetching provider credentials or profiles. */
|
|
3
|
+
export class SaasExecutionService {
|
|
4
|
+
options;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
}
|
|
8
|
+
async executeAction(reference, service, actionId, input, signal) {
|
|
9
|
+
const { project, config } = await this.options.projects.resolveExecutionConfig(service, reference.providerConfigId, reference.managedProjectId, signal);
|
|
10
|
+
if (!config.actionIds.includes(actionId))
|
|
11
|
+
throw new SaasError("oauth_source_unsupported", "The selected SaaS connection does not support this action.");
|
|
12
|
+
return this.options.client.executeAction(project, reference, actionId, input, signal);
|
|
13
|
+
}
|
|
14
|
+
async executeProxy(reference, service, input, signal) {
|
|
15
|
+
const request = await parseSaasProxyRequest(input);
|
|
16
|
+
const { project, config } = await this.options.projects.resolveExecutionConfig(service, reference.providerConfigId, reference.managedProjectId, signal);
|
|
17
|
+
if (!config.proxyAvailable)
|
|
18
|
+
throw new SaasError("proxy_not_supported", "The selected SaaS connection does not support proxy execution.", 501);
|
|
19
|
+
return this.options.client.executeProxy(project, reference, service, request, signal);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { OAuthConnectionRequestInput, OAuthConnectionRequestStart } from "../oauth/oauth-flow-service.ts";
|
|
2
|
+
import type { ConnectionRequest, ConnectionRequestStore } from "../server/storage/connection-request-store.ts";
|
|
3
|
+
import type { SaasProjectService } from "./saas-project-service.ts";
|
|
4
|
+
import { SaasClient } from "./saas-client.ts";
|
|
5
|
+
export interface SaasOAuthServiceOptions {
|
|
6
|
+
projects: SaasProjectService;
|
|
7
|
+
requests: ConnectionRequestStore;
|
|
8
|
+
client: SaasClient;
|
|
9
|
+
}
|
|
10
|
+
export interface SaasSyncResult {
|
|
11
|
+
request: ConnectionRequest;
|
|
12
|
+
returnUri?: string;
|
|
13
|
+
}
|
|
14
|
+
/** One owner for bearer polling and browser synchronization; no network work runs inside a database transaction. */
|
|
15
|
+
export declare class SaasOAuthService {
|
|
16
|
+
private readonly options;
|
|
17
|
+
private readonly syncing;
|
|
18
|
+
constructor(options: SaasOAuthServiceOptions);
|
|
19
|
+
assertLocalAuthorization(service: string): Promise<void>;
|
|
20
|
+
start(input: OAuthConnectionRequestInput): Promise<OAuthConnectionRequestStart | undefined>;
|
|
21
|
+
sync(id: string, owner: string, signal?: AbortSignal): Promise<ConnectionRequest | undefined>;
|
|
22
|
+
syncForBrowser(id: string, owner: string, signal?: AbortSignal): Promise<SaasSyncResult | undefined>;
|
|
23
|
+
private advance;
|
|
24
|
+
private nextPollAt;
|
|
25
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { callbackReturnUri, validateReturnUri } from "../oauth/oauth-flow-service.js";
|
|
2
|
+
import { SaasClient, SaasError } from "./saas-client.js";
|
|
3
|
+
/** One owner for bearer polling and browser synchronization; no network work runs inside a database transaction. */
|
|
4
|
+
export class SaasOAuthService {
|
|
5
|
+
options;
|
|
6
|
+
syncing = new Map();
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
async assertLocalAuthorization(service) {
|
|
11
|
+
if ((await this.options.projects.getSource(service)).mode === "saas")
|
|
12
|
+
throw new SaasError("invalid_input", "Use /v1/connections/:service/connect for SaaS OAuth authorization.");
|
|
13
|
+
}
|
|
14
|
+
async start(input) {
|
|
15
|
+
const source = input.target
|
|
16
|
+
? input.target.source === "saas"
|
|
17
|
+
? { mode: "saas", ...input.target.reference }
|
|
18
|
+
: { mode: "local" }
|
|
19
|
+
: await this.options.projects.getSource(input.service);
|
|
20
|
+
if (source.mode === "local")
|
|
21
|
+
return undefined;
|
|
22
|
+
if (input.authorizationOptionIds !== undefined || input.extra !== undefined || input.secretExtra !== undefined)
|
|
23
|
+
throw new SaasError("invalid_input", "Configure SaaS OAuth scopes and credentials on the SaaS provider config; per-request overrides are not supported.");
|
|
24
|
+
validateReturnUri(input.returnUri);
|
|
25
|
+
const signal = input.signal
|
|
26
|
+
? AbortSignal.any([input.signal, AbortSignal.timeout(30_000)])
|
|
27
|
+
: AbortSignal.timeout(30_000);
|
|
28
|
+
const { project } = await this.options.projects.resolveConfig(input.service, source.providerConfigId, source.managedProjectId, signal);
|
|
29
|
+
const id = crypto.randomUUID();
|
|
30
|
+
const completion = new URL("/oauth/saas/complete", this.options.projects.requireOrigin());
|
|
31
|
+
completion.searchParams.set("request", id);
|
|
32
|
+
const pending = {
|
|
33
|
+
connectionRequestId: id,
|
|
34
|
+
connectionId: input.target?.id ?? crypto.randomUUID(),
|
|
35
|
+
owner: input.owner,
|
|
36
|
+
service: input.service,
|
|
37
|
+
connectionName: input.target?.connectionName ?? input.connectionName ?? crypto.randomUUID(),
|
|
38
|
+
managedProjectId: project.id,
|
|
39
|
+
providerConfigId: source.providerConfigId,
|
|
40
|
+
externalUserId: await this.options.projects.externalUserId(),
|
|
41
|
+
createdAt: new Date().toISOString(),
|
|
42
|
+
expiresAt: new Date(Date.now() + 600_000).toISOString(),
|
|
43
|
+
returnUri: input.returnUri,
|
|
44
|
+
target: input.target ? { id: input.target.id, revision: input.target.revision } : undefined,
|
|
45
|
+
comment: input.target?.source === "saas" ? input.target.comment : null,
|
|
46
|
+
};
|
|
47
|
+
signal.throwIfAborted();
|
|
48
|
+
const lease = await this.options.requests.createSaas(pending);
|
|
49
|
+
try {
|
|
50
|
+
const remote = await this.options.client.createLink(project, {
|
|
51
|
+
providerConfigId: pending.providerConfigId,
|
|
52
|
+
externalUserId: pending.externalUserId,
|
|
53
|
+
alias: `connect-${id}`,
|
|
54
|
+
returnUri: completion.toString(),
|
|
55
|
+
}, signal);
|
|
56
|
+
if (remote.service !== input.service)
|
|
57
|
+
throw new SaasError("oauth_source_mismatch", "SaaS authorization service does not match.", 409);
|
|
58
|
+
if (!(await this.options.requests.saveSaasRequest(lease, remote.id, remote.expiresAt)))
|
|
59
|
+
throw new SaasError("request_superseded", "This authorization request was replaced or its lease was lost.", 409);
|
|
60
|
+
await this.options.requests.releaseSaas(lease, Date.now() + 2_000);
|
|
61
|
+
return {
|
|
62
|
+
connectionRequestId: id,
|
|
63
|
+
stateHandle: id,
|
|
64
|
+
status: "initiated",
|
|
65
|
+
expiresAt: remote.expiresAt,
|
|
66
|
+
authorizationUrl: remote.authorizationUrl,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
await this.options.requests.failSaas(lease, "oauth_source_result_unknown", "Authorization creation could not be confirmed. Check SaaS accounts before starting again.");
|
|
71
|
+
const error = new SaasError("oauth_source_result_unknown", "Authorization creation could not be confirmed. Check SaaS accounts before starting again.", 502);
|
|
72
|
+
error.connectionRequestId = id;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async sync(id, owner, signal) {
|
|
77
|
+
signal?.throwIfAborted();
|
|
78
|
+
const key = `${owner}\0${id}`;
|
|
79
|
+
let promise = this.syncing.get(key);
|
|
80
|
+
if (!promise) {
|
|
81
|
+
promise = this.advance(id, owner).finally(() => {
|
|
82
|
+
this.syncing.delete(key);
|
|
83
|
+
});
|
|
84
|
+
this.syncing.set(key, promise);
|
|
85
|
+
}
|
|
86
|
+
if (!signal)
|
|
87
|
+
await promise;
|
|
88
|
+
else {
|
|
89
|
+
const onAbort = () => rejectAbort(signal.reason);
|
|
90
|
+
let rejectAbort;
|
|
91
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
92
|
+
rejectAbort = reject;
|
|
93
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await Promise.race([promise, aborted]);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
signal.removeEventListener("abort", onAbort);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
signal?.throwIfAborted();
|
|
103
|
+
return this.options.requests.get(id, owner);
|
|
104
|
+
}
|
|
105
|
+
async syncForBrowser(id, owner, signal) {
|
|
106
|
+
const request = await this.sync(id, owner, signal);
|
|
107
|
+
if (!request)
|
|
108
|
+
return undefined;
|
|
109
|
+
const uri = await this.options.requests.getSaasReturnUri(id, owner);
|
|
110
|
+
const returnUri = uri && (request.status === "connected" || request.status === "failed")
|
|
111
|
+
? callbackReturnUri({ returnUri: uri, service: request.service }, request.status === "connected" ? "success" : "error", request.errorCode ?? undefined, request.errorMessage ?? undefined)
|
|
112
|
+
: undefined;
|
|
113
|
+
return { request, returnUri };
|
|
114
|
+
}
|
|
115
|
+
async advance(id, owner) {
|
|
116
|
+
const signal = AbortSignal.timeout(30_000);
|
|
117
|
+
signal.throwIfAborted();
|
|
118
|
+
const lease = await this.options.requests.claimSaas(id, owner);
|
|
119
|
+
if (!lease)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
signal.throwIfAborted();
|
|
123
|
+
if (lease.phase === "creating") {
|
|
124
|
+
await this.options.requests.failSaas(lease, "oauth_source_result_unknown", "Authorization creation could not be confirmed. Check SaaS accounts before starting again.");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (lease.candidate) {
|
|
128
|
+
signal.throwIfAborted();
|
|
129
|
+
await this.options.requests.completeSaas(lease);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const pending = lease.pending;
|
|
133
|
+
const project = await this.options.projects.requireProject(pending.managedProjectId);
|
|
134
|
+
const remote = await this.options.client.getRequest(project, lease.remoteRequestId, signal);
|
|
135
|
+
if (remote.providerConfigId !== pending.providerConfigId ||
|
|
136
|
+
remote.externalUserId !== pending.externalUserId ||
|
|
137
|
+
remote.service !== pending.service ||
|
|
138
|
+
remote.alias !== `connect-${pending.connectionRequestId}`)
|
|
139
|
+
throw new SaasError("oauth_source_mismatch", "SaaS authorization result does not match the saved request.", 409);
|
|
140
|
+
if (remote.status === "initiated") {
|
|
141
|
+
await this.options.requests.releaseSaas(lease, Date.now() + 2_000);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (remote.status !== "connected") {
|
|
145
|
+
await this.options.requests.failSaas(lease, "oauth_authorization_failed", "SaaS authorization failed or expired.");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const account = await this.options.client.getAccount(project, {
|
|
149
|
+
providerConfigId: pending.providerConfigId,
|
|
150
|
+
externalUserId: pending.externalUserId,
|
|
151
|
+
connectedAccountId: remote.connectedAccountId,
|
|
152
|
+
}, pending.service, signal);
|
|
153
|
+
if (account.alias !== remote.alias)
|
|
154
|
+
throw new SaasError("oauth_source_mismatch", "SaaS account alias does not match this authorization attempt.", 409);
|
|
155
|
+
if (account.status !== "active" && account.status !== "reauth_required") {
|
|
156
|
+
await this.options.requests.failSaas(lease, "oauth_authorization_failed", "The SaaS account is not available.");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
signal.throwIfAborted();
|
|
160
|
+
if (!(await this.options.requests.saveSaasCandidate(lease, {
|
|
161
|
+
connectedAccountId: account.connectedAccountId,
|
|
162
|
+
profile: {
|
|
163
|
+
accountId: account.providerAccountId ?? account.connectedAccountId,
|
|
164
|
+
displayName: account.accountLabel ?? account.service,
|
|
165
|
+
grantedScopes: account.scopes,
|
|
166
|
+
},
|
|
167
|
+
status: account.status,
|
|
168
|
+
comment: pending.comment ?? null,
|
|
169
|
+
})))
|
|
170
|
+
return;
|
|
171
|
+
signal.throwIfAborted();
|
|
172
|
+
await this.options.requests.completeSaas(lease);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error instanceof SaasError &&
|
|
176
|
+
(error.code === "oauth_source_mismatch" || error.code === "oauth_source_protocol_error")) {
|
|
177
|
+
await this.options.requests.failSaas(lease, error.code, error.message);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
await this.options.requests.releaseSaas(lease, this.nextPollAt(lease, error), true);
|
|
181
|
+
}
|
|
182
|
+
if (signal.aborted)
|
|
183
|
+
throw new SaasError("oauth_source_unavailable", "SaaS authorization synchronization timed out.", 504);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
nextPollAt(lease, error) {
|
|
188
|
+
let delay = Math.min(30_000, 2_000 * 2 ** Math.min(lease.pollAttempts, 4));
|
|
189
|
+
if (error instanceof SaasError && error.retryAfter) {
|
|
190
|
+
const seconds = Number(error.retryAfter);
|
|
191
|
+
const retryAt = Number.isFinite(seconds) ? Date.now() + seconds * 1_000 : Date.parse(error.retryAfter);
|
|
192
|
+
if (Number.isFinite(retryAt))
|
|
193
|
+
delay = Math.max(delay, retryAt - Date.now());
|
|
194
|
+
}
|
|
195
|
+
return Date.now() + delay;
|
|
196
|
+
}
|
|
197
|
+
}
|