@zackbart/connecta 0.35.0 → 0.36.1
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 +97 -0
- package/README.md +8 -4
- package/bin/version.mjs +1 -1
- package/dist/auth/downstream-oauth.js +71 -79
- package/dist/auth/static-oauth.js +104 -26
- package/dist/connectors/api-connector.d.ts +17 -0
- package/dist/connectors/api-connector.js +3 -1
- package/dist/http/client.js +1 -1
- package/dist/http/hatch-paging.js +2 -2
- package/dist/http/paging.js +3 -89
- package/dist/http/provider.js +1 -2
- package/dist/http/services.js +1 -1
- package/dist/http/tools.js +1 -27
- package/dist/operator-ui/contract.d.ts +1 -0
- package/dist/operator-ui/generated.js +4 -4
- package/dist/program-writes.js +0 -1
- package/dist/providers/_shared/google/kit.js +29 -139
- package/dist/providers/_shared/rest/kit.js +4 -404
- package/dist/providers/_shared/sensitive-fields.js +0 -16
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/ccb/api.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -8
- package/dist/providers/cloudflare/lib/config.d.ts +2 -1
- package/dist/providers/docs/api.js +1 -1
- package/dist/providers/docs/tools.js +1 -2
- package/dist/providers/drive/api.js +2 -2
- package/dist/providers/drive/lib/hatch.js +1 -1
- package/dist/providers/drive/tools.js +1 -1
- package/dist/providers/forms/api.js +1 -1
- package/dist/providers/forms/lib/hatch.js +1 -1
- package/dist/providers/forms/tools.js +2 -3
- package/dist/providers/github/api.js +1 -1
- package/dist/providers/github/lib/discovery.js +2 -2
- package/dist/providers/gmail/api.js +1 -1
- package/dist/providers/gmail/tools.js +2 -2
- package/dist/providers/granola/api.js +155 -0
- package/dist/providers/granola/index.d.ts +10 -0
- package/dist/providers/granola/index.js +68 -0
- package/dist/providers/granola/openapi.generated.js +30 -0
- package/dist/providers/granola/safety.js +37 -0
- package/dist/providers/granola/skill.generated.js +5 -0
- package/dist/providers/granola/tools.js +2 -0
- package/dist/providers/overflow/api.js +26 -26
- package/dist/providers/planning-center/api.js +4 -4
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +2 -2
- package/dist/providers/quickbooks/api.js +299 -0
- package/dist/providers/quickbooks/index.d.ts +13 -0
- package/dist/providers/quickbooks/index.js +93 -0
- package/dist/providers/quickbooks/lib/query.js +118 -0
- package/dist/providers/quickbooks/lib/writes.js +168 -0
- package/dist/providers/quickbooks/safety.js +222 -0
- package/dist/providers/quickbooks/skill.generated.js +5 -0
- package/dist/providers/quickbooks/tools.js +117 -0
- package/dist/providers/sheets/api.js +1 -1
- package/dist/providers/sheets/lib/hatch.js +2 -2
- package/dist/providers/sheets/lib/requests.js +1 -1
- package/dist/providers/sheets/tools.js +1 -2
- package/dist/providers/slides/api.js +1 -1
- package/dist/providers/slides/tools.js +2 -2
- package/dist/providers/stripe/api.js +7 -7
- package/dist/providers/tithely/api.js +2 -2
- package/dist/providers/vercel/api.js +12 -12
- package/dist/providers/vercel/index.js +1 -2
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/routes/mcp.js +1 -1
- package/dist/routes/oauth.js +3 -0
- package/dist/runtime/admission.js +0 -12
- package/dist/runtime/call.js +0 -4
- package/dist/skills.js +0 -1
- package/dist/storage/keys.js +2 -3
- package/dist/types.d.ts +2 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +0 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/README.md +1 -0
- package/documentation/architecture.md +249 -366
- package/documentation/auth.md +56 -19
- package/documentation/code-mode.md +6 -6
- package/documentation/integrating.md +1 -1
- package/documentation/meta-tools.md +6 -3
- package/documentation/operator-tests.md +11 -2
- package/documentation/operator-ui.md +17 -5
- package/documentation/providers.md +492 -0
- package/package.json +12 -9
- package/templates/node/package.json +1 -1
- package/dist/operator-ui/agents.d.ts +0 -75
- package/dist/operator-ui/agents.js +0 -114
- package/dist/operator-ui/config-summary.js +0 -566
- package/dist/operator-ui/connection-map.js +0 -378
- package/dist/operator-ui/endpoints.js +0 -212
- package/dist/operator-ui/fix-prompts.d.ts +0 -18
- package/dist/operator-ui/fix-prompts.js +0 -79
- package/dist/operator-ui/health.js +0 -52
- package/dist/operator-ui/member-page.js +0 -97
- package/dist/operator-ui/overview.js +0 -126
- package/dist/operator-ui/setup-commands.js +0 -47
- package/dist/operator-ui/view.d.ts +0 -378
- package/dist/operator-ui/view.js +0 -631
- package/dist/providers/_shared/rest/dispatch.js +0 -21
- package/dist/providers/_shared/rest/tools.js +0 -592
|
@@ -54,6 +54,15 @@ const RESERVED_AUTHORIZATION_PARAMS = new Set([
|
|
|
54
54
|
"code_challenge_method",
|
|
55
55
|
"resource",
|
|
56
56
|
]);
|
|
57
|
+
/** Protocol response fields have their own validation, never static assertions. */
|
|
58
|
+
const RESERVED_CALLBACK_PARAMS = new Set([
|
|
59
|
+
"code",
|
|
60
|
+
"state",
|
|
61
|
+
"iss",
|
|
62
|
+
"error",
|
|
63
|
+
"error_description",
|
|
64
|
+
"error_uri",
|
|
65
|
+
]);
|
|
57
66
|
/** Token-request headers the grant owns: client authentication and framing. */
|
|
58
67
|
const RESERVED_TOKEN_HEADERS = new Set([
|
|
59
68
|
"authorization",
|
|
@@ -92,65 +101,91 @@ function settingsFor(id, config) {
|
|
|
92
101
|
if (!config || typeof config !== "object") {
|
|
93
102
|
throw new Error(`[connecta] connector "${id}" oauth must be an object.`);
|
|
94
103
|
}
|
|
104
|
+
const invalid = (detail) => {
|
|
105
|
+
throw new Error(`[connecta] connector "${id}" oauth.${detail}`);
|
|
106
|
+
};
|
|
95
107
|
const authorizationEndpoint = configuredUrl(id, "authorizationEndpoint", config.authorizationEndpoint);
|
|
96
108
|
const tokenEndpoint = configuredUrl(id, "tokenEndpoint", config.tokenEndpoint);
|
|
109
|
+
const revocationEndpoint = config.revocationEndpoint === undefined
|
|
110
|
+
? undefined
|
|
111
|
+
: configuredUrl(id, "revocationEndpoint", config.revocationEndpoint);
|
|
112
|
+
if (revocationEndpoint?.search)
|
|
113
|
+
invalid(`revocationEndpoint must not carry a query.`);
|
|
114
|
+
const revocationRequestFormat = config.revocationRequestFormat ?? "form";
|
|
115
|
+
if ((revocationRequestFormat !== "form" && revocationRequestFormat !== "json") ||
|
|
116
|
+
(config.revocationRequestFormat !== undefined && !revocationEndpoint))
|
|
117
|
+
invalid(`revocationRequestFormat requires an endpoint and form or json.`);
|
|
97
118
|
if (tokenEndpoint.search) {
|
|
98
|
-
|
|
119
|
+
invalid(`tokenEndpoint must not carry a query.`);
|
|
99
120
|
}
|
|
100
121
|
if (typeof config.clientId !== "string" || config.clientId.trim() === "") {
|
|
101
|
-
|
|
122
|
+
invalid(`clientId must be a non-empty string.`);
|
|
102
123
|
}
|
|
103
124
|
if (config.clientSecret !== undefined && (typeof config.clientSecret !== "string" || config.clientSecret === "")) {
|
|
104
125
|
// Usually an unset environment variable read as "". Never echo it.
|
|
105
|
-
|
|
126
|
+
invalid(`clientSecret must be a non-empty string when set.`);
|
|
106
127
|
}
|
|
107
128
|
const clientAuthentication = config.tokenEndpointAuthMethod ?? (config.clientSecret !== undefined ? "client_secret_basic" : "none");
|
|
108
129
|
if (!CLIENT_AUTHENTICATION.has(clientAuthentication)) {
|
|
109
|
-
|
|
110
|
-
'"client_secret_basic", "client_secret_post", or "none".');
|
|
130
|
+
invalid(`tokenEndpointAuthMethod must be ` + '"client_secret_basic", "client_secret_post", or "none".');
|
|
111
131
|
}
|
|
112
132
|
if (clientAuthentication === "none" && config.clientSecret !== undefined) {
|
|
113
133
|
throw new Error(`[connecta] connector "${id}" oauth declares a clientSecret with ` +
|
|
114
134
|
'tokenEndpointAuthMethod "none"; a public client sends no secret.');
|
|
115
135
|
}
|
|
116
136
|
if (clientAuthentication !== "none" && config.clientSecret === undefined) {
|
|
117
|
-
|
|
137
|
+
invalid(`tokenEndpointAuthMethod ` + `"${clientAuthentication}" needs a clientSecret.`);
|
|
118
138
|
}
|
|
119
139
|
assertOAuthScope(id, config.scope);
|
|
120
140
|
const authorizationParams = [];
|
|
121
141
|
for (const [name, value] of Object.entries(config.authorizationParams ?? {})) {
|
|
122
142
|
if (RESERVED_AUTHORIZATION_PARAMS.has(name)) {
|
|
123
|
-
|
|
124
|
-
`"${name}"; the authorization request owns it.`);
|
|
143
|
+
invalid(`authorizationParams may not set ` + `"${name}"; the authorization request owns it.`);
|
|
125
144
|
}
|
|
126
145
|
if (typeof value !== "string") {
|
|
127
|
-
|
|
146
|
+
invalid(`authorizationParams.${name} must be a string.`);
|
|
128
147
|
}
|
|
129
148
|
authorizationParams.push([name, value]);
|
|
130
149
|
}
|
|
150
|
+
const expectedCallbackParameters = [];
|
|
151
|
+
if (config.expectedCallbackParameters !== undefined &&
|
|
152
|
+
(!config.expectedCallbackParameters ||
|
|
153
|
+
typeof config.expectedCallbackParameters !== "object" ||
|
|
154
|
+
Array.isArray(config.expectedCallbackParameters))) {
|
|
155
|
+
invalid(`expectedCallbackParameters must be a string record.`);
|
|
156
|
+
}
|
|
157
|
+
for (const [name, value] of Object.entries(config.expectedCallbackParameters ?? {})) {
|
|
158
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(name) ||
|
|
159
|
+
RESERVED_CALLBACK_PARAMS.has(name) ||
|
|
160
|
+
typeof value !== "string" ||
|
|
161
|
+
value === "") {
|
|
162
|
+
invalid(`expectedCallbackParameters must use nonempty parameter names ` +
|
|
163
|
+
"(ASCII letters, digits, dots, underscores or hyphens), nonempty string values, and no OAuth response fields.");
|
|
164
|
+
}
|
|
165
|
+
expectedCallbackParameters.push([name, value]);
|
|
166
|
+
}
|
|
131
167
|
const tokenRequestHeaders = [];
|
|
132
168
|
for (const [name, value] of Object.entries(config.tokenRequestHeaders ?? {})) {
|
|
133
169
|
if (!HEADER_NAME.test(name)) {
|
|
134
|
-
|
|
170
|
+
invalid(`tokenRequestHeaders has an invalid header name.`);
|
|
135
171
|
}
|
|
136
172
|
if (RESERVED_TOKEN_HEADERS.has(name.toLowerCase())) {
|
|
137
|
-
|
|
173
|
+
invalid(`tokenRequestHeaders may not set ` + `"${name}"; the token request owns it.`);
|
|
138
174
|
}
|
|
139
175
|
if (typeof value !== "string" || value.includes("\r") || value.includes("\n") || value.includes("\0")) {
|
|
140
|
-
|
|
176
|
+
invalid(`tokenRequestHeaders.${name} must be a single-line string.`);
|
|
141
177
|
}
|
|
142
178
|
tokenRequestHeaders.push([name, value]);
|
|
143
179
|
}
|
|
144
180
|
const origins = Array.isArray(config.apiOrigins) ? config.apiOrigins : [];
|
|
145
181
|
if (origins.length === 0) {
|
|
146
|
-
|
|
147
|
-
"origin; ctx.oauth.fetch sends the access token nowhere else.");
|
|
182
|
+
invalid(`apiOrigins must name at least one ` + "origin; ctx.oauth.fetch sends the access token nowhere else.");
|
|
148
183
|
}
|
|
149
184
|
const apiOrigins = new Set();
|
|
150
185
|
for (const origin of origins) {
|
|
151
186
|
const url = configuredUrl(id, "apiOrigins", origin);
|
|
152
187
|
if (url.origin !== origin) {
|
|
153
|
-
|
|
188
|
+
invalid(`apiOrigins entries must be exact ` +
|
|
154
189
|
'origins, like "https://api.example.com", with no path or trailing slash.');
|
|
155
190
|
}
|
|
156
191
|
apiOrigins.add(url.origin);
|
|
@@ -158,12 +193,15 @@ function settingsFor(id, config) {
|
|
|
158
193
|
return {
|
|
159
194
|
authorizationEndpoint,
|
|
160
195
|
tokenEndpoint,
|
|
196
|
+
...(revocationEndpoint ? { revocationEndpoint } : {}),
|
|
197
|
+
revocationRequestFormat,
|
|
161
198
|
clientId: config.clientId,
|
|
162
199
|
...(config.clientSecret !== undefined ? { clientSecret: config.clientSecret } : {}),
|
|
163
200
|
clientAuthentication,
|
|
164
201
|
...(config.scope !== undefined ? { scope: config.scope } : {}),
|
|
165
202
|
pkce: config.pkce ?? true,
|
|
166
203
|
authorizationParams,
|
|
204
|
+
expectedCallbackParameters,
|
|
167
205
|
tokenRequestHeaders,
|
|
168
206
|
apiOrigins,
|
|
169
207
|
serverUrl: new URL(origins[0]),
|
|
@@ -202,6 +240,29 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
202
240
|
tokenEndpoint: this.settings.tokenEndpoint.href,
|
|
203
241
|
};
|
|
204
242
|
}
|
|
243
|
+
configuredClientInformation() {
|
|
244
|
+
const { settings } = this;
|
|
245
|
+
return {
|
|
246
|
+
client_id: settings.clientId,
|
|
247
|
+
...(settings.clientSecret !== undefined ? { client_secret: settings.clientSecret } : {}),
|
|
248
|
+
redirect_uris: [this.redirectUrl],
|
|
249
|
+
token_endpoint_auth_method: settings.clientAuthentication,
|
|
250
|
+
issuer: settings.identity,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
revocationSnapshot(body) {
|
|
254
|
+
const { settings } = this;
|
|
255
|
+
return {
|
|
256
|
+
body: {
|
|
257
|
+
...body,
|
|
258
|
+
client: { value: this.configuredClientInformation() },
|
|
259
|
+
discovery: {
|
|
260
|
+
authorizationServerUrl: settings.identity,
|
|
261
|
+
authorizationServerMetadata: { revocation_endpoint: settings.revocationEndpoint.href },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
205
266
|
saveClientInformation() {
|
|
206
267
|
return runEdge(authOwnMethod(StaticOAuthProvider.prototype, this, "saveClientInformation", []));
|
|
207
268
|
}
|
|
@@ -245,16 +306,7 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
245
306
|
}
|
|
246
307
|
static {
|
|
247
308
|
registerAuthMethod(StaticOAuthProvider.prototype, "clientInformation", function () {
|
|
248
|
-
return authValue(() =>
|
|
249
|
-
const { settings } = this;
|
|
250
|
-
return {
|
|
251
|
-
client_id: settings.clientId,
|
|
252
|
-
...(settings.clientSecret !== undefined ? { client_secret: settings.clientSecret } : {}),
|
|
253
|
-
redirect_uris: [this.redirectUrl],
|
|
254
|
-
token_endpoint_auth_method: settings.clientAuthentication,
|
|
255
|
-
issuer: settings.identity,
|
|
256
|
-
};
|
|
257
|
-
});
|
|
309
|
+
return authValue(() => this.configuredClientInformation());
|
|
258
310
|
}, StaticOAuthProvider.prototype.clientInformation);
|
|
259
311
|
registerAuthMethod(StaticOAuthProvider.prototype, "saveClientInformation", function () {
|
|
260
312
|
// Nothing to save: the SDK only saves a client it registered or stamped.
|
|
@@ -359,6 +411,10 @@ const readChallengeNative = /* @__PURE__ */ registerAuth(readChallenge, function
|
|
|
359
411
|
/** The grant behind one `api()` connector's `oauth`, as the hooks it exposes. */
|
|
360
412
|
export function staticOAuth(id, config) {
|
|
361
413
|
const settings = settingsFor(id, config);
|
|
414
|
+
const callbackParametersMatch = (params) => settings.expectedCallbackParameters.every(([name, expected]) => {
|
|
415
|
+
const values = params?.getAll(name);
|
|
416
|
+
return values?.length === 1 && values[0] === expected;
|
|
417
|
+
});
|
|
362
418
|
const issuerContext = { issuer: settings.identity };
|
|
363
419
|
const coordinatorFor = refreshCoordinatorsByPartition();
|
|
364
420
|
// Keyed by request scope, so nothing here outlives the request that set it.
|
|
@@ -830,7 +886,25 @@ export function staticOAuth(id, config) {
|
|
|
830
886
|
const disconnectAuth = authMethod((ctx) => {
|
|
831
887
|
return authFlow(function* () {
|
|
832
888
|
rejectedScopes.delete(scopeOf(ctx));
|
|
833
|
-
|
|
889
|
+
if (!settings.revocationEndpoint) {
|
|
890
|
+
(yield authInvoke(providerFor(ctx), "resetAuthorization", [true]));
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
(yield authInvoke(providerFor(ctx), "disconnectAuthorization", [
|
|
894
|
+
authMethod((input, init = {}) => {
|
|
895
|
+
// Register the standard form's credentials before any JSON framing.
|
|
896
|
+
trackRemoteClientRequest(sentSecretsFor(ctx), input, init);
|
|
897
|
+
if (settings.revocationRequestFormat === "json" && init.body instanceof URLSearchParams) {
|
|
898
|
+
const params = new URLSearchParams(init.body);
|
|
899
|
+
params.delete("token_type_hint");
|
|
900
|
+
const headers = new Headers(init.headers);
|
|
901
|
+
headers.set("Content-Type", "application/json");
|
|
902
|
+
headers.set("Accept", "application/json");
|
|
903
|
+
init = { ...init, headers, body: JSON.stringify(Object.fromEntries(params)) };
|
|
904
|
+
}
|
|
905
|
+
return authCall(tokenEndpointFetch, [sentSecretsFor(ctx), input, init]);
|
|
906
|
+
}),
|
|
907
|
+
]));
|
|
834
908
|
});
|
|
835
909
|
});
|
|
836
910
|
const verifyState = authMethod((state, ctx) => {
|
|
@@ -854,6 +928,9 @@ export function staticOAuth(id, config) {
|
|
|
854
928
|
if (!consent || !(yield authInvoke(provider, "verifyCallbackState", [consent]))) {
|
|
855
929
|
throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback matches no pending consent; nothing was exchanged.`);
|
|
856
930
|
}
|
|
931
|
+
if (!callbackParametersMatch(callbackParams)) {
|
|
932
|
+
throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback did not match its configured parameters; nothing was exchanged.`);
|
|
933
|
+
}
|
|
857
934
|
provider.validateCallbackIssuer(callbackParams?.get("iss") ?? null);
|
|
858
935
|
provider.exchanging(authorizationCode);
|
|
859
936
|
(yield authInvoke(provider, "bindFlow", []));
|
|
@@ -877,6 +954,7 @@ export function staticOAuth(id, config) {
|
|
|
877
954
|
startAuth: retainAuthPartition(startAuth, 0),
|
|
878
955
|
disconnectAuth: retainAuthPartition(disconnectAuth, 0),
|
|
879
956
|
verifyState: retainAuthPartition(verifyState, 1),
|
|
957
|
+
verifyCallbackParameters: retainAuthPartition(authMethod((params, _ctx) => authValue(() => callbackParametersMatch(params))), 1),
|
|
880
958
|
verifyCallbackIssuer: retainAuthPartition(authMethod((issuer, ctx) => {
|
|
881
959
|
return authValue(() => {
|
|
882
960
|
const provider = callbackProviders.get(scopeOf(ctx));
|
|
@@ -42,6 +42,10 @@ export interface ApiOAuthConfig {
|
|
|
42
42
|
authorizationEndpoint: string;
|
|
43
43
|
/** Where codes are exchanged and refresh tokens redeemed. Absolute https URL. */
|
|
44
44
|
tokenEndpoint: string;
|
|
45
|
+
/** Trusted RFC 7009 endpoint; disconnect removes the local grant then attempts revocation once. */
|
|
46
|
+
revocationEndpoint?: string;
|
|
47
|
+
/** Revocation body encoding, default form. JSON omits the RFC 7009 token_type_hint. */
|
|
48
|
+
revocationRequestFormat?: "form" | "json";
|
|
45
49
|
/** The client id the provider issued for this deployment. */
|
|
46
50
|
clientId: string;
|
|
47
51
|
/**
|
|
@@ -64,6 +68,15 @@ export interface ApiOAuthConfig {
|
|
|
64
68
|
* `access_type`. The grant's own parameters cannot be overridden.
|
|
65
69
|
*/
|
|
66
70
|
authorizationParams?: Record<string, string>;
|
|
71
|
+
/**
|
|
72
|
+
* Fixed provider callback parameters, such as a configured company id.
|
|
73
|
+
* After pending-state verification, each must occur exactly once with this
|
|
74
|
+
* exact value, before issuer validation or code exchange. OAuth response
|
|
75
|
+
* fields (code, state, iss, error, error_description, error_uri) are reserved.
|
|
76
|
+
* Names must be nonempty ASCII letters, digits, dots, underscores or hyphens;
|
|
77
|
+
* values must be nonempty strings. Values are never described or returned.
|
|
78
|
+
*/
|
|
79
|
+
expectedCallbackParameters?: Readonly<Record<string, string>>;
|
|
67
80
|
/**
|
|
68
81
|
* Headers added to every token-endpoint request — the code exchange and
|
|
69
82
|
* each refresh — for a provider whose token endpoint demands its own
|
|
@@ -199,6 +212,7 @@ export declare const API_OPTIONS: import("../config-schema.js").Field<unknown, u
|
|
|
199
212
|
oauth: import("../config-schema.js").Field<unknown, unknown, true> & {
|
|
200
213
|
readonly shape: {
|
|
201
214
|
authorizationParams: import("../config-schema.js").Field<Record<string, string>, Readonly<Record<string, string>> | undefined, true>;
|
|
215
|
+
expectedCallbackParameters: import("../config-schema.js").Field<Record<string, string>, Readonly<Record<string, string>> | undefined, true>;
|
|
202
216
|
tokenRequestHeaders: import("../config-schema.js").Field<Record<string, string>, Readonly<Record<string, string>> | undefined, true>;
|
|
203
217
|
scope: import("../config-schema.js").Field<unknown, unknown, true>;
|
|
204
218
|
authorizationEndpoint: import("../config-schema.js").Field<unknown, unknown, true>;
|
|
@@ -208,6 +222,8 @@ export declare const API_OPTIONS: import("../config-schema.js").Field<unknown, u
|
|
|
208
222
|
pkce: import("../config-schema.js").Field<unknown, unknown, true>;
|
|
209
223
|
clientId: import("../config-schema.js").Field<unknown, unknown, true>;
|
|
210
224
|
clientSecret: import("../config-schema.js").Field<unknown, unknown, true>;
|
|
225
|
+
revocationEndpoint: import("../config-schema.js").Field<unknown, unknown, true>;
|
|
226
|
+
revocationRequestFormat: import("../config-schema.js").Field<unknown, unknown, true>;
|
|
211
227
|
};
|
|
212
228
|
};
|
|
213
229
|
tools: import("../config-schema.js").Field<unknown[], readonly unknown[] | undefined, true>;
|
|
@@ -233,6 +249,7 @@ export interface ApiOAuthHooks {
|
|
|
233
249
|
}): Promise<ConnectorStatus>;
|
|
234
250
|
disconnectAuth(ctx: ConnectorContext): Promise<void>;
|
|
235
251
|
verifyState(state: string | null, ctx: ConnectorContext): Promise<boolean>;
|
|
252
|
+
verifyCallbackParameters(params: URLSearchParams, ctx: ConnectorContext): Promise<boolean>;
|
|
236
253
|
verifyCallbackIssuer(issuer: string | null, ctx: ConnectorContext): Promise<boolean>;
|
|
237
254
|
consumeAuthError(ctx: ConnectorContext): Promise<void>;
|
|
238
255
|
/** Requires callbackParams with a nonempty state, including without PKCE. */
|
|
@@ -57,8 +57,9 @@ export const API_OPTIONS = optionsOf()({
|
|
|
57
57
|
usageGuide: USAGE_GUIDE,
|
|
58
58
|
credential: CREDENTIAL,
|
|
59
59
|
oauth: optionsOf()({
|
|
60
|
-
...keys("authorizationEndpoint", "tokenEndpoint", "clientId", "clientSecret", "tokenEndpointAuthMethod", "scope", "pkce", "apiOrigins"),
|
|
60
|
+
...keys("authorizationEndpoint", "tokenEndpoint", "clientId", "clientSecret", "tokenEndpointAuthMethod", "scope", "pkce", "apiOrigins", "revocationEndpoint", "revocationRequestFormat"),
|
|
61
61
|
authorizationParams: strings(),
|
|
62
|
+
expectedCallbackParameters: strings(),
|
|
62
63
|
tokenRequestHeaders: strings(),
|
|
63
64
|
}),
|
|
64
65
|
// A tool's annotations stay open: MCP lets a tool carry hints Connecta
|
|
@@ -158,6 +159,7 @@ export function apiConnector(id, opts, oauth) {
|
|
|
158
159
|
// Callback work belongs to its request, including the route's
|
|
159
160
|
// uninterruptible exchange, rather than any resource call.
|
|
160
161
|
verifyState: (state, ctx) => settleOnClose(ctx, () => oauth.verifyState(state, ctx), true),
|
|
162
|
+
verifyCallbackParameters: (params, ctx) => settleOnClose(ctx, () => oauth.verifyCallbackParameters(params, ctx), true),
|
|
161
163
|
verifyCallbackIssuer: (issuer, ctx) => settleOnClose(ctx, () => oauth.verifyCallbackIssuer(issuer, ctx), true),
|
|
162
164
|
consumeAuthError: (ctx) => settleOnClose(ctx, () => oauth.consumeAuthError(ctx), true),
|
|
163
165
|
finishAuth: (code, ctx, callbackParams) => settleOnClose(ctx, () => oauth.finishAuth(code, ctx, callbackParams), true),
|
package/dist/http/client.js
CHANGED
|
@@ -324,7 +324,7 @@ function bodyInit(request) {
|
|
|
324
324
|
return { body: body.body };
|
|
325
325
|
case "Uint8Array":
|
|
326
326
|
// The retained text when there is one, so the wire carries exactly the
|
|
327
|
-
// string guarded-fetch sent
|
|
327
|
+
// string guarded-fetch sent.
|
|
328
328
|
return { body: (body.text ?? body.body) };
|
|
329
329
|
case "FormData":
|
|
330
330
|
return { body: body.formData };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Internal opt-in adapters for the standard hatch.
|
|
2
|
-
//
|
|
1
|
+
// Internal opt-in adapters for the standard hatch. Inputs have crossed
|
|
2
|
+
// connection safety.
|
|
3
3
|
import { ConnectorCallError } from "../errors.js";
|
|
4
4
|
function record(value) {
|
|
5
5
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
package/dist/http/paging.js
CHANGED
|
@@ -1,30 +1,17 @@
|
|
|
1
1
|
// Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
// Stream for programs and the hatch. The codec is the only place a cursor is
|
|
7
|
-
// minted: `plain` hands the vendor's cursor through (what shipped providers
|
|
8
|
-
// promise today, so goldens hold), `bound` ties it to the tool and its
|
|
9
|
-
// arguments so a cursor from another call is refused instead of silently
|
|
10
|
-
// reading the wrong list.
|
|
11
|
-
import { Effect, Option, Schema, Stream } from "effect";
|
|
3
|
+
// List tools take one page at a time. Lookahead detects an extra row; bound
|
|
4
|
+
// cursors authenticate the tool, arguments, connection and credential owner.
|
|
5
|
+
import { Effect, Schema } from "effect";
|
|
12
6
|
import { ConnectorCallError } from "../errors.js";
|
|
13
7
|
import { Refusal } from "./errors.js";
|
|
14
|
-
import { confinementBase } from "../http-policy.js";
|
|
15
8
|
import { canonicalJson } from "../canonical-json.js";
|
|
16
9
|
import { ContinuationBinding, ContinuationStorage } from "../runtime/call.js";
|
|
17
10
|
import { continuationKeys } from "../storage/keys.js";
|
|
18
|
-
/** The `page` object every list tool returns. */
|
|
19
|
-
export const PageInfo = Schema.Struct({
|
|
20
|
-
hasMore: Schema.Boolean,
|
|
21
|
-
nextCursor: Schema.NullOr(Schema.String),
|
|
22
|
-
});
|
|
23
11
|
/**
|
|
24
12
|
* The standard `page` (decision 0011 S2): `nextCursor` is present exactly
|
|
25
13
|
* when `hasMore` is true and continuation is safe. A hatch adapter can report
|
|
26
14
|
* more data without a cursor when its continuation cannot be confined.
|
|
27
|
-
* Step-1 lists keep `PageInfo` until their step 2.
|
|
28
15
|
*/
|
|
29
16
|
export const StandardPage = Schema.Struct({
|
|
30
17
|
hasMore: Schema.Boolean,
|
|
@@ -56,79 +43,6 @@ export function lookahead(options) {
|
|
|
56
43
|
return { rows, page: { hasMore, nextCursor: id } };
|
|
57
44
|
});
|
|
58
45
|
}
|
|
59
|
-
/** Vendor-supplied continuation, read only after the connection made its data safe. */
|
|
60
|
-
export function token(options) {
|
|
61
|
-
return Effect.flatMap(Effect.suspend(options.fetch), ({ rows, hasMore, nextCursor }) => {
|
|
62
|
-
if (hasMore && !nextCursor)
|
|
63
|
-
return badPage("The vendor reported another page without a next cursor.");
|
|
64
|
-
return Effect.succeed({ rows, page: { hasMore, nextCursor: hasMore ? nextCursor : null } });
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
/** Offset paging advances by the rows actually returned, with a total when supplied. */
|
|
68
|
-
export function offset(options) {
|
|
69
|
-
return Effect.gen(function* () {
|
|
70
|
-
yield* position(options.offset, 0);
|
|
71
|
-
yield* position(options.limit, 1);
|
|
72
|
-
return yield* Effect.map(options.fetch(options.offset, options.limit), ({ rows, total }) => {
|
|
73
|
-
const next = options.offset + rows.length;
|
|
74
|
-
const hasMore = rows.length > 0 && (total === undefined ? rows.length >= options.limit : next < total);
|
|
75
|
-
return { rows, page: { hasMore, nextCursor: hasMore ? String(next) : null } };
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
/** Page numbers from a vendor's safe body or headers, with full-page fallback. */
|
|
80
|
-
export function page(options) {
|
|
81
|
-
return Effect.gen(function* () {
|
|
82
|
-
yield* position(options.page, 1);
|
|
83
|
-
yield* position(options.limit, 1);
|
|
84
|
-
return yield* Effect.map(options.fetch(options.page, options.limit), ({ rows, hasMore, totalPages }) => {
|
|
85
|
-
const more = hasMore ?? (totalPages === undefined ? rows.length >= options.limit : options.page < totalPages);
|
|
86
|
-
return { rows, page: { hasMore: more, nextCursor: more ? String(options.page + 1) : null } };
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
/** Extract only a cursor from a next URL confined to this connection's origin and path. */
|
|
91
|
-
export function link(options) {
|
|
92
|
-
const base = confinementBase("Paging", options.baseUrl);
|
|
93
|
-
const prefix = base.pathname.replace(/\/+$/, "");
|
|
94
|
-
if (!options.param)
|
|
95
|
-
throw new Error("Link paging needs a cursor parameter.");
|
|
96
|
-
return Effect.flatMap(Effect.suspend(options.fetch), ({ rows, next }) => {
|
|
97
|
-
if (!next)
|
|
98
|
-
return Effect.succeed({ rows, page: { hasMore: false, nextCursor: null } });
|
|
99
|
-
let url;
|
|
100
|
-
try {
|
|
101
|
-
url = new URL(next, base);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
return badPage("The vendor returned an invalid next-page URL.");
|
|
105
|
-
}
|
|
106
|
-
if (url.origin !== base.origin ||
|
|
107
|
-
!`${url.pathname}/`.startsWith(`${prefix}/`) ||
|
|
108
|
-
url.username ||
|
|
109
|
-
url.password ||
|
|
110
|
-
url.hash)
|
|
111
|
-
return badPage("The vendor returned a next-page URL outside this connection.");
|
|
112
|
-
const cursors = url.searchParams.getAll(options.param);
|
|
113
|
-
if (cursors.length !== 1 || !cursors[0])
|
|
114
|
-
return badPage("The vendor returned no unique next-page cursor.");
|
|
115
|
-
return Effect.succeed({ rows, page: { hasMore: true, nextCursor: cursors[0] } });
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
function position(value, minimum) {
|
|
119
|
-
return Number.isSafeInteger(value) && value >= minimum
|
|
120
|
-
? Effect.void
|
|
121
|
-
: Effect.fail(new Refusal({
|
|
122
|
-
error: new ConnectorCallError("invalid_args", "Paging positions and limits must be whole numbers within their bounds."),
|
|
123
|
-
}));
|
|
124
|
-
}
|
|
125
|
-
function badPage(message) {
|
|
126
|
-
return Effect.fail(new Refusal({ error: new ConnectorCallError("connector_call_failed", message, { retryable: false }) }));
|
|
127
|
-
}
|
|
128
|
-
/** Every row of every page, fetched as the stream is pulled. */
|
|
129
|
-
export function allRows(fetchPage) {
|
|
130
|
-
return Stream.paginate(undefined, (cursor) => Effect.map(fetchPage(cursor), ({ rows, page }) => [rows, Option.fromNullishOr(page.hasMore ? page.nextCursor : null)]));
|
|
131
|
-
}
|
|
132
46
|
const STRAY_CURSOR = "This cursor belongs to a different call. Pass back the nextCursor from the previous page of this same call, with the same arguments.";
|
|
133
47
|
/** Resolved, nonextractable keys only: no request promise is retained between calls. */
|
|
134
48
|
const signingKeys = new WeakMap();
|
package/dist/http/provider.js
CHANGED
|
@@ -191,8 +191,7 @@ export function defineApiProvider(definition) {
|
|
|
191
191
|
// The declared fact the Writes convention states must match what the write builders send.
|
|
192
192
|
if (idempotencyKey !== undefined && [...builtTools, ...hatchTools].some(takesIdempotencyKey) !== idempotencyKey)
|
|
193
193
|
throw new Error(`${metadata.title} declares idempotencyKey: ${idempotencyKey}, but its write tools ${idempotencyKey ? "send no" : "send an"} idempotency key.`);
|
|
194
|
-
const
|
|
195
|
-
const renderedGuide = "content" in usageGuide ? usageGuide : provider.usageGuide(usageGuide);
|
|
194
|
+
const renderedGuide = provider.usageGuide(guide(options));
|
|
196
195
|
const connector = tracksEveryRequest(api(id, {
|
|
197
196
|
...(defaultCallAdmission
|
|
198
197
|
? {
|
package/dist/http/services.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fromSignal } from "../runtime/run.js";
|
|
|
6
6
|
import { callFromContext, ContinuationBinding, ContinuationStorage, openCall as openCoreCall, serviceFor, Signal, } from "../runtime/call.js";
|
|
7
7
|
import { RequestRefused, ResendRefused } from "./errors.js";
|
|
8
8
|
export { writeRequestKey } from "./write-request.js";
|
|
9
|
-
export { Caller, Credentials, Secrets, RequestSecrets, Budget, Recovery, Signal, Classification, Ledger,
|
|
9
|
+
export { Caller, Credentials, Secrets, RequestSecrets, Budget, Recovery, Signal, Classification, Ledger, } from "../runtime/call.js";
|
|
10
10
|
/** Permanent write reservations plus resource dispatches, in reservation order. */
|
|
11
11
|
export class Dispatches extends Context.Service()("connecta/http/Dispatches") {
|
|
12
12
|
}
|
package/dist/http/tools.js
CHANGED
|
@@ -22,7 +22,7 @@ import { causeError, runEdge } from "../runtime/run.js";
|
|
|
22
22
|
import { recordUnauthorized, Refusal, RequestRefused, toConnectorCallError, withheldDefect, } from "./errors.js";
|
|
23
23
|
import { fieldsInput, fieldsProblem, selectFields } from "./fields.js";
|
|
24
24
|
import { catalogSchema, Integer } from "./json-schema.js";
|
|
25
|
-
import { boundCursor, Cursor,
|
|
25
|
+
import { boundCursor, Cursor, StandardPage } from "./paging.js";
|
|
26
26
|
import { Classification, Dispatches, IdempotencyKey, Recovery, ReadTool, WriteCapacity, writeRequestKey, fromConnectorContext, } from "./services.js";
|
|
27
27
|
const inputs = new WeakMap();
|
|
28
28
|
const keyed = new WeakSet();
|
|
@@ -118,28 +118,6 @@ export function get(connection, spec) {
|
|
|
118
118
|
export function list(connection, spec) {
|
|
119
119
|
if (spec.cursor !== undefined && spec.cursor !== "plain" && spec.cursor !== "bound")
|
|
120
120
|
throw new Error('A list cursor must be "plain" or "bound".');
|
|
121
|
-
if (!("key" in spec))
|
|
122
|
-
return standardList(connection, spec);
|
|
123
|
-
if (!spec.key || spec.key === "page")
|
|
124
|
-
throw new Error("A list needs a row key distinct from page.");
|
|
125
|
-
return read(connection, {
|
|
126
|
-
...spec,
|
|
127
|
-
output: Schema.Struct({ [spec.key]: Schema.Array(spec.row), page: PageInfo }),
|
|
128
|
-
run: (args) => Effect.gen(function* () {
|
|
129
|
-
const fields = args;
|
|
130
|
-
const supplied = typeof fields.cursor === "string" ? fields.cursor : undefined;
|
|
131
|
-
const cursor = supplied !== undefined && spec.cursor === "bound"
|
|
132
|
-
? yield* boundCursor.decode(spec.name, fields, supplied)
|
|
133
|
-
: supplied;
|
|
134
|
-
const result = yield* spec.page(args, cursor);
|
|
135
|
-
const nextCursor = result.page.nextCursor !== null && spec.cursor === "bound"
|
|
136
|
-
? yield* boundCursor.encode(spec.name, fields, result.page.nextCursor)
|
|
137
|
-
: result.page.nextCursor;
|
|
138
|
-
return { [spec.key]: result.rows, page: { ...result.page, nextCursor } };
|
|
139
|
-
}),
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
function standardList(connection, spec) {
|
|
143
121
|
const { default: size, max } = spec.limit;
|
|
144
122
|
if (spec.total !== undefined && spec.total !== true)
|
|
145
123
|
throw new Error(`${spec.name} total must opt in with true for a vendor-stated exact count.`);
|
|
@@ -264,10 +242,6 @@ export function write(connection, spec) {
|
|
|
264
242
|
...(spec.idempotent === undefined ? {} : { idempotentHint: spec.idempotent }),
|
|
265
243
|
}, spec.idempotency);
|
|
266
244
|
}
|
|
267
|
-
/** Money, mail, and refunds are always destructive and never idempotent hints. */
|
|
268
|
-
export function action(connection, spec) {
|
|
269
|
-
return build(connection, spec, "write", { readOnlyHint: false, destructiveHint: true, idempotentHint: false });
|
|
270
|
-
}
|
|
271
245
|
/**
|
|
272
246
|
* A key the transport may send: not blank, and only characters an HTTP
|
|
273
247
|
* header carries. The REST layer refuses the same keys before dispatch.
|
|
@@ -2,6 +2,7 @@ import type { ActivityOutcome } from "../activity.js";
|
|
|
2
2
|
import type { ConnectaConfigDescription } from "../describe-config.js";
|
|
3
3
|
import type { ConnectorStatus, ConnectorToolDescription } from "../types.js";
|
|
4
4
|
import type { UiProblem } from "./model.js";
|
|
5
|
+
export type OperatorPage = "overview" | "endpoints" | "settings" | "connector" | "tokens" | "connections" | "activity";
|
|
5
6
|
/** The registry's final verdict, independent of whether the viewer's endpoint is read-only. */
|
|
6
7
|
export interface OperatorTool extends ConnectorToolDescription {
|
|
7
8
|
address: string;
|