@zackbart/connecta 0.28.0 → 0.29.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/CHANGELOG.md +961 -5
- package/README.md +62 -39
- package/bin/connecta.mjs +110 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1632 -1509
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +526 -0
- package/dist/auth-elicitation.js +283 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +368 -224
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +631 -0
- package/dist/config.js +503 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +220 -0
- package/dist/connectors/api-connector.js +214 -0
- package/dist/connectors/api.d.ts +8 -71
- package/dist/connectors/api.js +13 -99
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +108 -25
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1780 -351
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +25 -257
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +230 -73
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +117 -0
- package/dist/oauth-partition.js +17 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/basecamp/index.d.ts +43 -0
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/breeze/index.d.ts +34 -0
- package/dist/providers/breeze/index.js +1446 -0
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/ccb/index.d.ts +103 -0
- package/dist/providers/ccb/index.js +1725 -0
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/overflow/index.d.ts +60 -0
- package/dist/providers/overflow/index.js +1535 -0
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/planning-center/index.d.ts +59 -0
- package/dist/providers/planning-center/index.js +2199 -0
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/tithely/index.d.ts +49 -0
- package/dist/providers/tithely/index.js +1003 -0
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +356 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +136 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +320 -54
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +137 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +434 -0
- package/documentation/architecture.md +665 -230
- package/documentation/auth.md +962 -458
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +125 -35
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +133 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { createRequestStateCodec, inputRequired, ProtocolError, INVALID_PARAMS, PROTOCOL_VERSION_META_KEY, CLIENT_CAPABILITIES_META_KEY, } from "@modelcontextprotocol/server";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
4
|
+
import { closeScope } from "./runtime/connector-scope.js";
|
|
5
|
+
import { storedCredentialShape } from "./credential-rules.js";
|
|
6
|
+
import { authRecoveryFacts, bindReplayReads, classificationDigest } from "./invocation-auth.js";
|
|
7
|
+
import { CatalogService } from "./catalog-service.js";
|
|
8
|
+
import { DownstreamElicitation, MAX_RELAY_STATE_CHARS, } from "./downstream-input.js";
|
|
9
|
+
import { canonicalState as canonical, requestDigest as digest, stateObject as object, invalidRequestState as invalidState, REQUEST_STATE_TTL_MS as TTL_MS, MAX_INPUT_ROUNDS as MAX_ROUNDS, } from "./request-state.js";
|
|
10
|
+
const INPUT_KEY = "connecta_auth";
|
|
11
|
+
const MESSAGE = "Connect this service in your browser, then retry the request.";
|
|
12
|
+
function failure(code, message) {
|
|
13
|
+
const structuredContent = { ok: false, error: { code, message, retryable: false } };
|
|
14
|
+
return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent, isError: true };
|
|
15
|
+
}
|
|
16
|
+
/** Request-local auth recovery at the MCP boundary, never inside a guest provider. */
|
|
17
|
+
export class AuthElicitation {
|
|
18
|
+
options;
|
|
19
|
+
codec;
|
|
20
|
+
downstream;
|
|
21
|
+
stateCodec() {
|
|
22
|
+
const key = this.options.vault?.requestStateKey;
|
|
23
|
+
if (!key)
|
|
24
|
+
return invalidState();
|
|
25
|
+
return (this.codec ??= key.call(this.options.vault).then((bytes) => createRequestStateCodec({
|
|
26
|
+
key: bytes,
|
|
27
|
+
ttlSeconds: TTL_MS / 1000,
|
|
28
|
+
bind: () => canonical({ principal: this.options.principal, endpoint: this.options.endpoint }),
|
|
29
|
+
})));
|
|
30
|
+
}
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.downstream = new DownstreamElicitation({
|
|
34
|
+
...options,
|
|
35
|
+
mint: async (state, context) => (await this.stateCodec()).mint(state, context),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** The SDK runs this before any tool handler, including non-auth tools. */
|
|
39
|
+
async verify(wire, context) {
|
|
40
|
+
const { vault, endpoint, principal, registry, canManage } = this.options;
|
|
41
|
+
if (!vault?.requestStateKey || wire.length > MAX_RELAY_STATE_CHARS)
|
|
42
|
+
return invalidState();
|
|
43
|
+
const state = await (await this.stateCodec()).verify(wire, context);
|
|
44
|
+
if (object(state) && state.version === 2 && state.kind === "downstream")
|
|
45
|
+
return this.downstream.verify(state, context);
|
|
46
|
+
if (wire.length > 8192)
|
|
47
|
+
return invalidState();
|
|
48
|
+
if (!object(state) ||
|
|
49
|
+
state.version !== 1 ||
|
|
50
|
+
!principal ||
|
|
51
|
+
state.principal !== principal ||
|
|
52
|
+
state.endpoint !== endpoint ||
|
|
53
|
+
typeof state.connector !== "string" ||
|
|
54
|
+
!registry.getConnector(state.connector) ||
|
|
55
|
+
!canManage(state.connector) ||
|
|
56
|
+
!Number.isSafeInteger(state.expiresAt) ||
|
|
57
|
+
state.expiresAt <= Date.now() ||
|
|
58
|
+
state.expiresAt > Date.now() + TTL_MS ||
|
|
59
|
+
!Number.isInteger(state.round) ||
|
|
60
|
+
state.round < 1 ||
|
|
61
|
+
state.round > MAX_ROUNDS ||
|
|
62
|
+
!["call_tool", "call_destructive_tool", "execute_code", "authorize_connector"].includes(String(state.tool)) ||
|
|
63
|
+
typeof state.digest !== "string" ||
|
|
64
|
+
!/^[a-f0-9]{64}$/.test(state.digest) ||
|
|
65
|
+
!Array.isArray(state.browserNonces) ||
|
|
66
|
+
state.browserNonces.length !== state.round ||
|
|
67
|
+
!state.browserNonces.every((nonce) => typeof nonce === "string" && /^[a-f0-9-]{36}$/.test(nonce)) ||
|
|
68
|
+
!Array.isArray(state.reads) ||
|
|
69
|
+
!state.reads.every((read) => object(read) &&
|
|
70
|
+
typeof read.address === "string" &&
|
|
71
|
+
typeof read.digest === "string" &&
|
|
72
|
+
/^[a-f0-9]{64}$/.test(read.digest)) ||
|
|
73
|
+
(state.address !== undefined && typeof state.address !== "string"))
|
|
74
|
+
return invalidState();
|
|
75
|
+
if (context.mcpReq.method !== "tools/call" || context.http?.req?.headers.get("Mcp-Name") !== state.tool)
|
|
76
|
+
return invalidState();
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
79
|
+
async browserProgress(state) {
|
|
80
|
+
let started;
|
|
81
|
+
let claimed;
|
|
82
|
+
let completed = false;
|
|
83
|
+
let pending = false;
|
|
84
|
+
for (const nonce of state.browserNonces) {
|
|
85
|
+
const progress = await this.options.linkProgress(state.connector, nonce);
|
|
86
|
+
if (progress === "started")
|
|
87
|
+
started = nonce;
|
|
88
|
+
if (progress === "claimed")
|
|
89
|
+
claimed = nonce;
|
|
90
|
+
completed ||= progress === "started";
|
|
91
|
+
pending ||= progress === "claimed";
|
|
92
|
+
}
|
|
93
|
+
const predecessor = started ?? claimed;
|
|
94
|
+
return { ...(predecessor ? { predecessor } : {}), completed: completed && !pending };
|
|
95
|
+
}
|
|
96
|
+
async connected(id, context) {
|
|
97
|
+
const { registry, publicUrl, vault, principal, defer } = this.options;
|
|
98
|
+
const connector = registry.getConnector(id);
|
|
99
|
+
const scope = {};
|
|
100
|
+
const signal = AbortSignal.any([context.mcpReq.signal, this.options.requestSignal]);
|
|
101
|
+
try {
|
|
102
|
+
return await runEdge(withDeadlineEffect((deadline) => Effect.gen(function* () {
|
|
103
|
+
const ctx = registry.contextFor(id, publicUrl, scope, { signal: deadline });
|
|
104
|
+
yield* Effect.addFinalizer(() => closeScope(connector, ctx, defer));
|
|
105
|
+
if (connector.credential) {
|
|
106
|
+
const metadata = yield* Effect.promise(() => vault.metadata(id, connector.authScope === "personal" ? principal : undefined));
|
|
107
|
+
if (storedCredentialShape(connector.credential, metadata?.fields ?? null).state !== "valid")
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (!connector.status)
|
|
111
|
+
return Boolean(connector.credential);
|
|
112
|
+
return ((yield* Effect.promise(() => registry.statusFor(id, publicUrl, scope, { signal: deadline }))).state ===
|
|
113
|
+
"ok");
|
|
114
|
+
}).pipe(Effect.scoped), { signal, timeoutMs: 5000, timeoutError: new Error("Auth status check timed out") }));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async run(tool, args, context, operation) {
|
|
121
|
+
this.downstream.bind(context);
|
|
122
|
+
const prior = context.mcpReq.requestState();
|
|
123
|
+
if (prior?.version === 2)
|
|
124
|
+
return this.downstream.resume(prior, tool, args, context, operation);
|
|
125
|
+
const previous = prior;
|
|
126
|
+
const requestDigest = previous ? await digest(tool, args) : undefined;
|
|
127
|
+
if (previous && (previous.tool !== tool || previous.digest !== requestDigest || previous.address !== args.address))
|
|
128
|
+
return failure("invalid_request_state", "Invalid or expired requestState");
|
|
129
|
+
const response = context.mcpReq.inputResponses?.[INPUT_KEY];
|
|
130
|
+
if (previous && object(response) && !["accept", "decline", "cancel"].includes(String(response.action))) {
|
|
131
|
+
throw new ProtocolError(INVALID_PARAMS, "Invalid auth elicitation response", {
|
|
132
|
+
reason: "invalid_input_response",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (previous && !(await this.options.claimRetry(previous.connector, previous.browserNonces.at(-1)))) {
|
|
136
|
+
return failure("invalid_request_state", "Invalid or expired requestState");
|
|
137
|
+
}
|
|
138
|
+
if (previous && object(response)) {
|
|
139
|
+
if (response.action === "decline")
|
|
140
|
+
return failure("auth_declined", "Connection was declined. The request was not retried.");
|
|
141
|
+
if (response.action === "cancel")
|
|
142
|
+
return failure("auth_cancelled", "Connection was cancelled. The request was not retried.");
|
|
143
|
+
}
|
|
144
|
+
if (previous?.reads.length) {
|
|
145
|
+
const catalog = new CatalogService(this.options.registry, this.options.publicUrl, {
|
|
146
|
+
requestScope: this.options.requestScope,
|
|
147
|
+
requestSignal: AbortSignal.any([context.mcpReq.signal, this.options.requestSignal]),
|
|
148
|
+
});
|
|
149
|
+
// Recheck every entered read before restarting any part of a program.
|
|
150
|
+
// A trusted pool cannot authorize replay of a newly classified write.
|
|
151
|
+
for (const read of previous.reads) {
|
|
152
|
+
const current = await catalog.resolveTool(read.address, { signal: context.mcpReq.signal });
|
|
153
|
+
if (!current.ok ||
|
|
154
|
+
current.resolved.classificationFresh !== true ||
|
|
155
|
+
current.resolved.definition.classification !== "read" ||
|
|
156
|
+
(await classificationDigest(current.resolved.definition)) !== read.digest) {
|
|
157
|
+
const structuredContent = {
|
|
158
|
+
ok: false,
|
|
159
|
+
error: {
|
|
160
|
+
code: "auth_replay_refused",
|
|
161
|
+
retryable: false,
|
|
162
|
+
reconciliationRequired: true,
|
|
163
|
+
message: "An entered read no longer has the same fresh classification. Reconcile its target before starting a new request.",
|
|
164
|
+
authorizationUrl: (await this.options.connectLink(previous.connector)).url,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
isError: true,
|
|
169
|
+
structuredContent,
|
|
170
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
bindReplayReads(this.options.requestScope, previous.reads);
|
|
175
|
+
}
|
|
176
|
+
const browser = previous && tool === "authorize_connector" ? await this.browserProgress(previous) : undefined;
|
|
177
|
+
if (browser?.completed && previous) {
|
|
178
|
+
// Retire unspent links before the status read. A browser claim racing
|
|
179
|
+
// these CASes either wins and is observed as pending, or is refused.
|
|
180
|
+
await this.options.closeLinks(previous.connector, previous.browserNonces);
|
|
181
|
+
if ((await this.browserProgress(previous)).completed && (await this.connected(previous.connector, context))) {
|
|
182
|
+
const structuredContent = { connector: previous.connector, status: "ok" };
|
|
183
|
+
return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
let result = await operation();
|
|
187
|
+
const relayed = await this.downstream.finish(tool, args, context, result, previous);
|
|
188
|
+
if (relayed !== result)
|
|
189
|
+
return relayed;
|
|
190
|
+
const error = result.structuredContent?.error;
|
|
191
|
+
const authFailure = result.isError && object(error) && (error.code === "auth_required" || error.code === "downstream_oauth_required");
|
|
192
|
+
const explicit = tool === "authorize_connector" &&
|
|
193
|
+
["oauth", "operator_config"].includes(String(result.structuredContent?.recovery));
|
|
194
|
+
if (!authFailure && !explicit)
|
|
195
|
+
return result;
|
|
196
|
+
const id = authFailure ? error.connector : explicit ? args.connector : undefined;
|
|
197
|
+
// Every accepted retry owns new request-local facts. Handler-authored
|
|
198
|
+
// errors and program write summaries cannot establish replay eligibility.
|
|
199
|
+
const facts = await authRecoveryFacts(this.options.requestScope, String(id));
|
|
200
|
+
const blocked = !explicit && (!facts.eligible || facts.writeEntered);
|
|
201
|
+
if (authFailure && (facts.unsafeEntered || (tool !== "call_tool" && !facts.eligible))) {
|
|
202
|
+
const structuredContent = {
|
|
203
|
+
...result.structuredContent,
|
|
204
|
+
error: {
|
|
205
|
+
...error,
|
|
206
|
+
retryable: false,
|
|
207
|
+
reconciliationRequired: true,
|
|
208
|
+
retry: "This call may have partially run. Reconcile its target before retrying after the operator completes recovery.",
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
result = { ...result, structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
212
|
+
}
|
|
213
|
+
const { registry, vault, principal, publicUrl, canManage, unavailable } = this.options;
|
|
214
|
+
const connector = typeof id === "string" ? registry.getConnector(id) : undefined;
|
|
215
|
+
const recoverable = connector?.startAuth || (connector?.credential && this.options.credentialUi);
|
|
216
|
+
if (typeof id !== "string" ||
|
|
217
|
+
!/^[a-z0-9_-]+$/.test(id) ||
|
|
218
|
+
!recoverable ||
|
|
219
|
+
!principal ||
|
|
220
|
+
!publicUrl ||
|
|
221
|
+
unavailable ||
|
|
222
|
+
!canManage(id))
|
|
223
|
+
return result;
|
|
224
|
+
const envelope = context.mcpReq.envelope;
|
|
225
|
+
const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
|
|
226
|
+
const elicitation = object(capabilities) ? capabilities.elicitation : undefined;
|
|
227
|
+
const capable = envelope?.[PROTOCOL_VERSION_META_KEY] === "2026-07-28" &&
|
|
228
|
+
object(elicitation) &&
|
|
229
|
+
Object.hasOwn(elicitation, "url") &&
|
|
230
|
+
object(elicitation.url);
|
|
231
|
+
if (blocked || !capable || !vault?.requestStateKey || !vault.seal || !vault.open) {
|
|
232
|
+
if (!authFailure)
|
|
233
|
+
return result;
|
|
234
|
+
const structuredContent = {
|
|
235
|
+
...result.structuredContent,
|
|
236
|
+
error: {
|
|
237
|
+
...result.structuredContent?.error,
|
|
238
|
+
authorizationUrl: (await this.options.connectLink(id)).url,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
return { ...result, structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
242
|
+
}
|
|
243
|
+
if (previous && previous.round >= MAX_ROUNDS)
|
|
244
|
+
return failure("auth_round_limit", "Connection is still required after three prompts. Connect explicitly, then start a new request.");
|
|
245
|
+
// Once a verified browser has started this attempt, retries continue it.
|
|
246
|
+
// A pending consent must never be reset by another force=true prompt.
|
|
247
|
+
const forced = explicit && args.force === true;
|
|
248
|
+
const link = await this.options.connectLink(id, forced && !browser?.predecessor, forced ? browser?.predecessor : undefined);
|
|
249
|
+
const state = {
|
|
250
|
+
version: 1,
|
|
251
|
+
principal,
|
|
252
|
+
endpoint: this.options.endpoint,
|
|
253
|
+
connector: id,
|
|
254
|
+
tool,
|
|
255
|
+
...(typeof args.address === "string" ? { address: args.address } : {}),
|
|
256
|
+
digest: requestDigest ?? (await digest(tool, args)),
|
|
257
|
+
round: (previous?.round ?? 0) + 1,
|
|
258
|
+
expiresAt: previous?.expiresAt ?? Date.now() + TTL_MS,
|
|
259
|
+
browserNonces: [...(previous?.browserNonces ?? []), link.nonce],
|
|
260
|
+
reads: [...new Map([...(previous?.reads ?? []), ...facts.reads].map((read) => [read.address, read])).values()],
|
|
261
|
+
};
|
|
262
|
+
const wire = await (await this.stateCodec()).mint(state, context);
|
|
263
|
+
if (wire.length > 8192) {
|
|
264
|
+
const structuredContent = {
|
|
265
|
+
...result.structuredContent,
|
|
266
|
+
error: {
|
|
267
|
+
...result.structuredContent?.error,
|
|
268
|
+
authorizationUrl: link.url,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
return { ...result, structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
|
|
272
|
+
}
|
|
273
|
+
return inputRequired({
|
|
274
|
+
inputRequests: {
|
|
275
|
+
[INPUT_KEY]: inputRequired.elicitUrl({
|
|
276
|
+
message: MESSAGE,
|
|
277
|
+
url: link.url,
|
|
278
|
+
}),
|
|
279
|
+
},
|
|
280
|
+
requestState: wire,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
package/dist/branding.d.ts
CHANGED
|
@@ -48,25 +48,18 @@ export declare function droppedThemeTokens(theme?: ConnectaTheme): string[];
|
|
|
48
48
|
* anything that would need escaping is dropped rather than rewritten.
|
|
49
49
|
*/
|
|
50
50
|
export declare function themeCss(theme: ResolvedTheme): string;
|
|
51
|
-
/**
|
|
52
|
-
* The resolved scheme as the root element's attribute. The dark palette keys
|
|
53
|
-
* off `html[data-scheme]`, so "system" leaves it off and the media query
|
|
54
|
-
* decides.
|
|
55
|
-
*/
|
|
56
|
-
export declare function schemeAttribute(theme: ResolvedTheme): string;
|
|
57
51
|
/** Text or attribute value, escaped for any position in an HTML document. */
|
|
58
52
|
export declare function escapeHtml(value: string): string;
|
|
59
53
|
export interface PageLayout {
|
|
60
54
|
/** The complete `<title>`. */
|
|
61
55
|
title: string;
|
|
56
|
+
/** The SPA puts the masthead in its sidebar. */
|
|
57
|
+
operatorShell?: boolean;
|
|
62
58
|
/** Whether the operator UI is mounted, and so serves `/favicon.*` and `/`. */
|
|
63
59
|
uiMounted: boolean;
|
|
64
60
|
/**
|
|
65
61
|
* The origin that serves `/favicon.*`, for a page that may be rendered on
|
|
66
|
-
* another host: root-relative icon hrefs resolve against it.
|
|
67
|
-
* on the artifact host passes `publicUrl`, since that host answers every
|
|
68
|
-
* non-artifact path, the favicons included, with 404. Omitted, they stay
|
|
69
|
-
* root-relative.
|
|
62
|
+
* another host: root-relative icon hrefs resolve against it. Omitted, they stay root-relative.
|
|
70
63
|
*/
|
|
71
64
|
iconOrigin?: string | undefined;
|
|
72
65
|
/** Markup after the masthead: the page's `<main>`. */
|
|
@@ -119,15 +112,14 @@ export declare const STATUS_ICONS: {
|
|
|
119
112
|
/**
|
|
120
113
|
* The 404 a browser gets for a path nothing serves, and everyone else's plain
|
|
121
114
|
* `Not Found`. The body is built from deployment configuration alone and names
|
|
122
|
-
* no path, so every unserved path
|
|
123
|
-
* answers with the same bytes for the same `Accept`: a 404 must not tell one
|
|
115
|
+
* no path, so every unserved path answers with the same bytes for the same `Accept`: a 404 must not tell one
|
|
124
116
|
* route from another. The home link appears only when the operator UI is
|
|
125
|
-
* mounted to land on, and points at the public origin
|
|
126
|
-
* 404 does not link to another 404.
|
|
117
|
+
* mounted to land on, and points at the configured public origin.
|
|
127
118
|
*/
|
|
128
119
|
export declare function notFoundResponse(request: Request, opts: {
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
ui?: {
|
|
121
|
+
readonly branding?: ConnectaBranding | undefined;
|
|
122
|
+
} | undefined;
|
|
131
123
|
publicUrl?: string | undefined;
|
|
132
124
|
}): Response;
|
|
133
125
|
/**
|
package/dist/branding.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PAGE_CSS, TOKENS_CSS } from "./page-styles.js";
|
|
2
|
+
import { htmlSecurityHeaders } from "./html-security.js";
|
|
2
3
|
/** Connecta's default monochrome "C" mark. */
|
|
3
4
|
export const CONNECTA_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
4
5
|
<style>
|
|
@@ -33,11 +34,8 @@ export function resolveBranding(branding) {
|
|
|
33
34
|
...(ownerUrl && isSafeHttpUrl(ownerUrl) ? { ownerUrl } : {}),
|
|
34
35
|
description: trimmedString(branding?.description) ??
|
|
35
36
|
`Manage the services this ${productName} instance makes available to agents.`,
|
|
36
|
-
pageTitle: trimmedString(branding?.pageTitle) ??
|
|
37
|
-
|
|
38
|
-
faviconHref: faviconHref && isSafeIconHref(faviconHref)
|
|
39
|
-
? faviconHref
|
|
40
|
-
: DEFAULT_FAVICON_HREF,
|
|
37
|
+
pageTitle: trimmedString(branding?.pageTitle) ?? (ownerName ? `${productName} — ${ownerName}` : productName),
|
|
38
|
+
faviconHref: faviconHref && isSafeIconHref(faviconHref) ? faviconHref : DEFAULT_FAVICON_HREF,
|
|
41
39
|
themeColor: configuredThemeColor(branding) ?? "#ffffff",
|
|
42
40
|
theme: resolveTheme(branding?.theme),
|
|
43
41
|
};
|
|
@@ -63,8 +61,7 @@ function isFontStack(value) {
|
|
|
63
61
|
if (value.length > 200)
|
|
64
62
|
return false;
|
|
65
63
|
const names = value.split(",");
|
|
66
|
-
return names.length <= 12 &&
|
|
67
|
-
names.every((name) => FONT_NAME.test(name.trim()));
|
|
64
|
+
return names.length <= 12 && names.every((name) => FONT_NAME.test(name.trim()));
|
|
68
65
|
}
|
|
69
66
|
const COLOR_SCHEMES = ["system", "light", "dark"];
|
|
70
67
|
/**
|
|
@@ -96,9 +93,7 @@ export function resolveTheme(theme) {
|
|
|
96
93
|
*/
|
|
97
94
|
function radiusLength(radius) {
|
|
98
95
|
if (typeof radius === "number") {
|
|
99
|
-
return Number.isFinite(radius) && radius >= 0 && radius <= 999
|
|
100
|
-
? `${radius}px`
|
|
101
|
-
: undefined;
|
|
96
|
+
return Number.isFinite(radius) && radius >= 0 && radius <= 999 ? `${radius}px` : undefined;
|
|
102
97
|
}
|
|
103
98
|
const value = trimmedString(radius);
|
|
104
99
|
if (!value || !CSS_LENGTH.test(value))
|
|
@@ -128,8 +123,7 @@ export function droppedThemeTokens(theme) {
|
|
|
128
123
|
}
|
|
129
124
|
// Compared against the trimmed value the resolver reads, so `" dark "` is
|
|
130
125
|
// not reported as dropped when it was applied.
|
|
131
|
-
if (isSetValue(theme.colorScheme) &&
|
|
132
|
-
trimmedString(theme.colorScheme) !== resolved.colorScheme) {
|
|
126
|
+
if (isSetValue(theme.colorScheme) && trimmedString(theme.colorScheme) !== resolved.colorScheme) {
|
|
133
127
|
dropped.push("colorScheme");
|
|
134
128
|
}
|
|
135
129
|
return dropped.map((token) => `theme.${token}`);
|
|
@@ -154,18 +148,12 @@ export function themeCss(theme) {
|
|
|
154
148
|
* off `html[data-scheme]`, so "system" leaves it off and the media query
|
|
155
149
|
* decides.
|
|
156
150
|
*/
|
|
157
|
-
|
|
158
|
-
return theme.colorScheme === "system"
|
|
159
|
-
? ""
|
|
160
|
-
: ` data-scheme="${theme.colorScheme}"`;
|
|
151
|
+
function schemeAttribute(theme) {
|
|
152
|
+
return theme.colorScheme === "system" ? "" : ` data-scheme="${theme.colorScheme}"`;
|
|
161
153
|
}
|
|
162
154
|
/** Text or attribute value, escaped for any position in an HTML document. */
|
|
163
155
|
export function escapeHtml(value) {
|
|
164
|
-
return value
|
|
165
|
-
.replaceAll("&", "&")
|
|
166
|
-
.replaceAll('"', """)
|
|
167
|
-
.replaceAll("<", "<")
|
|
168
|
-
.replaceAll(">", ">");
|
|
156
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
169
157
|
}
|
|
170
158
|
/**
|
|
171
159
|
* The masthead's `--surface` in each palette, for the browser chrome a
|
|
@@ -184,8 +172,8 @@ function themeColorMeta(branding, theme) {
|
|
|
184
172
|
if (theme.colorScheme !== "system") {
|
|
185
173
|
return `<meta name="theme-color" content="${SURFACE[theme.colorScheme]}">`;
|
|
186
174
|
}
|
|
187
|
-
return `<meta name="theme-color" content="${SURFACE.light}" media="(prefers-color-scheme: light)">\n` +
|
|
188
|
-
`<meta name="theme-color" content="${SURFACE.dark}" media="(prefers-color-scheme: dark)"
|
|
175
|
+
return (`<meta name="theme-color" content="${SURFACE.light}" media="(prefers-color-scheme: light)">\n` +
|
|
176
|
+
`<meta name="theme-color" content="${SURFACE.dark}" media="(prefers-color-scheme: dark)">`);
|
|
189
177
|
}
|
|
190
178
|
/**
|
|
191
179
|
* The one page layout every server-rendered page shares: the operator shell,
|
|
@@ -212,22 +200,16 @@ export function renderPage(branding, layout) {
|
|
|
212
200
|
const owner = brand.ownerName
|
|
213
201
|
? label("brand", brand.ownerName, brand.ownerUrl)
|
|
214
202
|
: label("brand", brand.productName, brand.productUrl);
|
|
215
|
-
const product = brand.ownerName
|
|
216
|
-
? label("product", brand.productName, brand.productUrl)
|
|
217
|
-
: "";
|
|
203
|
+
const product = brand.ownerName ? label("product", brand.productName, brand.productUrl) : "";
|
|
218
204
|
const customIcon = brand.faviconHref !== DEFAULT_FAVICON_HREF;
|
|
219
205
|
// `isSafeIconHref` already limited a relative href to one leading slash, so
|
|
220
206
|
// resolving it can only land on `iconOrigin` itself.
|
|
221
|
-
const iconHref = (href) => layout.iconOrigin && href.startsWith("/")
|
|
222
|
-
? new URL(href, layout.iconOrigin).toString()
|
|
223
|
-
: href;
|
|
207
|
+
const iconHref = (href) => layout.iconOrigin && href.startsWith("/") ? new URL(href, layout.iconOrigin).toString() : href;
|
|
224
208
|
const icons = [
|
|
225
209
|
...(customIcon || layout.uiMounted
|
|
226
210
|
? [`<link rel="icon" href="${escapeHtml(iconHref(brand.faviconHref))}" type="image/svg+xml">`]
|
|
227
211
|
: []),
|
|
228
|
-
...(layout.uiMounted
|
|
229
|
-
? [`<link rel="shortcut icon" href="${escapeHtml(iconHref("/favicon.ico"))}">`]
|
|
230
|
-
: []),
|
|
212
|
+
...(layout.uiMounted ? [`<link rel="shortcut icon" href="${escapeHtml(iconHref("/favicon.ico"))}">`] : []),
|
|
231
213
|
];
|
|
232
214
|
const skip = layout.skipTo
|
|
233
215
|
? `<a class="skip-link" href="#${escapeHtml(layout.skipTo.id)}">${escapeHtml(layout.skipTo.label)}</a>\n`
|
|
@@ -245,7 +227,7 @@ ${icons.join("\n")}
|
|
|
245
227
|
${layout.head ?? ""}
|
|
246
228
|
<style>${styles}${themeCss(brand.theme)}</style>
|
|
247
229
|
</head>
|
|
248
|
-
<body>
|
|
230
|
+
<body${layout.operatorShell ? ' class="operator-shell"' : ""}>
|
|
249
231
|
${skip}<header class="masthead shell">
|
|
250
232
|
<div class="masthead-inner">
|
|
251
233
|
<div class="mast-nav">
|
|
@@ -278,17 +260,15 @@ function wantsHtml(request) {
|
|
|
278
260
|
/**
|
|
279
261
|
* The 404 a browser gets for a path nothing serves, and everyone else's plain
|
|
280
262
|
* `Not Found`. The body is built from deployment configuration alone and names
|
|
281
|
-
* no path, so every unserved path
|
|
282
|
-
* answers with the same bytes for the same `Accept`: a 404 must not tell one
|
|
263
|
+
* no path, so every unserved path answers with the same bytes for the same `Accept`: a 404 must not tell one
|
|
283
264
|
* route from another. The home link appears only when the operator UI is
|
|
284
|
-
* mounted to land on, and points at the public origin
|
|
285
|
-
* 404 does not link to another 404.
|
|
265
|
+
* mounted to land on, and points at the configured public origin.
|
|
286
266
|
*/
|
|
287
267
|
export function notFoundResponse(request, opts) {
|
|
288
268
|
if (!wantsHtml(request)) {
|
|
289
269
|
return new Response("Not Found", { status: 404, headers: { Vary: "Accept" } });
|
|
290
270
|
}
|
|
291
|
-
const brand = resolveBranding(opts.branding);
|
|
271
|
+
const brand = resolveBranding(opts.ui?.branding);
|
|
292
272
|
const uiMounted = Boolean(opts.ui);
|
|
293
273
|
const homeHref = opts.publicUrl ? new URL("/", opts.publicUrl).toString() : "/";
|
|
294
274
|
const home = uiMounted
|
|
@@ -305,7 +285,7 @@ export function notFoundResponse(request, opts) {
|
|
|
305
285
|
${home}
|
|
306
286
|
</section>
|
|
307
287
|
</main>`;
|
|
308
|
-
return new Response(renderPage(opts.branding, {
|
|
288
|
+
return new Response(renderPage(opts.ui?.branding, {
|
|
309
289
|
title: `Page not found — ${brand.pageTitle}`,
|
|
310
290
|
uiMounted,
|
|
311
291
|
// Same bytes on either host: the icons, like the home link, come from
|
|
@@ -314,7 +294,7 @@ export function notFoundResponse(request, opts) {
|
|
|
314
294
|
body,
|
|
315
295
|
}), {
|
|
316
296
|
status: 404,
|
|
317
|
-
headers: { "Content-Type": "text/html; charset=utf-8", Vary: "Accept" },
|
|
297
|
+
headers: htmlSecurityHeaders({ "Content-Type": "text/html; charset=utf-8", Vary: "Accept" }),
|
|
318
298
|
});
|
|
319
299
|
}
|
|
320
300
|
/**
|
|
@@ -326,9 +306,7 @@ export function notFoundResponse(request, opts) {
|
|
|
326
306
|
* from leaving the field alone, and both take the default silently.
|
|
327
307
|
*/
|
|
328
308
|
function isSetValue(value) {
|
|
329
|
-
return typeof value === "string"
|
|
330
|
-
? trimmedString(value) !== undefined
|
|
331
|
-
: value !== undefined && value !== null;
|
|
309
|
+
return typeof value === "string" ? trimmedString(value) !== undefined : value !== undefined && value !== null;
|
|
332
310
|
}
|
|
333
311
|
/**
|
|
334
312
|
* Names of the branding URLs the operator set that failed their gate and were
|
|
@@ -342,16 +320,9 @@ export function droppedBrandingUrls(branding) {
|
|
|
342
320
|
const resolved = resolveBranding(branding);
|
|
343
321
|
const faviconHref = branding.favicon?.href;
|
|
344
322
|
return [
|
|
345
|
-
...(isSetValue(branding.productUrl) && !resolved.productUrl
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
...(isSetValue(branding.ownerUrl) && !resolved.ownerUrl
|
|
349
|
-
? ["ownerUrl"]
|
|
350
|
-
: []),
|
|
351
|
-
...(isSetValue(faviconHref) &&
|
|
352
|
-
trimmedString(faviconHref) !== resolved.faviconHref
|
|
353
|
-
? ["favicon.href"]
|
|
354
|
-
: []),
|
|
323
|
+
...(isSetValue(branding.productUrl) && !resolved.productUrl ? ["productUrl"] : []),
|
|
324
|
+
...(isSetValue(branding.ownerUrl) && !resolved.ownerUrl ? ["ownerUrl"] : []),
|
|
325
|
+
...(isSetValue(faviconHref) && trimmedString(faviconHref) !== resolved.faviconHref ? ["favicon.href"] : []),
|
|
355
326
|
];
|
|
356
327
|
}
|
|
357
328
|
/**
|
|
@@ -443,11 +414,7 @@ export function droppedUiAuthUrls(uiAuth) {
|
|
|
443
414
|
return [];
|
|
444
415
|
return [
|
|
445
416
|
...(isSafeHttpsUrl(uiAuth.frontendApiUrl) ? [] : ["uiAuth.frontendApiUrl"]),
|
|
446
|
-
...(isSetValue(uiAuth.signInUrl) && !isSafeHttpsUrl(uiAuth.signInUrl)
|
|
447
|
-
|
|
448
|
-
: []),
|
|
449
|
-
...(isSetValue(uiAuth.signUpUrl) && !isSafeHttpsUrl(uiAuth.signUpUrl)
|
|
450
|
-
? ["uiAuth.signUpUrl"]
|
|
451
|
-
: []),
|
|
417
|
+
...(isSetValue(uiAuth.signInUrl) && !isSafeHttpsUrl(uiAuth.signInUrl) ? ["uiAuth.signInUrl"] : []),
|
|
418
|
+
...(isSetValue(uiAuth.signUpUrl) && !isSafeHttpsUrl(uiAuth.signUpUrl) ? ["uiAuth.signUpUrl"] : []),
|
|
452
419
|
];
|
|
453
420
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Web-API only, like the rest of the core: Node and workerd read a body alike.
|
|
2
|
+
const decoder = new TextDecoder();
|
|
3
|
+
/**
|
|
4
|
+
* A downstream's response whose `text()` and `json()` read bytes.
|
|
5
|
+
*
|
|
6
|
+
* workerd, when `.text()` (and `.json()`, which reads text) meets a body whose
|
|
7
|
+
* Content-Type it does not parse as text, prints a native warning quoting that
|
|
8
|
+
* Content-Type whole: below every console and logger, so a downstream could
|
|
9
|
+
* write into the deployment's log (INV-6, #695). No filter on the header can
|
|
10
|
+
* agree with workerd's own parser for every value a downstream can send, so
|
|
11
|
+
* connecta never asks for text: `arrayBuffer()` carries no such warning, and
|
|
12
|
+
* the bytes are decoded here exactly as `text()` would decode them (UTF-8,
|
|
13
|
+
* BOM stripped, malformed sequences replaced).
|
|
14
|
+
*
|
|
15
|
+
* The response itself is returned, its readers shadowed by own properties, so
|
|
16
|
+
* `status`, `headers`, `url`, `body`, and `instanceof Response` are unchanged,
|
|
17
|
+
* and so is everything an SDK or a handler reads. A clone is shadowed too:
|
|
18
|
+
* the pinned SDK reads a token response's `clone().json()`.
|
|
19
|
+
*/
|
|
20
|
+
export function byteReadResponse(response) {
|
|
21
|
+
if (!Object.isExtensible(response)) {
|
|
22
|
+
// A custom fetch that froze its response: shadow a copy instead, which
|
|
23
|
+
// keeps everything but `url` and `redirected`.
|
|
24
|
+
return byteReadResponse(new Response(response.body, {
|
|
25
|
+
status: response.status,
|
|
26
|
+
statusText: response.statusText,
|
|
27
|
+
headers: response.headers,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
const arrayBuffer = response.arrayBuffer.bind(response);
|
|
31
|
+
const clone = response.clone.bind(response);
|
|
32
|
+
const text = async () => decoder.decode(await arrayBuffer());
|
|
33
|
+
const own = (value) => ({ value, configurable: true, writable: true });
|
|
34
|
+
return Object.defineProperties(response, {
|
|
35
|
+
text: own(text),
|
|
36
|
+
json: own(async () => JSON.parse(await text())),
|
|
37
|
+
clone: own(() => byteReadResponse(clone())),
|
|
38
|
+
});
|
|
39
|
+
}
|
package/dist/call-admission.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Deferred, Duration, Effect } from "effect";
|
|
2
2
|
import { ConnectorCallError } from "./errors.js";
|
|
3
|
-
import { provideCallAdmissionProgram, startCallAdmission
|
|
3
|
+
import { provideCallAdmissionProgram, startCallAdmission } from "./runtime/call-admission.js";
|
|
4
4
|
import { fromSignal, runEdge } from "./runtime/run.js";
|
|
5
5
|
const DEFAULT_MAX_QUEUE_SIZE = 32;
|
|
6
6
|
const DEFAULT_QUEUE_TIMEOUT_MS = 5_000;
|
|
@@ -28,7 +28,11 @@ export function isCallAdmissionError(error) {
|
|
|
28
28
|
/** Sum gauges and counters without publishing connector or principal keys. */
|
|
29
29
|
export function aggregateCallAdmissionSnapshots(snapshots) {
|
|
30
30
|
const aggregate = {
|
|
31
|
-
rules: 0,
|
|
31
|
+
rules: 0,
|
|
32
|
+
partitions: 0,
|
|
33
|
+
active: 0,
|
|
34
|
+
queued: 0,
|
|
35
|
+
closed: snapshots.length > 0,
|
|
32
36
|
totals: { admitted: 0, queued: 0, rejected: 0, rateLimited: 0, cancelled: 0 },
|
|
33
37
|
queueWaitMs: { count: 0, total: 0, max: 0 },
|
|
34
38
|
};
|
|
@@ -88,8 +92,7 @@ export class ConnectorCallAdmissionController {
|
|
|
88
92
|
throw new TypeError(`connector "${connectorId}" callAdmission.rules must contain exactly one rule in this release.`);
|
|
89
93
|
}
|
|
90
94
|
const rule = policy.rules[0];
|
|
91
|
-
if (rule.maxConcurrency === undefined &&
|
|
92
|
-
rule.budget === undefined) {
|
|
95
|
+
if (rule.maxConcurrency === undefined && rule.budget === undefined) {
|
|
93
96
|
throw new TypeError(`connector "${connectorId}" callAdmission rule must declare maxConcurrency or budget.`);
|
|
94
97
|
}
|
|
95
98
|
this.maxConcurrency =
|
|
@@ -97,9 +100,7 @@ export class ConnectorCallAdmissionController {
|
|
|
97
100
|
? undefined
|
|
98
101
|
: positiveWhole(rule.maxConcurrency, `connector "${connectorId}" callAdmission maxConcurrency`);
|
|
99
102
|
if (this.maxConcurrency === undefined &&
|
|
100
|
-
(rule.maxQueueSize !== undefined ||
|
|
101
|
-
rule.queueTimeoutMs !== undefined ||
|
|
102
|
-
rule.retryAfterMs !== undefined)) {
|
|
103
|
+
(rule.maxQueueSize !== undefined || rule.queueTimeoutMs !== undefined || rule.retryAfterMs !== undefined)) {
|
|
103
104
|
throw new TypeError(`connector "${connectorId}" callAdmission queue settings require maxConcurrency.`);
|
|
104
105
|
}
|
|
105
106
|
this.maxQueueSize = nonNegativeWhole(rule.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, `connector "${connectorId}" callAdmission maxQueueSize`);
|
|
@@ -193,8 +194,7 @@ export class ConnectorCallAdmissionController {
|
|
|
193
194
|
pump(state) {
|
|
194
195
|
if (this.closed || this.maxConcurrency === undefined)
|
|
195
196
|
return;
|
|
196
|
-
while (state.active < this.maxConcurrency &&
|
|
197
|
-
state.waiters.length > 0) {
|
|
197
|
+
while (state.active < this.maxConcurrency && state.waiters.length > 0) {
|
|
198
198
|
const waiter = state.waiters.shift();
|
|
199
199
|
// The waiter's signal is not read here. This runs in whichever request
|
|
200
200
|
// released a slot, and on Workers reading an AbortSignal that another
|
|
@@ -248,9 +248,7 @@ export class ConnectorCallAdmissionController {
|
|
|
248
248
|
}
|
|
249
249
|
maybeDeletePartition(key, state, now) {
|
|
250
250
|
this.pruneBudget(state, now);
|
|
251
|
-
if (state.active === 0 &&
|
|
252
|
-
state.waiters.length === 0 &&
|
|
253
|
-
state.admittedAt.length === 0) {
|
|
251
|
+
if (state.active === 0 && state.waiters.length === 0 && state.admittedAt.length === 0) {
|
|
254
252
|
this.partitions.delete(key);
|
|
255
253
|
}
|
|
256
254
|
}
|
|
@@ -322,8 +320,7 @@ export class ConnectorCallAdmissionController {
|
|
|
322
320
|
controller.rejectedTotal++;
|
|
323
321
|
return Effect.fail(new CallAdmissionError("partition", "connector_call_failed", `Connector "${controller.connectorId}" call-admission partitionKey threw.`, { cause }));
|
|
324
322
|
}
|
|
325
|
-
if (typeof key !== "string" ||
|
|
326
|
-
enc.encode(key).length > MAX_PARTITION_KEY_BYTES) {
|
|
323
|
+
if (typeof key !== "string" || enc.encode(key).length > MAX_PARTITION_KEY_BYTES) {
|
|
327
324
|
controller.rejectedTotal++;
|
|
328
325
|
return Effect.fail(new CallAdmissionError("partition", "connector_call_failed", `Connector "${controller.connectorId}" call-admission partitionKey must return a string of at most ${MAX_PARTITION_KEY_BYTES} UTF-8 bytes.`));
|
|
329
326
|
}
|
|
@@ -352,8 +349,7 @@ export class ConnectorCallAdmissionController {
|
|
|
352
349
|
controller.rateLimitedTotal++;
|
|
353
350
|
return Effect.fail(controller.budgetLimited(budgetRetryAfterMs));
|
|
354
351
|
}
|
|
355
|
-
if (controller.maxConcurrency === undefined ||
|
|
356
|
-
state.active < controller.maxConcurrency) {
|
|
352
|
+
if (controller.maxConcurrency === undefined || state.active < controller.maxConcurrency) {
|
|
357
353
|
return Effect.succeed(controller.admit(state, queuedAt, 0));
|
|
358
354
|
}
|
|
359
355
|
if (state.waiters.length >= controller.maxQueueSize) {
|