@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +8 -4
  3. package/bin/version.mjs +1 -1
  4. package/dist/auth/downstream-oauth.js +71 -79
  5. package/dist/auth/static-oauth.js +104 -26
  6. package/dist/connectors/api-connector.d.ts +17 -0
  7. package/dist/connectors/api-connector.js +3 -1
  8. package/dist/http/client.js +1 -1
  9. package/dist/http/hatch-paging.js +2 -2
  10. package/dist/http/paging.js +3 -89
  11. package/dist/http/provider.js +1 -2
  12. package/dist/http/services.js +1 -1
  13. package/dist/http/tools.js +1 -27
  14. package/dist/operator-ui/contract.d.ts +1 -0
  15. package/dist/operator-ui/generated.js +4 -4
  16. package/dist/program-writes.js +0 -1
  17. package/dist/providers/_shared/google/kit.js +29 -139
  18. package/dist/providers/_shared/rest/kit.js +4 -404
  19. package/dist/providers/_shared/sensitive-fields.js +0 -16
  20. package/dist/providers/breeze/api.js +2 -2
  21. package/dist/providers/ccb/api.js +1 -1
  22. package/dist/providers/cloudflare/api.js +8 -8
  23. package/dist/providers/cloudflare/lib/config.d.ts +2 -1
  24. package/dist/providers/docs/api.js +1 -1
  25. package/dist/providers/docs/tools.js +1 -2
  26. package/dist/providers/drive/api.js +2 -2
  27. package/dist/providers/drive/lib/hatch.js +1 -1
  28. package/dist/providers/drive/tools.js +1 -1
  29. package/dist/providers/forms/api.js +1 -1
  30. package/dist/providers/forms/lib/hatch.js +1 -1
  31. package/dist/providers/forms/tools.js +2 -3
  32. package/dist/providers/github/api.js +1 -1
  33. package/dist/providers/github/lib/discovery.js +2 -2
  34. package/dist/providers/gmail/api.js +1 -1
  35. package/dist/providers/gmail/tools.js +2 -2
  36. package/dist/providers/granola/api.js +155 -0
  37. package/dist/providers/granola/index.d.ts +10 -0
  38. package/dist/providers/granola/index.js +68 -0
  39. package/dist/providers/granola/openapi.generated.js +30 -0
  40. package/dist/providers/granola/safety.js +37 -0
  41. package/dist/providers/granola/skill.generated.js +5 -0
  42. package/dist/providers/granola/tools.js +2 -0
  43. package/dist/providers/overflow/api.js +26 -26
  44. package/dist/providers/planning-center/api.js +4 -4
  45. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  46. package/dist/providers/planning-center/lib/json-api.js +2 -2
  47. package/dist/providers/quickbooks/api.js +299 -0
  48. package/dist/providers/quickbooks/index.d.ts +13 -0
  49. package/dist/providers/quickbooks/index.js +93 -0
  50. package/dist/providers/quickbooks/lib/query.js +118 -0
  51. package/dist/providers/quickbooks/lib/writes.js +168 -0
  52. package/dist/providers/quickbooks/safety.js +222 -0
  53. package/dist/providers/quickbooks/skill.generated.js +5 -0
  54. package/dist/providers/quickbooks/tools.js +117 -0
  55. package/dist/providers/sheets/api.js +1 -1
  56. package/dist/providers/sheets/lib/hatch.js +2 -2
  57. package/dist/providers/sheets/lib/requests.js +1 -1
  58. package/dist/providers/sheets/tools.js +1 -2
  59. package/dist/providers/slides/api.js +1 -1
  60. package/dist/providers/slides/tools.js +2 -2
  61. package/dist/providers/stripe/api.js +7 -7
  62. package/dist/providers/tithely/api.js +2 -2
  63. package/dist/providers/vercel/api.js +12 -12
  64. package/dist/providers/vercel/index.js +1 -2
  65. package/dist/providers/vercel/lib/pin.js +1 -1
  66. package/dist/routes/mcp.js +1 -1
  67. package/dist/routes/oauth.js +3 -0
  68. package/dist/runtime/admission.js +0 -12
  69. package/dist/runtime/call.js +0 -4
  70. package/dist/skills.js +0 -1
  71. package/dist/storage/keys.js +2 -3
  72. package/dist/types.d.ts +2 -0
  73. package/dist/ui.d.ts +1 -1
  74. package/dist/ui.js +0 -1
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/documentation/README.md +1 -0
  78. package/documentation/architecture.md +249 -366
  79. package/documentation/auth.md +56 -19
  80. package/documentation/code-mode.md +6 -6
  81. package/documentation/integrating.md +1 -1
  82. package/documentation/meta-tools.md +6 -3
  83. package/documentation/operator-tests.md +11 -2
  84. package/documentation/operator-ui.md +17 -5
  85. package/documentation/providers.md +492 -0
  86. package/package.json +12 -9
  87. package/templates/node/package.json +1 -1
  88. package/dist/operator-ui/agents.d.ts +0 -75
  89. package/dist/operator-ui/agents.js +0 -114
  90. package/dist/operator-ui/config-summary.js +0 -566
  91. package/dist/operator-ui/connection-map.js +0 -378
  92. package/dist/operator-ui/endpoints.js +0 -212
  93. package/dist/operator-ui/fix-prompts.d.ts +0 -18
  94. package/dist/operator-ui/fix-prompts.js +0 -79
  95. package/dist/operator-ui/health.js +0 -52
  96. package/dist/operator-ui/member-page.js +0 -97
  97. package/dist/operator-ui/overview.js +0 -126
  98. package/dist/operator-ui/setup-commands.js +0 -47
  99. package/dist/operator-ui/view.d.ts +0 -378
  100. package/dist/operator-ui/view.js +0 -631
  101. package/dist/providers/_shared/rest/dispatch.js +0 -21
  102. 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
- throw new Error(`[connecta] connector "${id}" oauth.tokenEndpoint must not carry a query.`);
119
+ invalid(`tokenEndpoint must not carry a query.`);
99
120
  }
100
121
  if (typeof config.clientId !== "string" || config.clientId.trim() === "") {
101
- throw new Error(`[connecta] connector "${id}" oauth.clientId must be a non-empty string.`);
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
- throw new Error(`[connecta] connector "${id}" oauth.clientSecret must be a non-empty string when set.`);
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
- throw new Error(`[connecta] connector "${id}" oauth.tokenEndpointAuthMethod must be ` +
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
- throw new Error(`[connecta] connector "${id}" oauth.tokenEndpointAuthMethod ` + `"${clientAuthentication}" needs a clientSecret.`);
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
- throw new Error(`[connecta] connector "${id}" oauth.authorizationParams may not set ` +
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
- throw new Error(`[connecta] connector "${id}" oauth.authorizationParams.${name} must be a string.`);
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
- throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders has an invalid header name.`);
170
+ invalid(`tokenRequestHeaders has an invalid header name.`);
135
171
  }
136
172
  if (RESERVED_TOKEN_HEADERS.has(name.toLowerCase())) {
137
- throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders may not set ` + `"${name}"; the token request owns it.`);
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
- throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders.${name} must be a single-line string.`);
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
- throw new Error(`[connecta] connector "${id}" oauth.apiOrigins must name at least one ` +
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
- throw new Error(`[connecta] connector "${id}" oauth.apiOrigins entries must be exact ` +
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
- (yield authInvoke(providerFor(ctx), "resetAuthorization", [true]));
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),
@@ -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 and goldens record the same body.
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. No maintained provider opts
2
- // in until its separate step-2 migration. Inputs have crossed connection safety.
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 : {};
@@ -1,30 +1,17 @@
1
1
  // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
2
2
  //
3
- // Paging is a strategy plus one cursor codec. A strategy turns "the next page
4
- // after this cursor" into a vendor request and reads back rows and the next
5
- // vendor cursor; tools take one page, and `allRows` walks every page as a
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();
@@ -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 usageGuide = guide(options);
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
  ? {
@@ -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, openRequest, operationView, deriveView, viewCaller, callView, chargeView, } from "../runtime/call.js";
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
  }
@@ -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, PageInfo, StandardPage } from "./paging.js";
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;