@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,335 @@
|
|
|
1
|
+
// Deterministic inverse RFC 6570 expansion for advertised resource templates.
|
|
2
|
+
// Each expression consumes up to the leftmost following literal. Ambiguous
|
|
3
|
+
// boundaries are refused rather than searched again with another capture.
|
|
4
|
+
const MAX_URI_LENGTH = 8192;
|
|
5
|
+
const MAX_MATCH_WORK = 262_144;
|
|
6
|
+
/** Bound parsing plus templates × URI length for the whole resource read. */
|
|
7
|
+
export function resourceUriMatchesTemplates(uri, templates, note = () => { }) {
|
|
8
|
+
let work = 0;
|
|
9
|
+
for (const template of templates) {
|
|
10
|
+
work += uri.length + template.uriTemplate.length;
|
|
11
|
+
if (work > MAX_MATCH_WORK) {
|
|
12
|
+
note("resource_match_budget_exceeded");
|
|
13
|
+
return { matched: false, refusal: "resource_match_budget_exceeded" };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
let matched = false;
|
|
17
|
+
let refusal;
|
|
18
|
+
for (const template of templates) {
|
|
19
|
+
const parsed = parse(template.uriTemplate);
|
|
20
|
+
if (parsed === "resource_template_ambiguous") {
|
|
21
|
+
note(parsed);
|
|
22
|
+
refusal = parsed;
|
|
23
|
+
}
|
|
24
|
+
else if (parsed && matches(uri, parsed))
|
|
25
|
+
matched = true;
|
|
26
|
+
}
|
|
27
|
+
return matched ? { matched } : { matched, ...(refusal ? { refusal } : {}) };
|
|
28
|
+
}
|
|
29
|
+
export function resourceUriMatchesTemplate(uri, template) {
|
|
30
|
+
const parsed = parse(template);
|
|
31
|
+
return typeof parsed === "object" && matches(uri, parsed);
|
|
32
|
+
}
|
|
33
|
+
function parse(template) {
|
|
34
|
+
if (template.length > MAX_URI_LENGTH)
|
|
35
|
+
return;
|
|
36
|
+
const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(template)?.[0];
|
|
37
|
+
if (!scheme)
|
|
38
|
+
return;
|
|
39
|
+
const authority = literalAuthority(template.slice(scheme.length));
|
|
40
|
+
if (authority !== undefined && /[{}]/.test(authority))
|
|
41
|
+
return;
|
|
42
|
+
const parts = [];
|
|
43
|
+
let offset = 0;
|
|
44
|
+
let count = 0;
|
|
45
|
+
while (offset < template.length) {
|
|
46
|
+
const start = template.indexOf("{", offset);
|
|
47
|
+
if (start < 0)
|
|
48
|
+
break;
|
|
49
|
+
const literal = template.slice(offset, start);
|
|
50
|
+
const end = template.indexOf("}", start + 1);
|
|
51
|
+
if (literal.includes("}") || end < 0 || ++count > 16)
|
|
52
|
+
return;
|
|
53
|
+
const body = template.slice(start + 1, end);
|
|
54
|
+
const operator = /^[+#./;?&]/.test(body) ? body[0] : "";
|
|
55
|
+
const specs = body.slice(operator.length).split(",");
|
|
56
|
+
if (specs.length > 16)
|
|
57
|
+
return;
|
|
58
|
+
const variables = [];
|
|
59
|
+
for (const spec of specs) {
|
|
60
|
+
const value = /^((?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+)*)(\*|:[1-9][0-9]{0,3})?$/.exec(spec);
|
|
61
|
+
if (!value)
|
|
62
|
+
return;
|
|
63
|
+
variables.push({
|
|
64
|
+
name: value[1],
|
|
65
|
+
explode: value[2] === "*",
|
|
66
|
+
prefix: value[2]?.startsWith(":") ? Number(value[2].slice(1)) : undefined,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (new Set(variables.map((value) => value.name)).size !== variables.length ||
|
|
70
|
+
(variables.length > 1 && variables.some((value) => value.explode)))
|
|
71
|
+
return;
|
|
72
|
+
parts.push(literal, { operator, variables });
|
|
73
|
+
offset = end + 1;
|
|
74
|
+
}
|
|
75
|
+
const tail = template.slice(offset);
|
|
76
|
+
if (/[{}]/.test(tail))
|
|
77
|
+
return;
|
|
78
|
+
parts.push(tail);
|
|
79
|
+
for (let index = 1; index < parts.length; index += 2) {
|
|
80
|
+
const expression = parts[index];
|
|
81
|
+
const literal = parts[index + 1];
|
|
82
|
+
if ((!literal && index + 2 < parts.length) ||
|
|
83
|
+
(literal && Array.from(literal).every((character) => expressionCharacter(expression, character)))) {
|
|
84
|
+
return "resource_template_ambiguous";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { parts, scheme, authority };
|
|
88
|
+
}
|
|
89
|
+
function literalAuthority(rest) {
|
|
90
|
+
if (!rest.startsWith("//"))
|
|
91
|
+
return;
|
|
92
|
+
const value = rest.slice(2);
|
|
93
|
+
const end = value.search(/[/?#]|\{[/?#]/);
|
|
94
|
+
return end < 0 ? value : value.slice(0, end);
|
|
95
|
+
}
|
|
96
|
+
function expressionCharacter({ operator, variables }, character) {
|
|
97
|
+
const base = operator === "+" || operator === "#"
|
|
98
|
+
? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.~!$'()*+@-/,%"
|
|
99
|
+
: operator === "."
|
|
100
|
+
? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~-%"
|
|
101
|
+
: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.~-%";
|
|
102
|
+
const syntax = operator === ";"
|
|
103
|
+
? ";=,"
|
|
104
|
+
: operator === "?" || operator === "&"
|
|
105
|
+
? operator + "&=,"
|
|
106
|
+
: operator === "/" || operator === "."
|
|
107
|
+
? operator + ","
|
|
108
|
+
: ",";
|
|
109
|
+
return (base.includes(character) || syntax.includes(character) || variables.some((value) => value.name.includes(character)));
|
|
110
|
+
}
|
|
111
|
+
// KMP gives a single forward scan even when a literal has repeated prefixes.
|
|
112
|
+
function findLiteral(uri, literal, offset) {
|
|
113
|
+
const failure = Array.from({ length: literal.length }, () => 0);
|
|
114
|
+
for (let index = 1, prefix = 0; index < literal.length; index++) {
|
|
115
|
+
while (prefix && literal[index] !== literal[prefix])
|
|
116
|
+
prefix = failure[prefix - 1];
|
|
117
|
+
if (literal[index] === literal[prefix])
|
|
118
|
+
prefix++;
|
|
119
|
+
failure[index] = prefix;
|
|
120
|
+
}
|
|
121
|
+
for (let index = offset, prefix = 0; index < uri.length; index++) {
|
|
122
|
+
while (prefix && uri[index] !== literal[prefix])
|
|
123
|
+
prefix = failure[prefix - 1];
|
|
124
|
+
if (uri[index] === literal[prefix])
|
|
125
|
+
prefix++;
|
|
126
|
+
if (prefix === literal.length)
|
|
127
|
+
return index + 1 - prefix;
|
|
128
|
+
}
|
|
129
|
+
return -1;
|
|
130
|
+
}
|
|
131
|
+
function matches(uri, { parts, scheme, authority }) {
|
|
132
|
+
if (uri.length > MAX_URI_LENGTH ||
|
|
133
|
+
!uri.startsWith(scheme) ||
|
|
134
|
+
(authority === undefined && uri.slice(scheme.length).startsWith("//")))
|
|
135
|
+
return false;
|
|
136
|
+
const values = new Map();
|
|
137
|
+
const capture = (variable, raw, path) => {
|
|
138
|
+
const decoded = raw.map((value) => safeValue(value, variable.prefix, path));
|
|
139
|
+
if (decoded.some((value) => value === undefined))
|
|
140
|
+
return false;
|
|
141
|
+
const prior = values.get(variable.name) ?? [];
|
|
142
|
+
prior.push({ parts: decoded, prefix: variable.prefix });
|
|
143
|
+
values.set(variable.name, prior);
|
|
144
|
+
return true;
|
|
145
|
+
};
|
|
146
|
+
let offset = 0;
|
|
147
|
+
for (let index = 0; index < parts.length; index++) {
|
|
148
|
+
const part = parts[index];
|
|
149
|
+
if (typeof part === "string") {
|
|
150
|
+
if (!uri.startsWith(part, offset))
|
|
151
|
+
return false;
|
|
152
|
+
offset += part.length;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const literal = parts[index + 1];
|
|
156
|
+
const end = literal ? findLiteral(uri, literal, offset) : uri.length;
|
|
157
|
+
if (end < 0)
|
|
158
|
+
return false;
|
|
159
|
+
const raw = uri.slice(offset, end);
|
|
160
|
+
offset = end;
|
|
161
|
+
if (!raw)
|
|
162
|
+
continue;
|
|
163
|
+
const { operator, variables } = part;
|
|
164
|
+
const prefix = operator === "+" ? "" : operator;
|
|
165
|
+
if (!raw.startsWith(prefix))
|
|
166
|
+
return false;
|
|
167
|
+
const body = raw.slice(prefix.length);
|
|
168
|
+
if (operator === "?" || operator === "&" || operator === ";") {
|
|
169
|
+
const tokens = body.split(operator === ";" ? ";" : "&");
|
|
170
|
+
let last = -1;
|
|
171
|
+
for (const token of tokens) {
|
|
172
|
+
const equal = token.indexOf("=");
|
|
173
|
+
const name = equal < 0 ? token : token.slice(0, equal);
|
|
174
|
+
const next = variables.findIndex((value) => value.name === name);
|
|
175
|
+
if (next <= last || next < 0 || (equal < 0 && operator !== ";"))
|
|
176
|
+
return false;
|
|
177
|
+
last = next;
|
|
178
|
+
const variable = variables[next];
|
|
179
|
+
const value = equal < 0 ? "" : token.slice(equal + 1);
|
|
180
|
+
if (!capture(variable, variable.prefix === undefined ? value.split(",") : [value], false))
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const separator = operator === "/" || operator === "." ? operator : ",";
|
|
186
|
+
if (operator === "." && body.includes(".."))
|
|
187
|
+
return false;
|
|
188
|
+
const tokens = body.split(separator);
|
|
189
|
+
if (operator === "/" && tokens.length > 1 && tokens.some((value) => !value))
|
|
190
|
+
return false;
|
|
191
|
+
if (variables.length === 1) {
|
|
192
|
+
const variable = variables[0];
|
|
193
|
+
const list = variable.prefix === undefined && (variable.explode || (operator !== "/" && operator !== "."));
|
|
194
|
+
if ((!list && tokens.length > 1) ||
|
|
195
|
+
!capture(variable, list ? tokens : [body], operator === "+" || operator === "#"))
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
if (tokens.length > variables.length)
|
|
200
|
+
return false;
|
|
201
|
+
for (const [i, value] of tokens.entries())
|
|
202
|
+
if (!capture(variables[i], [value], operator === "+" || operator === "#"))
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (offset !== uri.length)
|
|
208
|
+
return false;
|
|
209
|
+
const candidates = new Map();
|
|
210
|
+
for (const [name, occurrences] of values) {
|
|
211
|
+
const candidate = occurrences.find((value) => value.prefix === undefined || Array.from(value.parts[0]).length < value.prefix) ??
|
|
212
|
+
occurrences.reduce((a, b) => (a.parts[0].length >= b.parts[0].length ? a : b));
|
|
213
|
+
if (!occurrences.every((value) => value.prefix === undefined
|
|
214
|
+
? JSON.stringify(value.parts) === JSON.stringify(candidate.parts)
|
|
215
|
+
: value.parts.length === 1 &&
|
|
216
|
+
value.parts[0] === Array.from(candidate.parts[0]).slice(0, value.prefix).join("")))
|
|
217
|
+
return false;
|
|
218
|
+
candidates.set(name, candidate.parts);
|
|
219
|
+
}
|
|
220
|
+
const expanded = parts.map((part) => (typeof part === "string" ? part : expand(part, candidates))).join("");
|
|
221
|
+
const canonical = (text) => text.replace(/%[0-9a-f]{2}/gi, (value) => value.toUpperCase());
|
|
222
|
+
// Empty expressions can join literals into an authority the template did
|
|
223
|
+
// not advertise. Compare the final RFC 3986 authority after re-expansion.
|
|
224
|
+
const rest = uri.slice(scheme.length);
|
|
225
|
+
return (canonical(expanded) === canonical(uri) &&
|
|
226
|
+
literalAuthority(rest) === authority &&
|
|
227
|
+
sameHost(uri, scheme, authority) &&
|
|
228
|
+
safePath(authority === undefined ? rest : rest.slice(authority.length + 2)));
|
|
229
|
+
}
|
|
230
|
+
function sameHost(uri, scheme, authority) {
|
|
231
|
+
// WHATWG special schemes can turn an RFC 3986 path into a host. A literal
|
|
232
|
+
// authority supplies the expected host, including normal port/IDNA handling;
|
|
233
|
+
// without one, WHATWG must also find no host. Parser errors stay local.
|
|
234
|
+
if (authority !== undefined && /[\\\p{Cc}\p{Cf}]/u.test(authority))
|
|
235
|
+
return false;
|
|
236
|
+
try {
|
|
237
|
+
const host = authority === undefined ? "" : new URL(`${scheme}//${authority}/`).host;
|
|
238
|
+
return new URL(uri).host === host;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function safePath(path) {
|
|
245
|
+
// Inspect the RFC 3986 path before WHATWG removes dot segments. Empty
|
|
246
|
+
// captures can combine with literals to make traversal absent from any
|
|
247
|
+
// individual value. Queries and fragments do not contribute path segments.
|
|
248
|
+
const end = path.search(/[?#]/);
|
|
249
|
+
let value = end < 0 ? path : path.slice(0, end);
|
|
250
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
251
|
+
if (/[\\\p{Cc}\p{Cf}]/u.test(value) || unsafeUnicode(value) || /(?:^|\/)\.{1,2}(?:\/|$)/.test(value))
|
|
252
|
+
return false;
|
|
253
|
+
if (!value.includes("%") || (depth > 0 && !/%[0-9a-f]{2}/i.test(value)))
|
|
254
|
+
return true;
|
|
255
|
+
try {
|
|
256
|
+
value = depth === 0 ? decodeURIComponent(value) : decodeNestedPercentEscapes(value);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
function expand({ operator, variables }, values) {
|
|
265
|
+
const named = operator === ";" || operator === "?" || operator === "&";
|
|
266
|
+
const prefix = operator === "+" ? "" : operator;
|
|
267
|
+
const separator = operator === "/" || operator === "." || operator === ";" ? operator : named ? "&" : ",";
|
|
268
|
+
const encode = (value) => operator === "+" || operator === "#"
|
|
269
|
+
? value
|
|
270
|
+
.split(/(%[0-9a-f]{2})/gi)
|
|
271
|
+
.map((part) => (/^%[0-9a-f]{2}$/i.test(part) ? part : encodeURI(part)))
|
|
272
|
+
.join("")
|
|
273
|
+
: encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
274
|
+
const nameValue = (name, value) => named ? name + (value || operator !== ";" ? `=${value}` : "") : value;
|
|
275
|
+
const expansions = [];
|
|
276
|
+
for (const variable of variables) {
|
|
277
|
+
const parts = values.get(variable.name);
|
|
278
|
+
if (!parts)
|
|
279
|
+
continue;
|
|
280
|
+
const encoded = parts.map((value) => encode(variable.prefix === undefined ? value : Array.from(value).slice(0, variable.prefix).join("")));
|
|
281
|
+
expansions.push(variable.explode && parts.length > 1
|
|
282
|
+
? encoded.map((value) => nameValue(variable.name, value)).join(separator)
|
|
283
|
+
: nameValue(variable.name, encoded.join(",")));
|
|
284
|
+
}
|
|
285
|
+
return expansions.length ? prefix + expansions.join(separator) : "";
|
|
286
|
+
}
|
|
287
|
+
function safeValue(raw, prefix, path) {
|
|
288
|
+
let expanded;
|
|
289
|
+
try {
|
|
290
|
+
expanded = decodeURIComponent(raw);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
let value = expanded;
|
|
296
|
+
// Check each decoding layer. Reserved paths may contain slashes, but cannot
|
|
297
|
+
// introduce URI syntax, a network path, traversal, controls or format marks.
|
|
298
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
299
|
+
if (/[\\:?#&;=\p{Cc}\p{Cf}]/u.test(value) ||
|
|
300
|
+
unsafeUnicode(value) ||
|
|
301
|
+
(!path && value.includes("/")) ||
|
|
302
|
+
value.startsWith("/") ||
|
|
303
|
+
value.split("/").some((segment) => segment === "." || segment === ".."))
|
|
304
|
+
return undefined;
|
|
305
|
+
if (!/%[0-9a-f]{2}/i.test(value))
|
|
306
|
+
return prefix === undefined || Array.from(expanded).length <= prefix ? expanded : undefined;
|
|
307
|
+
try {
|
|
308
|
+
value = decodeNestedPercentEscapes(value);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
function decodeNestedPercentEscapes(value) {
|
|
317
|
+
// The first decode stays strict. Later layers can contain literal percent
|
|
318
|
+
// signs decoded from %25. Preserve those while decoding every remaining
|
|
319
|
+
// %HH escape, so literal data cannot hide unsafe escapes elsewhere. Invalid
|
|
320
|
+
// UTF-8 still throws and the callers keep the same eight-layer bound.
|
|
321
|
+
return decodeURIComponent(value.replace(/%(?![0-9a-f]{2})/gi, "%25"));
|
|
322
|
+
}
|
|
323
|
+
function unsafeUnicode(value) {
|
|
324
|
+
// Refuse non-ASCII separators and compatibility lookalikes of URI syntax,
|
|
325
|
+
// rather than all non-ASCII. Ordinary percent-encoded UTF-8 path segments
|
|
326
|
+
// and ASCII spaces remain valid. Inspect every decoding layer without
|
|
327
|
+
// normalizing the value used for expansion.
|
|
328
|
+
for (const character of value) {
|
|
329
|
+
if (character <= "\x7f")
|
|
330
|
+
continue;
|
|
331
|
+
if (/\p{Z}/u.test(character) || /[\\/:?#&;=.%@]/.test(character.normalize("NFKC")))
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
@@ -27,6 +27,16 @@ export interface CredentialVault {
|
|
|
27
27
|
* Without both `seal` and `open`, that state is stored as plaintext.
|
|
28
28
|
*/
|
|
29
29
|
seal?(connectorId: string, purpose: string, plaintext: string, owner?: string): Promise<string>;
|
|
30
|
+
/** Sign browser OAuth handoffs with a purpose-specific key; nothing is stored. */
|
|
31
|
+
signOAuthHandoff?(payload: string): Promise<string>;
|
|
32
|
+
/** Verify a browser OAuth handoff signature without exposing the key. */
|
|
33
|
+
verifyOAuthHandoff?(payload: string, signature: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Host-only key for the MCP request-state codec. Must be a stable,
|
|
36
|
+
* purpose-specific deployment key of at least 32 bytes. The registry never
|
|
37
|
+
* exposes the vault or this key through a connector context or guest API.
|
|
38
|
+
*/
|
|
39
|
+
requestStateKey?(): Promise<Uint8Array>;
|
|
30
40
|
/** Reverse `seal` for the same connector, purpose, and owner; rejects otherwise. */
|
|
31
41
|
open?(connectorId: string, purpose: string, sealed: string, owner?: string): Promise<string>;
|
|
32
42
|
}
|
package/dist/credential-rules.js
CHANGED
|
@@ -25,9 +25,7 @@ export const STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR = "Stored credential fields
|
|
|
25
25
|
export function storedCredentialShape(config, stored) {
|
|
26
26
|
if (!stored)
|
|
27
27
|
return { state: "missing" };
|
|
28
|
-
const mode = config.fields?.length
|
|
29
|
-
? "multiple"
|
|
30
|
-
: "single";
|
|
28
|
+
const mode = config.fields?.length ? "multiple" : "single";
|
|
31
29
|
const declared = new Set(mode === "multiple" ? config.fields.map((field) => field.name) : ["value"]);
|
|
32
30
|
const actual = Object.keys(stored);
|
|
33
31
|
const present = new Set(actual);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ConnectorCallError } from "./errors.js";
|
|
2
|
+
function refused() {
|
|
3
|
+
throw new ConnectorCallError("connector_call_failed", "OAuth metadata or consent URL cannot be safely checked; refusing authorization.", { retryable: false });
|
|
4
|
+
}
|
|
5
|
+
/** Matching only: never change the endpoint that discovery supplied. */
|
|
6
|
+
export function percentDecoded(value) {
|
|
7
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
8
|
+
const next = value.replace(/(?:%[0-9a-f]{2})+/gi, (escaped) => {
|
|
9
|
+
try {
|
|
10
|
+
return decodeURIComponent(escaped);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// Replacement decoding preserves valid UTF-8 beside malformed bytes.
|
|
14
|
+
const bytes = escaped.match(/%[0-9a-f]{2}/gi);
|
|
15
|
+
return new TextDecoder().decode(Uint8Array.from(bytes, (byte) => parseInt(byte.slice(1), 16)));
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
if (next === value)
|
|
19
|
+
return value;
|
|
20
|
+
value = next;
|
|
21
|
+
}
|
|
22
|
+
// Deeper nesting has no stable matching view within the normalization bound.
|
|
23
|
+
if (/%[0-9a-f]{2}/i.test(value))
|
|
24
|
+
refused();
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
/** RFC 3492 decoding, using no Node-only URL or punycode implementation. URL
|
|
28
|
+
* has already validated the ASCII host. Bound arithmetic and Unicode scalars. */
|
|
29
|
+
function unicodeLabel(label) {
|
|
30
|
+
if (!label.startsWith("xn--"))
|
|
31
|
+
return label;
|
|
32
|
+
const input = label.slice(4);
|
|
33
|
+
const delimiter = input.lastIndexOf("-");
|
|
34
|
+
const output = delimiter < 0 ? [] : Array.from(input.slice(0, delimiter), (char) => char.charCodeAt(0));
|
|
35
|
+
let cursor = delimiter < 0 ? 0 : delimiter + 1;
|
|
36
|
+
let n = 128;
|
|
37
|
+
let i = 0;
|
|
38
|
+
let bias = 72;
|
|
39
|
+
while (cursor < input.length) {
|
|
40
|
+
const previous = i;
|
|
41
|
+
let weight = 1;
|
|
42
|
+
for (let k = 36;; k += 36) {
|
|
43
|
+
const code = input.charCodeAt(cursor++);
|
|
44
|
+
const digit = code >= 97 && code <= 122 ? code - 97 : code >= 48 && code <= 57 ? code - 22 : 36;
|
|
45
|
+
if (digit >= 36)
|
|
46
|
+
refused();
|
|
47
|
+
i += digit * weight;
|
|
48
|
+
if (!Number.isSafeInteger(i))
|
|
49
|
+
refused();
|
|
50
|
+
const threshold = k <= bias ? 1 : k >= bias + 26 ? 26 : k - bias;
|
|
51
|
+
if (digit < threshold)
|
|
52
|
+
break;
|
|
53
|
+
weight *= 36 - threshold;
|
|
54
|
+
if (!Number.isSafeInteger(weight))
|
|
55
|
+
refused();
|
|
56
|
+
}
|
|
57
|
+
const count = output.length + 1;
|
|
58
|
+
let delta = previous === 0 ? Math.floor((i - previous) / 700) : Math.floor((i - previous) / 2);
|
|
59
|
+
delta += Math.floor(delta / count);
|
|
60
|
+
let k = 0;
|
|
61
|
+
while (delta > 455) {
|
|
62
|
+
delta = Math.floor(delta / 35);
|
|
63
|
+
k += 36;
|
|
64
|
+
}
|
|
65
|
+
bias = k + Math.floor((36 * delta) / (delta + 38));
|
|
66
|
+
n += Math.floor(i / count);
|
|
67
|
+
if (n > 0x10ffff || (n >= 0xd800 && n <= 0xdfff))
|
|
68
|
+
refused();
|
|
69
|
+
i %= count;
|
|
70
|
+
output.splice(i++, 0, n);
|
|
71
|
+
}
|
|
72
|
+
return String.fromCodePoint(...output);
|
|
73
|
+
}
|
|
74
|
+
/** Decode each component, and concatenate with and without query names so a
|
|
75
|
+
* credential split across path/query boundaries still matches. Hosts have
|
|
76
|
+
* ASCII and Unicode views and match case-insensitively; paths remain exact. */
|
|
77
|
+
export function credentialUrlViews(value) {
|
|
78
|
+
let url;
|
|
79
|
+
try {
|
|
80
|
+
url = new URL(value);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return refused();
|
|
84
|
+
}
|
|
85
|
+
const host = url.hostname.toLowerCase();
|
|
86
|
+
const hosts = [host, host.split(".").map(unicodeLabel).join(".").toLowerCase()];
|
|
87
|
+
const path = url.pathname.split("/").map(percentDecoded);
|
|
88
|
+
const query = [...url.searchParams].map(([key, item]) => [percentDecoded(key), percentDecoded(item)]);
|
|
89
|
+
const prefix = [url.protocol, percentDecoded(url.username), percentDecoded(url.password)];
|
|
90
|
+
const suffix = [percentDecoded(url.hash.slice(1))];
|
|
91
|
+
const components = [
|
|
92
|
+
value,
|
|
93
|
+
percentDecoded(url.href),
|
|
94
|
+
...prefix,
|
|
95
|
+
...hosts,
|
|
96
|
+
url.port,
|
|
97
|
+
percentDecoded(url.pathname),
|
|
98
|
+
percentDecoded(url.search),
|
|
99
|
+
...path,
|
|
100
|
+
...query.flat(),
|
|
101
|
+
...suffix,
|
|
102
|
+
];
|
|
103
|
+
const joined = hosts.flatMap((hostname) => [
|
|
104
|
+
[...prefix, hostname, url.port, ...path, ...query.flat(), ...suffix].join(""),
|
|
105
|
+
[...prefix, hostname, url.port, ...path, ...query.map((entry) => entry[1]), ...suffix].join(""),
|
|
106
|
+
]);
|
|
107
|
+
return { hosts, components, joined };
|
|
108
|
+
}
|
package/dist/credentials.d.ts
CHANGED
|
@@ -2,14 +2,19 @@ import type { CredentialMetadata, CredentialVault as Vault } from "./credential-
|
|
|
2
2
|
import type { ConnectorCredentialValues, KVStorage } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Encrypted, connector-scoped credential vault over the deployment's existing
|
|
5
|
-
* KVStorage. Only ciphertext enters
|
|
5
|
+
* KVStorage. Only ciphertext enters storage; the AES-GCM key remains an environment
|
|
6
6
|
* secret outside the store. The same key seals downstream OAuth state, which
|
|
7
7
|
* the OAuth provider stores itself.
|
|
8
8
|
*/
|
|
9
9
|
export declare class CredentialVault implements Vault {
|
|
10
10
|
private readonly storage;
|
|
11
11
|
private readonly key;
|
|
12
|
+
private readonly handoffKey;
|
|
13
|
+
private readonly retryKey;
|
|
12
14
|
constructor(storage: KVStorage, encryptionKey: string);
|
|
15
|
+
signOAuthHandoff(payload: string): Promise<string>;
|
|
16
|
+
requestStateKey(): Promise<Uint8Array>;
|
|
17
|
+
verifyOAuthHandoff(payload: string, signature: string): Promise<boolean>;
|
|
13
18
|
private additionalData;
|
|
14
19
|
private read;
|
|
15
20
|
get(connectorId: string, field?: string, owner?: string): Promise<string | null>;
|
package/dist/credentials.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { deriveOAuthHandoffKey, deriveRequestStateKey } from "./oauth-sealing.js";
|
|
2
|
+
import { credentialKeys } from "./storage/keys.js";
|
|
1
3
|
const KEY_BYTES = 32;
|
|
2
4
|
const IV_BYTES = 12;
|
|
3
5
|
const MAX_CREDENTIAL_BYTES = 16_384;
|
|
@@ -14,9 +16,7 @@ function oauthStateAdditionalData(connectorId, purpose, owner) {
|
|
|
14
16
|
return encoder.encode(`connecta:oauth-state:v1|${connectorId}|${owner ?? ""}|${purpose}`);
|
|
15
17
|
}
|
|
16
18
|
function storageKey(connectorId, owner) {
|
|
17
|
-
return owner
|
|
18
|
-
? `principal:${owner}:conn:${connectorId}:credential:v1`
|
|
19
|
-
: `conn:${connectorId}:credential:v1`;
|
|
19
|
+
return credentialKeys.credential(connectorId, owner);
|
|
20
20
|
}
|
|
21
21
|
function bytesToBase64(bytes) {
|
|
22
22
|
let binary = "";
|
|
@@ -52,14 +52,11 @@ function parseEnvelope(raw) {
|
|
|
52
52
|
function parsePlaintext(raw) {
|
|
53
53
|
try {
|
|
54
54
|
const parsed = JSON.parse(raw);
|
|
55
|
-
const values = typeof parsed.value === "string"
|
|
56
|
-
? { value: parsed.value }
|
|
57
|
-
: parsed.values;
|
|
55
|
+
const values = typeof parsed.value === "string" ? { value: parsed.value } : parsed.values;
|
|
58
56
|
if (!values ||
|
|
59
57
|
typeof values !== "object" ||
|
|
60
58
|
Array.isArray(values) ||
|
|
61
|
-
!Object.entries(values).every(([field, value]) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(field) &&
|
|
62
|
-
typeof value === "string") ||
|
|
59
|
+
!Object.entries(values).every(([field, value]) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(field) && typeof value === "string") ||
|
|
63
60
|
typeof parsed.updatedAt !== "string" ||
|
|
64
61
|
typeof parsed.updatedBy !== "string") {
|
|
65
62
|
throw new Error("invalid plaintext");
|
|
@@ -88,33 +85,48 @@ function validateValues(values) {
|
|
|
88
85
|
}
|
|
89
86
|
normalized[field] = value;
|
|
90
87
|
}
|
|
91
|
-
if (encoder.encode(JSON.stringify(normalized)).byteLength >
|
|
92
|
-
MAX_CREDENTIAL_BYTES) {
|
|
88
|
+
if (encoder.encode(JSON.stringify(normalized)).byteLength > MAX_CREDENTIAL_BYTES) {
|
|
93
89
|
throw new Error(`Credential cannot exceed ${MAX_CREDENTIAL_BYTES} bytes`);
|
|
94
90
|
}
|
|
95
91
|
return normalized;
|
|
96
92
|
}
|
|
97
93
|
/**
|
|
98
94
|
* Encrypted, connector-scoped credential vault over the deployment's existing
|
|
99
|
-
* KVStorage. Only ciphertext enters
|
|
95
|
+
* KVStorage. Only ciphertext enters storage; the AES-GCM key remains an environment
|
|
100
96
|
* secret outside the store. The same key seals downstream OAuth state, which
|
|
101
97
|
* the OAuth provider stores itself.
|
|
102
98
|
*/
|
|
103
99
|
export class CredentialVault {
|
|
104
100
|
storage;
|
|
105
101
|
key;
|
|
102
|
+
handoffKey;
|
|
103
|
+
retryKey;
|
|
106
104
|
constructor(storage, encryptionKey) {
|
|
107
105
|
this.storage = storage;
|
|
108
106
|
const raw = base64ToBytes(encryptionKey.trim());
|
|
109
107
|
if (raw.byteLength !== KEY_BYTES) {
|
|
110
108
|
throw new Error("The credential vault encryption key must be a base64-encoded 32-byte key");
|
|
111
109
|
}
|
|
110
|
+
this.handoffKey = deriveOAuthHandoffKey(raw);
|
|
111
|
+
this.retryKey = deriveRequestStateKey(raw);
|
|
112
112
|
this.key = crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
113
113
|
}
|
|
114
|
+
async signOAuthHandoff(payload) {
|
|
115
|
+
return bytesToBase64(new Uint8Array(await crypto.subtle.sign("HMAC", await this.handoffKey, encoder.encode(payload))));
|
|
116
|
+
}
|
|
117
|
+
async requestStateKey() {
|
|
118
|
+
return new Uint8Array(await this.retryKey);
|
|
119
|
+
}
|
|
120
|
+
async verifyOAuthHandoff(payload, signature) {
|
|
121
|
+
try {
|
|
122
|
+
return await crypto.subtle.verify("HMAC", await this.handoffKey, base64ToBytes(signature), encoder.encode(payload));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
114
128
|
additionalData(connectorId, owner) {
|
|
115
|
-
return encoder.encode(owner
|
|
116
|
-
? `connecta:credential:principal:${owner}:${connectorId}:v1`
|
|
117
|
-
: `connecta:credential:${connectorId}:v1`);
|
|
129
|
+
return encoder.encode(owner ? `connecta:credential:principal:${owner}:${connectorId}:v1` : `connecta:credential:${connectorId}:v1`);
|
|
118
130
|
}
|
|
119
131
|
async read(connectorId, owner) {
|
|
120
132
|
const raw = await this.storage.get(storageKey(connectorId, owner));
|
|
@@ -207,10 +219,7 @@ export class CredentialVault {
|
|
|
207
219
|
/** Decrypt what `seal` produced for this connector, purpose, and owner. */
|
|
208
220
|
async open(connectorId, purpose, sealed, owner) {
|
|
209
221
|
const [version, iv, ciphertext, ...rest] = sealed.split(".");
|
|
210
|
-
if (version !== SEALED_VERSION ||
|
|
211
|
-
iv === undefined ||
|
|
212
|
-
ciphertext === undefined ||
|
|
213
|
-
rest.length > 0) {
|
|
222
|
+
if (version !== SEALED_VERSION || iv === undefined || ciphertext === undefined || rest.length > 0) {
|
|
214
223
|
throw new Error("Sealed OAuth state is invalid or corrupted");
|
|
215
224
|
}
|
|
216
225
|
try {
|