@zackbart/connecta 0.37.1 → 0.39.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 +94 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/agent-routes.js +231 -0
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2516
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/native.js +27 -0
- package/dist/auth/oauth-grant.js +24 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +9 -3
- package/dist/authorization-handoff.js +11 -5
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-cache.js +130 -43
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +285 -48
- package/dist/catalog.js +18 -2
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +57 -25
- package/dist/config.js +30 -77
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +11 -7
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +671 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +469 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +544 -0
- package/dist/connectors/remote-mcp.js +61 -2898
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +4 -2
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +172 -125
- package/dist/executor-result.js +5 -2
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +63 -20
- package/dist/guest-types.d.ts +24 -10
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +7 -6
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +4 -3
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +33 -5
- package/dist/http/safety.js +9 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +45 -30
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +21 -6
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +50 -35
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +6 -2
- package/dist/oauth-handoff.js +2 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +136 -14
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -49
- package/dist/operator-ui/contract.d.ts +12 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +49 -53
- package/dist/providers/_shared/rest/value-safety.js +106 -28
- package/dist/providers/breeze/api.js +9 -8
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +7 -7
- package/dist/providers/ccb/api.js +3 -7
- package/dist/providers/ccb/index.js +7 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +10 -3
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +4 -14
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +23 -41
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +11 -16
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +12 -26
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +8 -4
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +5 -2
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +14 -23
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +19 -26
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +5 -8
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +2 -7
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +6 -4
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +11 -5
- package/dist/registry.js +46 -20
- package/dist/request-principal.js +1 -1
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +130 -61
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +34 -17
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +6 -5
- package/dist/routes/shared.d.ts +13 -8
- package/dist/routes/shared.js +15 -9
- package/dist/routes/ui-config.js +3 -3
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +17 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +8 -3
- package/dist/runtime/native-storage.js +9 -0
- package/dist/runtime/request-reads.js +182 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/runtime/storage.js +12 -0
- package/dist/sent-secrets.d.ts +1 -2
- package/dist/sent-secrets.js +3 -8
- package/dist/server.js +4 -4
- package/dist/skills.js +44 -54
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +69 -128
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +27 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +13 -14
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +33 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +18 -37
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +19 -55
- package/documentation/architecture.md +436 -515
- package/documentation/auth.md +229 -88
- package/documentation/code-mode.md +201 -94
- package/documentation/deploying.md +25 -43
- package/documentation/integrating.md +18 -19
- package/documentation/meta-tools.md +225 -51
- package/documentation/migration-0.33.md +10 -6
- package/documentation/migration-0.34.md +5 -5
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +20 -120
- package/documentation/operator-ui.md +108 -16
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +14 -16
- package/examples/worker/README.md +24 -118
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +9 -31
- package/guides/how-it-works.md +6 -4
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
|
@@ -26,10 +26,6 @@ export function resourceUriMatchesTemplates(uri, templates, note = () => { }) {
|
|
|
26
26
|
}
|
|
27
27
|
return matched ? { matched } : { matched, ...(refusal ? { refusal } : {}) };
|
|
28
28
|
}
|
|
29
|
-
export function resourceUriMatchesTemplate(uri, template) {
|
|
30
|
-
const parsed = parse(template);
|
|
31
|
-
return typeof parsed === "object" && matches(uri, parsed);
|
|
32
|
-
}
|
|
33
29
|
function parse(template) {
|
|
34
30
|
if (template.length > MAX_URI_LENGTH)
|
|
35
31
|
return;
|
|
@@ -10,6 +10,12 @@ export interface CredentialMetadata {
|
|
|
10
10
|
/** Backward-compatible metadata for the reserved single credential field. */
|
|
11
11
|
lastFour?: string;
|
|
12
12
|
updatedAt: string;
|
|
13
|
+
/**
|
|
14
|
+
* Opaque and unique to the save this metadata describes. Credential
|
|
15
|
+
* verification is recorded only against it, so a vault that omits it
|
|
16
|
+
* stores credentials that are never shown as verified.
|
|
17
|
+
*/
|
|
18
|
+
revision?: string;
|
|
13
19
|
/** Per-field masked metadata for named multi-value credentials. */
|
|
14
20
|
fields?: Record<string, CredentialFieldMetadata>;
|
|
15
21
|
}
|
|
@@ -20,6 +26,15 @@ export interface CredentialVault {
|
|
|
20
26
|
set(connectorId: string, value: string, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
|
|
21
27
|
setAll(connectorId: string, values: ConnectorCredentialValues, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
|
|
22
28
|
delete(connectorId: string, owner?: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* The values and metadata of one save, read together so neither can come
|
|
31
|
+
* from a later save. Without it, a Test of the stored credential records
|
|
32
|
+
* no verification.
|
|
33
|
+
*/
|
|
34
|
+
snapshot?(connectorId: string, owner?: string): Promise<{
|
|
35
|
+
values: ConnectorCredentialValues;
|
|
36
|
+
metadata: CredentialMetadata;
|
|
37
|
+
} | null>;
|
|
23
38
|
/**
|
|
24
39
|
* Encrypt downstream OAuth state (tokens, a registered client, a PKCE
|
|
25
40
|
* verifier) before it enters storage. `purpose` is the storage key the value
|
|
@@ -37,6 +52,14 @@ export interface CredentialVault {
|
|
|
37
52
|
* exposes the vault or this key through a connector context or guest API.
|
|
38
53
|
*/
|
|
39
54
|
requestStateKey?(): Promise<Uint8Array>;
|
|
55
|
+
/**
|
|
56
|
+
* Host-only HMAC-SHA-256 signing key for stored remote catalog,
|
|
57
|
+
* negotiation-verdict and connector-health keys, so those partitions are
|
|
58
|
+
* deployment-keyed. Must be stable across the deployment's processes,
|
|
59
|
+
* purpose-specific, and never stored. Without it, those keys use a random
|
|
60
|
+
* per-process key, and such entries are reused only within one process.
|
|
61
|
+
*/
|
|
62
|
+
storageDigestKey?(): Promise<CryptoKey>;
|
|
40
63
|
/** Reverse `seal` for the same connector, purpose, and owner; rejects otherwise. */
|
|
41
64
|
open?(connectorId: string, purpose: string, sealed: string, owner?: string): Promise<string>;
|
|
42
65
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const readers = new WeakMap();
|
|
2
|
+
/** `canonical` is the base class's own prototype methods, never the instance's. */
|
|
3
|
+
export function registerCredentialReads(vault, canonical, memo) {
|
|
4
|
+
readers.set(vault, { canonical, memo });
|
|
5
|
+
}
|
|
6
|
+
/** A `fresh` reader always reads storage and records what it saw. */
|
|
7
|
+
export function credentialReads(vault, reads, fresh = false) {
|
|
8
|
+
const reader = readers.get(vault);
|
|
9
|
+
return {
|
|
10
|
+
getAll: (connectorId, owner) => reader && vault.getAll === reader.canonical.getAll
|
|
11
|
+
? reader.memo.getAll(connectorId, owner, reads, fresh)
|
|
12
|
+
: vault.getAll(connectorId, owner),
|
|
13
|
+
metadata: (connectorId, owner) => reader && vault.metadata === reader.canonical.metadata
|
|
14
|
+
? reader.memo.metadata(connectorId, owner, reads, fresh)
|
|
15
|
+
: vault.metadata(connectorId, owner),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The last verification of a connector's operator-managed credential: a typed,
|
|
2
|
+
// payload-free observation stored per connector and owner (the connector's
|
|
3
|
+
// `ctx.storage` is already partitioned by both). Credential tests and saves
|
|
4
|
+
// write it; the operator page only reads it (INV-10).
|
|
5
|
+
import { classifyCallError } from "./errors.js";
|
|
6
|
+
import { verificationKeys } from "./storage/keys.js";
|
|
7
|
+
const FAILURES = ["rejected", "scope", "unreachable", "timeout", "failed"];
|
|
8
|
+
const OUTCOMES = ["ok", "unverified", "rejected", "scope", "unreachable", "timeout", "failed"];
|
|
9
|
+
/** Each typed code that names a class; every other code is `failed`. */
|
|
10
|
+
const CLASS_OF = {
|
|
11
|
+
auth_required: "rejected",
|
|
12
|
+
downstream_oauth_required: "rejected",
|
|
13
|
+
provider_permission_denied: "scope",
|
|
14
|
+
timeout: "timeout",
|
|
15
|
+
unavailable: "unreachable",
|
|
16
|
+
rate_limited: "unreachable",
|
|
17
|
+
};
|
|
18
|
+
/** A thrown failure's class, read from its typed code alone, never its text. */
|
|
19
|
+
export const credentialFailureClass = (error) => CLASS_OF[classifyCallError(error).code] ?? "failed";
|
|
20
|
+
/** A hook's answer as an outcome. Only its typed fields count; `message` never does. */
|
|
21
|
+
export const credentialTestOutcome = (result) => result?.ok === true ? "ok" : FAILURES.includes(result?.failure) ? result.failure : "failed";
|
|
22
|
+
/** A checked copy, or undefined: nothing here trusts stored bytes. */
|
|
23
|
+
function checkedVerification(value) {
|
|
24
|
+
const { outcome, observedAt } = (value ?? {});
|
|
25
|
+
return OUTCOMES.includes(outcome) &&
|
|
26
|
+
typeof observedAt === "string" &&
|
|
27
|
+
observedAt.length < 64 &&
|
|
28
|
+
!Number.isNaN(Date.parse(observedAt))
|
|
29
|
+
? { outcome: outcome, observedAt }
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The stored observation, or undefined when there is none or it cannot be
|
|
34
|
+
* read. With `credential`, only one made about that vault credential (its
|
|
35
|
+
* `updatedAt`) counts: a replacement saved since is unverified.
|
|
36
|
+
*/
|
|
37
|
+
export async function readVerification(storage, credential) {
|
|
38
|
+
try {
|
|
39
|
+
const stored = JSON.parse((await storage.get(verificationKeys.record)) ?? "null");
|
|
40
|
+
return credential === undefined || stored?.credential === credential ? checkedVerification(stored) : undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Record an outcome, bound to the vault credential's `updatedAt` when it is about one; `null` removes the record. */
|
|
47
|
+
export async function writeVerification(storage, outcome, credential) {
|
|
48
|
+
if (!outcome)
|
|
49
|
+
return void (await storage.delete(verificationKeys.record));
|
|
50
|
+
const record = { outcome, observedAt: new Date().toISOString() };
|
|
51
|
+
await storage.set(verificationKeys.record, JSON.stringify({ ...record, credential }));
|
|
52
|
+
return record;
|
|
53
|
+
}
|
package/dist/credentials.d.ts
CHANGED
|
@@ -11,15 +11,22 @@ export declare class CredentialVault implements Vault {
|
|
|
11
11
|
private readonly key;
|
|
12
12
|
private readonly handoffKey;
|
|
13
13
|
private readonly retryKey;
|
|
14
|
+
private readonly digestKey;
|
|
14
15
|
constructor(storage: KVStorage, encryptionKey: string);
|
|
15
16
|
signOAuthHandoff(payload: string): Promise<string>;
|
|
16
17
|
requestStateKey(): Promise<Uint8Array>;
|
|
18
|
+
storageDigestKey(): Promise<CryptoKey>;
|
|
17
19
|
verifyOAuthHandoff(payload: string, signature: string): Promise<boolean>;
|
|
18
20
|
private additionalData;
|
|
19
21
|
private read;
|
|
20
22
|
get(connectorId: string, field?: string, owner?: string): Promise<string | null>;
|
|
21
23
|
getAll(connectorId: string, owner?: string): Promise<ConnectorCredentialValues | null>;
|
|
22
24
|
metadata(connectorId: string, owner?: string): Promise<CredentialMetadata | null>;
|
|
25
|
+
/** The values and metadata of one save, from a single envelope read. */
|
|
26
|
+
snapshot(connectorId: string, owner?: string): Promise<{
|
|
27
|
+
values: ConnectorCredentialValues;
|
|
28
|
+
metadata: CredentialMetadata;
|
|
29
|
+
} | null>;
|
|
23
30
|
set(connectorId: string, value: string, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
|
|
24
31
|
setAll(connectorId: string, values: ConnectorCredentialValues, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
|
|
25
32
|
delete(connectorId: string, owner?: string): Promise<void>;
|
package/dist/credentials.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { advanceCatalogRevision } from "./catalog-revision.js";
|
|
2
|
-
import { deriveOAuthHandoffKey, deriveRequestStateKey } from "./oauth-sealing.js";
|
|
2
|
+
import { deriveOAuthHandoffKey, deriveRequestStateKey, deriveStorageDigestKey } from "./oauth-sealing.js";
|
|
3
3
|
import { credentialKeys } from "./storage/keys.js";
|
|
4
|
+
import { registerCredentialReads } from "./credential-reads.js";
|
|
4
5
|
const KEY_BYTES = 32;
|
|
5
6
|
const IV_BYTES = 12;
|
|
6
7
|
const MAX_CREDENTIAL_BYTES = 16_384;
|
|
@@ -50,7 +51,7 @@ function parseEnvelope(raw) {
|
|
|
50
51
|
throw new Error("Stored credential is invalid or corrupted");
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
function parsePlaintext(raw) {
|
|
54
|
+
function parsePlaintext(raw, iv) {
|
|
54
55
|
try {
|
|
55
56
|
const parsed = JSON.parse(raw);
|
|
56
57
|
const values = typeof parsed.value === "string" ? { value: parsed.value } : parsed.values;
|
|
@@ -66,6 +67,7 @@ function parsePlaintext(raw) {
|
|
|
66
67
|
values: { ...values },
|
|
67
68
|
updatedAt: parsed.updatedAt,
|
|
68
69
|
updatedBy: parsed.updatedBy,
|
|
70
|
+
revision: typeof parsed.revision === "string" ? parsed.revision : iv,
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
catch {
|
|
@@ -91,6 +93,28 @@ function validateValues(values) {
|
|
|
91
93
|
}
|
|
92
94
|
return normalized;
|
|
93
95
|
}
|
|
96
|
+
function metadataOf(credential) {
|
|
97
|
+
if (!credential)
|
|
98
|
+
return null;
|
|
99
|
+
const fields = Object.fromEntries(Object.entries(credential.values).map(([field, value]) => [
|
|
100
|
+
field,
|
|
101
|
+
{
|
|
102
|
+
configured: true,
|
|
103
|
+
// Only reveal the tail on values comfortably longer than four chars;
|
|
104
|
+
// for a short secret those four would be half of it.
|
|
105
|
+
...(value.length >= 12 ? { lastFour: value.slice(-4) } : {}),
|
|
106
|
+
updatedAt: credential.updatedAt,
|
|
107
|
+
},
|
|
108
|
+
]));
|
|
109
|
+
const single = fields.value;
|
|
110
|
+
return {
|
|
111
|
+
configured: true,
|
|
112
|
+
...(single?.lastFour ? { lastFour: single.lastFour } : {}),
|
|
113
|
+
updatedAt: credential.updatedAt,
|
|
114
|
+
revision: credential.revision,
|
|
115
|
+
fields,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
94
118
|
/**
|
|
95
119
|
* Encrypted, connector-scoped credential vault over the deployment's existing
|
|
96
120
|
* KVStorage. Only ciphertext enters storage; the AES-GCM key remains an environment
|
|
@@ -102,6 +126,7 @@ export class CredentialVault {
|
|
|
102
126
|
key;
|
|
103
127
|
handoffKey;
|
|
104
128
|
retryKey;
|
|
129
|
+
digestKey;
|
|
105
130
|
constructor(storage, encryptionKey) {
|
|
106
131
|
this.storage = storage;
|
|
107
132
|
const raw = base64ToBytes(encryptionKey.trim());
|
|
@@ -110,7 +135,16 @@ export class CredentialVault {
|
|
|
110
135
|
}
|
|
111
136
|
this.handoffKey = deriveOAuthHandoffKey(raw);
|
|
112
137
|
this.retryKey = deriveRequestStateKey(raw);
|
|
138
|
+
this.digestKey = deriveStorageDigestKey(raw);
|
|
113
139
|
this.key = crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
140
|
+
// The base class's original methods, so an override is never replaced.
|
|
141
|
+
registerCredentialReads(this, canonicalReads, {
|
|
142
|
+
getAll: async (connectorId, owner, reads, fresh) => {
|
|
143
|
+
const credential = await this.read(connectorId, owner, { reads, fresh });
|
|
144
|
+
return credential ? { ...credential.values } : null;
|
|
145
|
+
},
|
|
146
|
+
metadata: async (connectorId, owner, reads, fresh) => metadataOf(await this.read(connectorId, owner, { reads, fresh })),
|
|
147
|
+
});
|
|
114
148
|
}
|
|
115
149
|
async signOAuthHandoff(payload) {
|
|
116
150
|
return bytesToBase64(new Uint8Array(await crypto.subtle.sign("HMAC", await this.handoffKey, encoder.encode(payload))));
|
|
@@ -118,6 +152,9 @@ export class CredentialVault {
|
|
|
118
152
|
async requestStateKey() {
|
|
119
153
|
return new Uint8Array(await this.retryKey);
|
|
120
154
|
}
|
|
155
|
+
storageDigestKey() {
|
|
156
|
+
return this.digestKey;
|
|
157
|
+
}
|
|
121
158
|
async verifyOAuthHandoff(payload, signature) {
|
|
122
159
|
try {
|
|
123
160
|
return await crypto.subtle.verify("HMAC", await this.handoffKey, base64ToBytes(signature), encoder.encode(payload));
|
|
@@ -129,8 +166,11 @@ export class CredentialVault {
|
|
|
129
166
|
additionalData(connectorId, owner) {
|
|
130
167
|
return encoder.encode(owner ? `connecta:credential:principal:${owner}:${connectorId}:v1` : `connecta:credential:${connectorId}:v1`);
|
|
131
168
|
}
|
|
132
|
-
async read(connectorId, owner) {
|
|
133
|
-
const
|
|
169
|
+
async read(connectorId, owner, memo) {
|
|
170
|
+
const key = storageKey(connectorId, owner);
|
|
171
|
+
const raw = memo
|
|
172
|
+
? await memo.reads.readPromise(this.storage, key, () => this.storage.get(key), memo.fresh)
|
|
173
|
+
: await this.storage.get(key);
|
|
134
174
|
if (!raw)
|
|
135
175
|
return null;
|
|
136
176
|
const envelope = parseEnvelope(raw);
|
|
@@ -140,7 +180,7 @@ export class CredentialVault {
|
|
|
140
180
|
iv: base64ToBytes(envelope.iv),
|
|
141
181
|
additionalData: this.additionalData(connectorId, owner),
|
|
142
182
|
}, await this.key, base64ToBytes(envelope.ciphertext));
|
|
143
|
-
return parsePlaintext(decoder.decode(plaintext));
|
|
183
|
+
return parsePlaintext(decoder.decode(plaintext), envelope.iv);
|
|
144
184
|
}
|
|
145
185
|
catch {
|
|
146
186
|
throw new Error("Stored credential could not be decrypted");
|
|
@@ -155,26 +195,12 @@ export class CredentialVault {
|
|
|
155
195
|
return credential ? { ...credential.values } : null;
|
|
156
196
|
}
|
|
157
197
|
async metadata(connectorId, owner) {
|
|
198
|
+
return metadataOf(await this.read(connectorId, owner));
|
|
199
|
+
}
|
|
200
|
+
/** The values and metadata of one save, from a single envelope read. */
|
|
201
|
+
async snapshot(connectorId, owner) {
|
|
158
202
|
const credential = await this.read(connectorId, owner);
|
|
159
|
-
|
|
160
|
-
return null;
|
|
161
|
-
const fields = Object.fromEntries(Object.entries(credential.values).map(([field, value]) => [
|
|
162
|
-
field,
|
|
163
|
-
{
|
|
164
|
-
configured: true,
|
|
165
|
-
// Only reveal the tail on values comfortably longer than four chars;
|
|
166
|
-
// for a short secret those four would be half of it.
|
|
167
|
-
...(value.length >= 12 ? { lastFour: value.slice(-4) } : {}),
|
|
168
|
-
updatedAt: credential.updatedAt,
|
|
169
|
-
},
|
|
170
|
-
]));
|
|
171
|
-
const single = fields.value;
|
|
172
|
-
return {
|
|
173
|
-
configured: true,
|
|
174
|
-
...(single?.lastFour ? { lastFour: single.lastFour } : {}),
|
|
175
|
-
updatedAt: credential.updatedAt,
|
|
176
|
-
fields,
|
|
177
|
-
};
|
|
203
|
+
return credential && { values: { ...credential.values }, metadata: metadataOf(credential) };
|
|
178
204
|
}
|
|
179
205
|
async set(connectorId, value, updatedBy, owner) {
|
|
180
206
|
// `await` (not a bare promise return) so a validation throw inside setAll
|
|
@@ -188,6 +214,7 @@ export class CredentialVault {
|
|
|
188
214
|
values: normalized,
|
|
189
215
|
updatedAt: new Date().toISOString(),
|
|
190
216
|
updatedBy,
|
|
217
|
+
revision: crypto.randomUUID(),
|
|
191
218
|
};
|
|
192
219
|
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
193
220
|
const ciphertext = await crypto.subtle.encrypt({
|
|
@@ -203,7 +230,9 @@ export class CredentialVault {
|
|
|
203
230
|
};
|
|
204
231
|
await this.storage.set(storageKey(connectorId, owner), JSON.stringify(envelope));
|
|
205
232
|
advanceCatalogRevision(this);
|
|
206
|
-
|
|
233
|
+
// What this write stored, never a re-read: a concurrent save may already
|
|
234
|
+
// have replaced it, and its revision must not be reported as this one's.
|
|
235
|
+
return metadataOf(plaintext);
|
|
207
236
|
}
|
|
208
237
|
async delete(connectorId, owner) {
|
|
209
238
|
await this.storage.delete(storageKey(connectorId, owner));
|
|
@@ -238,6 +267,12 @@ export class CredentialVault {
|
|
|
238
267
|
}
|
|
239
268
|
}
|
|
240
269
|
}
|
|
270
|
+
// Captured once at definition: a later subclass, instance or prototype
|
|
271
|
+
// override of either method is always called itself (credential-reads.ts).
|
|
272
|
+
const canonicalReads = {
|
|
273
|
+
getAll: CredentialVault.prototype.getAll,
|
|
274
|
+
metadata: CredentialVault.prototype.metadata,
|
|
275
|
+
};
|
|
241
276
|
export function encryptedCredentialVault(storage, encryptionKey) {
|
|
242
277
|
return new CredentialVault(storage, encryptionKey);
|
|
243
278
|
}
|
package/dist/d1.d.ts
CHANGED
|
@@ -38,93 +38,4 @@ export declare function d1Storage(db: D1DatabaseBinding): KVStorage;
|
|
|
38
38
|
* write prunes a bounded batch of rows older than `retentionDays`.
|
|
39
39
|
*/
|
|
40
40
|
export declare function d1ActivityStore(db: D1DatabaseBinding, options?: D1ActivityOptions): ActivityStore;
|
|
41
|
-
/**
|
|
42
|
-
* The part of a Workers KV binding `copyKvToD1` reads. Structural, like
|
|
43
|
-
* `D1DatabaseBinding`; a Worker's `KVNamespace` satisfies it.
|
|
44
|
-
*/
|
|
45
|
-
export interface KVNamespaceBinding {
|
|
46
|
-
list(options?: {
|
|
47
|
-
limit?: number;
|
|
48
|
-
cursor?: string | null;
|
|
49
|
-
}): Promise<{
|
|
50
|
-
keys: readonly {
|
|
51
|
-
name: string;
|
|
52
|
-
expiration?: number;
|
|
53
|
-
}[];
|
|
54
|
-
list_complete: boolean;
|
|
55
|
-
cursor?: string;
|
|
56
|
-
}>;
|
|
57
|
-
get(key: string, type: "text"): Promise<string | null>;
|
|
58
|
-
}
|
|
59
|
-
export interface KvToD1CopyOptions {
|
|
60
|
-
/** The source KV namespace id. Resume tokens are bound to this identity. */
|
|
61
|
-
source: string;
|
|
62
|
-
/** Resume against the same source and database within seven days. */
|
|
63
|
-
cursor?: string;
|
|
64
|
-
/** Explicit families whose conflicting D1 rows the operator judged stale. */
|
|
65
|
-
overwriteFamilies?: readonly string[];
|
|
66
|
-
/** Compare value hashes and absolute expiries without copying entries. */
|
|
67
|
-
verify?: boolean;
|
|
68
|
-
/** Override the cutover guard only after assessing stale KV state. */
|
|
69
|
-
allowStale?: boolean;
|
|
70
|
-
/** Keys read per call. Default 500; use a smaller budget for large rows. */
|
|
71
|
-
maxKeys?: number;
|
|
72
|
-
}
|
|
73
|
-
/** What a copy did with one family's keys. */
|
|
74
|
-
export interface KvToD1Counts {
|
|
75
|
-
/** Written where D1 held no live entry. */
|
|
76
|
-
copied: number;
|
|
77
|
-
/** D1 already held the same value. */
|
|
78
|
-
unchanged: number;
|
|
79
|
-
/** D1 held a different value or expiry and kept it. */
|
|
80
|
-
conflicts: number;
|
|
81
|
-
/** D1 held a different value or expiry, replaced under `overwriteFamilies`. */
|
|
82
|
-
overwritten: number;
|
|
83
|
-
/** Expired or deleted in Workers KV before it could be read. */
|
|
84
|
-
expired: number;
|
|
85
|
-
/** A key/value/row D1 cannot hold, left in Workers KV. */
|
|
86
|
-
invalid: number;
|
|
87
|
-
/** Exact value hash and expiry match during a verification pass. */
|
|
88
|
-
verified: number;
|
|
89
|
-
/** Missing or different value/expiry during a verification pass. */
|
|
90
|
-
mismatches: number;
|
|
91
|
-
}
|
|
92
|
-
export interface KvToD1CopyResult {
|
|
93
|
-
/** True once the whole namespace has been read. */
|
|
94
|
-
done: boolean;
|
|
95
|
-
/**
|
|
96
|
-
* Pass back as `options.cursor` to continue; absent when `done`. A random
|
|
97
|
-
* token: the Workers KV cursor it stands for stays in D1, because that
|
|
98
|
-
* cursor can spell the last key listed.
|
|
99
|
-
*/
|
|
100
|
-
cursor?: string;
|
|
101
|
-
/**
|
|
102
|
-
* Counts by key family (`src/storage/keys.ts`), plus `connector-owned` and
|
|
103
|
-
* `unclassified`. Never a key or a value.
|
|
104
|
-
*/
|
|
105
|
-
families: Record<string, KvToD1Counts>;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* A copy that stopped. The message is fixed wording that names a family at
|
|
109
|
-
* most, never a key or value, and the binding's own error is not attached:
|
|
110
|
-
* KV and D1 errors can quote either. Entries before `cursor` are copied;
|
|
111
|
-
* rerunning from it is safe while the maintenance window still holds.
|
|
112
|
-
*/
|
|
113
|
-
export declare class KvToD1CopyError extends Error {
|
|
114
|
-
/** Resume point; absent means start over, which is also safe. */
|
|
115
|
-
readonly cursor: string | undefined;
|
|
116
|
-
readonly name = "KvToD1CopyError";
|
|
117
|
-
constructor(message: string,
|
|
118
|
-
/** Resume point; absent means start over, which is also safe. */
|
|
119
|
-
cursor: string | undefined);
|
|
120
|
-
}
|
|
121
|
-
/** Mark cutover before reopening traffic. The marker stays in D1 permanently. */
|
|
122
|
-
export declare function markKvToD1Live(db: D1DatabaseBinding, source: string): Promise<void>;
|
|
123
|
-
/**
|
|
124
|
-
* Copy live KV entries verbatim while all writers and traffic are stopped.
|
|
125
|
-
* A live D1 row is kept unless its family is explicitly allowed to overwrite.
|
|
126
|
-
* `verify` reads hashes and absolute expiries without changing copied entries.
|
|
127
|
-
* Results contain counts only. Resume tokens are source-bound atomic claims.
|
|
128
|
-
*/
|
|
129
|
-
export declare function copyKvToD1(kv: KVNamespaceBinding, db: D1DatabaseBinding, options: KvToD1CopyOptions): Promise<KvToD1CopyResult>;
|
|
130
41
|
export {};
|
package/dist/d1.js
CHANGED
|
@@ -3,12 +3,8 @@
|
|
|
3
3
|
// One D1 database holds everything: `d1Storage(db)` is the deployment's
|
|
4
4
|
// `KVStorage`, and `d1ActivityStore(db)` its activity history. Both create
|
|
5
5
|
// their tables on first use, so binding the database is the whole setup.
|
|
6
|
-
// `copyKvToD1` moves a 0.28 deployment's Workers KV state into it once.
|
|
7
|
-
import { runEdge } from "./runtime/run.js";
|
|
8
6
|
import { d1Driver } from "./storage/d1-driver.js";
|
|
9
|
-
import { copyKvToD1Effect, markKvToD1LiveEffect } from "./storage/d1-copy.js";
|
|
10
7
|
import { sqlActivityStore, sqlStorage } from "./storage/sql.js";
|
|
11
|
-
import { keepErrorName } from "./redaction-identity.js";
|
|
12
8
|
/**
|
|
13
9
|
* `KVStorage` over a D1 database, with the atomic `compareAndSet` every
|
|
14
10
|
* subsystem relies on. The `connecta_kv` table is created on first use; a
|
|
@@ -25,33 +21,3 @@ export function d1Storage(db) {
|
|
|
25
21
|
export function d1ActivityStore(db, options) {
|
|
26
22
|
return sqlActivityStore(d1Driver(db), "d1", options);
|
|
27
23
|
}
|
|
28
|
-
/**
|
|
29
|
-
* A copy that stopped. The message is fixed wording that names a family at
|
|
30
|
-
* most, never a key or value, and the binding's own error is not attached:
|
|
31
|
-
* KV and D1 errors can quote either. Entries before `cursor` are copied;
|
|
32
|
-
* rerunning from it is safe while the maintenance window still holds.
|
|
33
|
-
*/
|
|
34
|
-
export class KvToD1CopyError extends Error {
|
|
35
|
-
cursor;
|
|
36
|
-
name = "KvToD1CopyError";
|
|
37
|
-
constructor(message,
|
|
38
|
-
/** Resume point; absent means start over, which is also safe. */
|
|
39
|
-
cursor) {
|
|
40
|
-
super(message);
|
|
41
|
-
this.cursor = cursor;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
keepErrorName(KvToD1CopyError, "KvToD1CopyError");
|
|
45
|
-
/** Mark cutover before reopening traffic. The marker stays in D1 permanently. */
|
|
46
|
-
export function markKvToD1Live(db, source) {
|
|
47
|
-
return runEdge(markKvToD1LiveEffect(db, source));
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Copy live KV entries verbatim while all writers and traffic are stopped.
|
|
51
|
-
* A live D1 row is kept unless its family is explicitly allowed to overwrite.
|
|
52
|
-
* `verify` reads hashes and absolute expiries without changing copied entries.
|
|
53
|
-
* Results contain counts only. Resume tokens are source-bound atomic claims.
|
|
54
|
-
*/
|
|
55
|
-
export function copyKvToD1(kv, db, options) {
|
|
56
|
-
return runEdge(copyKvToD1Effect(kv, db, options));
|
|
57
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
2
2
|
import type { ConnectaConfig, ResolvedConfig } from "./config.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type ResolvedPool } from "./membership.js";
|
|
4
4
|
import type { ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, DescribedEndpoint, IdentityReference } from "./types.js";
|
|
5
5
|
/** Where a limit's value came from. */
|
|
6
6
|
export type ConfigValueSource = "default" | "config";
|
|
@@ -100,11 +100,21 @@ export interface ConnectaConfigDescription {
|
|
|
100
100
|
};
|
|
101
101
|
/** Whether the default /mcp endpoint refuses every write, and exact classification overrides from deployment code. */
|
|
102
102
|
readOnly: boolean;
|
|
103
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* The people plain `/mcp` serves the whole catalog, resolved, in declaration
|
|
105
|
+
* order. Empty on an open deployment. Every other person reaches plain
|
|
106
|
+
* `/mcp` only through exactly one pool's `members`, which it then serves.
|
|
107
|
+
*/
|
|
104
108
|
maintainers: Array<{
|
|
105
109
|
namespace: string;
|
|
106
110
|
id: string;
|
|
107
111
|
}>;
|
|
112
|
+
/** Non-maintainer people listed in several pools, whom plain `/mcp` refuses. */
|
|
113
|
+
peopleInSeveralPools: Array<{
|
|
114
|
+
namespace: string;
|
|
115
|
+
id: string;
|
|
116
|
+
pools: string[];
|
|
117
|
+
}>;
|
|
108
118
|
classification: Record<string, Record<string, "read" | "write" | "hidden">>;
|
|
109
119
|
/** Inbound auth providers in the order they are tried. */
|
|
110
120
|
auth: Array<{
|
package/dist/describe-config.js
CHANGED
|
@@ -15,6 +15,7 @@ import { resolveBranding } from "./branding.js";
|
|
|
15
15
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
16
16
|
import { describedEndpoint, describedHref, describedOrigin, describedTools, describedUrl } from "./described.js";
|
|
17
17
|
import { connectorGuideRequired, connectorGuideSummary } from "./skills.js";
|
|
18
|
+
import { peopleInSeveralPools } from "./membership.js";
|
|
18
19
|
import { CONNECTA_VERSION } from "./version.js";
|
|
19
20
|
const str = (value) => (typeof value === "string" ? value : undefined);
|
|
20
21
|
const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
@@ -235,6 +236,7 @@ export function describeConfig(input) {
|
|
|
235
236
|
},
|
|
236
237
|
readOnly: config.readOnly,
|
|
237
238
|
maintainers: input.maintainers.map(({ namespace, id }) => ({ namespace: String(namespace), id: String(id) })),
|
|
239
|
+
peopleInSeveralPools: peopleInSeveralPools(input.pools, input.maintainers),
|
|
238
240
|
classification: structuredClone(config.classification ?? {}),
|
|
239
241
|
auth: config.auth.map((provider) => ({
|
|
240
242
|
kind: String(provider.kind),
|