@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
@@ -4,7 +4,6 @@ import { CONFIG_DEFAULTS } from "./config-defaults.js";
4
4
  import { Effect } from "effect";
5
5
  import { jsonResult } from "./meta-tools.js";
6
6
  import { classifyWriteOutcome } from "./write-outcome.js";
7
- export { classifyWriteOutcome } from "./write-outcome.js";
8
7
  /** Writes one program may send (`execute.maxWrites`), unless configured. */
9
8
  export const DEFAULT_MAX_WRITES = CONFIG_DEFAULTS.execute.maxWrites;
10
9
  /** What a dispatched write's outcome says about whether it landed. */
@@ -5,17 +5,14 @@
5
5
  // This module adds what every product needs: the Google error
6
6
  // table, the delegated machine grant, a value-safety resolver over the
7
7
  // product's own operations, a request adapter that keeps the legacy request
8
- // bytes and write wording, and the step-1 catalog bridge.
9
- import { Effect, Option, Schema } from "effect";
10
- import { callClassification } from "../../../call-classification.js";
8
+ // bytes, and failure phases for product recovery.
9
+ import { Effect, Option } from "effect";
11
10
  import { deriveContext } from "../../../derived-context.js";
12
11
  import { ConnectorCallError } from "../../../errors.js";
13
- import { attachFailureFacts } from "../../../operator-record.js";
14
12
  import { Refusal, StatusFailure, toConnectorCallError, } from "../../../http/errors.js";
15
13
  import { Classification, Dispatches, Signal } from "../../../http/services.js";
16
- import { read, write } from "../../../http/tools.js";
17
14
  import { callMemo, Operation } from "../../../runtime/call.js";
18
- import { providerFailureMessage, unsentAfterUnauthorized } from "../../../write-outcome.js";
15
+ import { providerFailureMessage } from "../../../write-outcome.js";
19
16
  import { delegatedToken, forgetDelegatedToken } from "./service-account.js";
20
17
  import { subjectFor, workspaceConnection } from "./workspace.js";
21
18
  import { tagWorkspaceSubject } from "../../../workspace-subject.js";
@@ -349,40 +346,18 @@ function queryPairs(query) {
349
346
  }
350
347
  return pairs;
351
348
  }
352
- /**
353
- * Words the legacy client gave a served write whose answer proves nothing:
354
- * no answer, a 5xx, a redirect, or a 2xx whose reply could not be read. Only
355
- * the message and retry flag are its; the core still decides the outcome,
356
- * which for each of these is unknown.
357
- */
358
- function unknownWrite(message, status) {
359
- return new Refusal({
360
- error: attachFailureFacts(new ConnectorCallError("connector_call_failed", message, { retryable: false }), status === undefined ? {} : { httpStatus: status }),
361
- });
362
- }
363
- export function googleClient(connection, provider, configuration = {}) {
364
- if (configuration.standard || configuration.index)
365
- requireGoogleIndex(configuration.index);
349
+ export function googleClient(connection, index) {
350
+ requireGoogleIndex(index);
366
351
  const send = (request, accept, options, binary = false) => Effect.gen(function* () {
367
- if (configuration.index) {
368
- yield* Effect.try({
369
- try: () => {
370
- if (!request.template)
371
- throw new ConnectorCallError("invalid_args", "This indexed Google named request needs its declared operation template.");
372
- googleNamedOperation(configuration.index, request.method, request.path, request.template);
373
- },
374
- catch: (error) => new Refusal({ error: error }),
375
- });
376
- }
377
- const active = yield* Effect.serviceOption(Operation);
378
- const callWrite = (yield* Classification) === "write";
352
+ yield* Effect.try({
353
+ try: () => {
354
+ if (!request.template)
355
+ throw new ConnectorCallError("invalid_args", "This indexed Google named request needs its declared operation template.");
356
+ googleNamedOperation(index, request.method, request.path, request.template);
357
+ },
358
+ catch: (error) => new Refusal({ error: error }),
359
+ });
379
360
  const dispatches = yield* Dispatches;
380
- const signal = yield* Signal;
381
- // Match the legacy wording rule even for direct calls: their GET
382
- // lookups have no served verdict. Physical write accounting stays the
383
- // core's Classification; only these Google-specific words use this.
384
- const served = Option.isSome(active) && active.value ? callClassification(active.value) : undefined;
385
- const write = (served ?? (request.method === "GET" ? "read" : "write")) === "write";
386
361
  const operation = yield* connection.safety.resolve(request.method, request.path);
387
362
  return yield* connection
388
363
  .sendPath({
@@ -406,73 +381,27 @@ export function googleClient(connection, provider, configuration = {}) {
406
381
  ...(options?.effect ? { effect: options.effect } : {}),
407
382
  ...(options?.step ? { step: options.step } : {}),
408
383
  })
409
- .pipe(Effect.catch((failure) => Effect.fail(settled(failure, dispatches.find((entry) => entry.failure === failure), write, callWrite, signal))));
384
+ .pipe(Effect.catch((failure) => Effect.fail(settled(failure, dispatches.find((entry) => entry.failure === failure)))));
410
385
  });
411
- function settled(failure, own, write, callWrite, signal) {
412
- // A lookup inside a write call has not sent the write. Keep the lookup's
413
- // own verdict; only a call-owned request gets uncertain-write wording.
414
- write = write && own?.request?.auxiliary !== true;
386
+ function settled(failure, own) {
415
387
  const status = own?.status;
416
388
  const dispatched = own?.request?.dispatched === true;
417
- const at = (phase, code = status) => ({ dispatched, phase, ...(code === undefined ? {} : { status: code }) });
418
389
  if (!dispatched)
419
390
  return noted(failure, { dispatched: false, phase: "before-send" });
420
- if (configuration.standard) {
421
- // Preserve phase evidence for partial-create recovery; settlement and
422
- // reconciliation wording belong to the kit in standard mode.
423
- const phase = failure._tag === "TransportFailure"
424
- ? "awaiting-response"
425
- : failure._tag === "UnreadableBody" || failure._tag === "ResponseTooLarge"
426
- ? "reading-body"
427
- : failure._tag === "RedirectRefused" || (status !== undefined && status >= 300 && status < 400)
428
- ? "redirected"
429
- : status === 401
430
- ? "unauthorized"
431
- : status !== undefined && status >= 500
432
- ? "server-error"
433
- : "refused";
434
- return noted(failure, at(phase));
435
- }
436
- switch (failure._tag) {
437
- case "StatusFailure": {
438
- const code = failure.status;
439
- if (code >= 300 && code < 400)
440
- return noted(write
441
- ? unknownWrite(`${provider} answered HTTP ${code} with a redirect, which this connection does not follow, so whether the request was applied is unknown. Re-read its target before repeating it.`, status)
442
- : failure, at("redirected", code));
443
- if (write && code >= 500)
444
- return noted(unknownWrite(`${provider} answered HTTP ${code} after receiving the request, so its outcome is unknown — it may have been applied. Re-read its target before repeating it.`, status), at("server-error", code));
445
- if (callWrite && own?.request?.auxiliary !== true && code === 401) {
446
- // Never sent again (INV-9); the next call mints a fresh token.
447
- const refusal = toConnectorCallError(failure, connection.errors, {
448
- classification: "write",
449
- dispatches: [own],
450
- withholds: connection.safety.withholdsErrors,
451
- signal,
452
- });
453
- return noted(new Refusal({ error: unsentAfterUnauthorized(provider, refusal) }), at("unauthorized", 401));
454
- }
455
- return noted(failure, at("refused", code));
456
- }
457
- case "RedirectRefused":
458
- return noted(write
459
- ? unknownWrite(`${provider} answered HTTP ${failure.status} with a redirect, which this connection does not follow, so whether the request was applied is unknown. Re-read its target before repeating it.`, status)
460
- : failure, at("redirected", failure.status));
461
- case "TransportFailure":
462
- return noted(write
463
- ? unknownWrite(`${provider} was sent the request but no answer came back, so it may or may not have been applied. Re-read its target before repeating it.`, status)
464
- : failure, at("awaiting-response", undefined));
465
- case "ResponseTooLarge":
466
- case "UnreadableBody":
467
- // An error status keeps its meaning whatever went wrong reading it.
468
- if (status !== undefined && status >= 400)
469
- return noted(failure, at(write && status >= 500 ? "server-error" : "refused"));
470
- return noted(write
471
- ? unknownWrite(`${provider} accepted the request but its reply could not be read; the change probably applied. Re-read before repeating it.`, status)
472
- : failure, at("reading-body"));
473
- default:
474
- return noted(failure, at("refused"));
475
- }
391
+ // Preserve phase evidence for partial-create recovery; settlement and
392
+ // reconciliation wording belong to the kit.
393
+ const phase = failure._tag === "TransportFailure"
394
+ ? "awaiting-response"
395
+ : failure._tag === "UnreadableBody" || failure._tag === "ResponseTooLarge"
396
+ ? "reading-body"
397
+ : failure._tag === "RedirectRefused" || (status !== undefined && status >= 300 && status < 400)
398
+ ? "redirected"
399
+ : status === 401
400
+ ? "unauthorized"
401
+ : status !== undefined && status >= 500
402
+ ? "server-error"
403
+ : "refused";
404
+ return noted(failure, { dispatched, phase, ...(status === undefined ? {} : { status }) });
476
405
  }
477
406
  return {
478
407
  json: (request, options) => Effect.map(send(request, "application/json", options), (answer) => answer.data),
@@ -496,42 +425,3 @@ export function googleFailureMessage(connection, failure, message) {
496
425
  return yield* Effect.fail(outcome ? noted(reworded, outcome) : reworded);
497
426
  });
498
427
  }
499
- /** Arguments arrive validated against the exact legacy `inputSchema` by `api()` before any handler runs. */
500
- const ARGUMENTS = Schema.Record(Schema.String, Schema.Unknown);
501
- /**
502
- * The step-1 catalog bridge. The tool is a kit `read` or `write` (its
503
- * lease, write capacity, transport, failure mapping and result safety are
504
- * the kit's), but its catalog keeps the legacy declaration's bytes:
505
- * annotations, input and output schemas. `api()` validates every call, from
506
- * every entry path, against that exact input schema before the handler runs
507
- * (`callTool` in `src/connectors/api-connector.ts`, fail-closed), so the
508
- * decoder accepts whatever reaches it and keeps the caller's key order. Step
509
- * 2 replaces these schemas with Effect declarations (0011).
510
- */
511
- export function googleTool(connection, declaration) {
512
- const { annotations } = declaration;
513
- const spec = {
514
- name: declaration.name,
515
- description: declaration.description,
516
- input: ARGUMENTS,
517
- output: Schema.Unknown,
518
- ...(declaration.writeSteps ? { writeSteps: declaration.writeSteps } : {}),
519
- run: (args) => Effect.gen(function* () {
520
- const operation = yield* Effect.serviceOption(Operation);
521
- const ctx = Option.isSome(operation) ? operation.value : undefined;
522
- if (!ctx)
523
- return yield* Effect.die(new Error(`${declaration.name} needs the call's connector context.`));
524
- return yield* declaration.run(args, ctx);
525
- }).pipe(
526
- // A product's own refusals are thrown ConnectorCallErrors, as on the legacy client.
527
- Effect.catchDefect((defect) => defect instanceof ConnectorCallError ? Effect.fail(new Refusal({ error: defect })) : Effect.die(defect))),
528
- };
529
- const built = annotations.readOnlyHint === true && annotations.destructiveHint !== true
530
- ? read(connection, spec)
531
- : write(connection, { ...spec, destructive: annotations.destructiveHint === true });
532
- return Object.assign(built, {
533
- annotations: declaration.annotations,
534
- inputSchema: declaration.inputSchema,
535
- outputSchema: declaration.outputSchema,
536
- });
537
- }
@@ -1,17 +1,9 @@
1
- // Step-1 REST bridge: pinned contracts and legacy hatch bytes on the provider kit.
2
- // The index remains the hatch validator; every network answer comes from Connection.
1
+ // Shared REST call shapes, pinned-schema conversion and indexed vendor declarations.
3
2
  import { Effect, Schema } from "effect";
4
- import { IDEMPOTENCY_KEY_MAX_LENGTH } from "../../../call-recovery.js";
5
- import { ConnectorCallError, keyResendAdvice } from "../../../errors.js";
6
- import { providerFailureMessage } from "../../../write-outcome.js";
7
- import { Refusal, toConnectorCallError } from "../../../http/errors.js";
3
+ import { ConnectorCallError } from "../../../errors.js";
4
+ import { Refusal } from "../../../http/errors.js";
8
5
  import { Integer, isMinChars, isMaxChars } from "../../../http/json-schema.js";
9
- import { Classification, Dispatches, IdempotencyKey, Recovery, Signal, } from "../../../http/services.js";
10
- import { read, write } from "../../../http/tools.js";
11
6
  import { vendor as declareVendor, get, head, post, put, patch, del, } from "../../../http/vendor.js";
12
- const MAX_SELECT = 50;
13
- const READ = { readOnlyHint: true };
14
- const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
15
7
  function isRecord(value) {
16
8
  return !!value && typeof value === "object" && !Array.isArray(value);
17
9
  }
@@ -36,7 +28,7 @@ export function typedParams(params) {
36
28
  return [name, value];
37
29
  }));
38
30
  }
39
- /** Only the JSON forms the pinned index and legacy hatch catalogs publish. No vendor encoders run here. */
31
+ /** Convert the JSON forms published by pinned indexes. No vendor encoders run here. */
40
32
  export function schema(node) {
41
33
  const n = node;
42
34
  let out;
@@ -152,395 +144,3 @@ export function indexedVendor(name, index, named = []) {
152
144
  indexed: { operations: index.ops.map((op) => `${op.method} ${op.path}`).sort(), api: () => (built ??= build()) },
153
145
  };
154
146
  }
155
- function project(value, paths) {
156
- const root = new Map();
157
- for (const path of paths) {
158
- let node = root;
159
- const parts = path.split(".");
160
- for (const [index, part] of parts.entries()) {
161
- const existing = node.get(part);
162
- // A shorter path already keeps this whole subtree.
163
- if (existing === true)
164
- break;
165
- if (index === parts.length - 1) {
166
- node.set(part, true);
167
- break;
168
- }
169
- const next = existing ?? new Map();
170
- node.set(part, next);
171
- node = next;
172
- }
173
- }
174
- const pick = (current, tree) => {
175
- if (Array.isArray(current))
176
- return current.map((item) => pick(item, tree));
177
- if (!isRecord(current))
178
- return current;
179
- const out = {};
180
- for (const [key, child] of tree) {
181
- if (!Object.hasOwn(current, key))
182
- continue;
183
- out[key] = child === true ? current[key] : pick(current[key], child);
184
- }
185
- return out;
186
- };
187
- return pick(value, root);
188
- }
189
- function selected(args, result) {
190
- const select = args["select"];
191
- if (!Array.isArray(select) || select.length === 0)
192
- return result;
193
- return { ...result, data: project(result.data, select) };
194
- }
195
- function pathSchema(example = "/v1/customers/cus_123") {
196
- return {
197
- type: "string",
198
- minLength: 2,
199
- maxLength: 2048,
200
- description: `Concrete path from search with ids filled in, e.g. ${example}. No query string.`,
201
- };
202
- }
203
- const QUERY = {
204
- type: "object",
205
- description: "Query parameters as JSON, named as details lists them.",
206
- };
207
- const SELECT = {
208
- type: "array",
209
- maxItems: MAX_SELECT,
210
- items: { type: "string", minLength: 1, maxLength: 200 },
211
- description: "Dot paths to keep from data, e.g. data.id; a path through a list applies to each item.",
212
- };
213
- const PAGE = {
214
- type: "object",
215
- description: "Present on lists: pass next as the param query parameter (a body field when in is body) for the next page. hasMore without next: no cursor exists.",
216
- properties: {
217
- hasMore: { type: "boolean" },
218
- next: { type: "string" },
219
- param: { type: "string" },
220
- in: { type: "string", enum: ["query", "body"] },
221
- path: { type: "string", description: "GET this path for the next page, when it differs from this call's." },
222
- },
223
- required: ["hasMore"],
224
- };
225
- function envelope(extra = {}) {
226
- return {
227
- type: "object",
228
- properties: {
229
- status: { type: "integer", description: "HTTP status." },
230
- data: {
231
- description: "Response body: JSON, text, NDJSON rows, { contentType, bytes, base64 }, or HEAD's headers.",
232
- },
233
- page: PAGE,
234
- ...extra,
235
- },
236
- required: ["status", "data"],
237
- };
238
- }
239
- function closed(properties, required) {
240
- return { type: "object", properties, required, additionalProperties: false };
241
- }
242
- /** Restore declaration key order only after proving the kit generated the identical schema. */
243
- export function catalogOrder(actual, legacy) {
244
- if (Array.isArray(legacy)) {
245
- if (!Array.isArray(actual) || actual.length !== legacy.length)
246
- throw new Error("Kit catalog array differs from the legacy declaration.");
247
- return legacy.map((value, index) => catalogOrder(actual[index], value));
248
- }
249
- if (isRecord(legacy)) {
250
- if (!isRecord(actual) ||
251
- Object.keys(actual).length !== Object.keys(legacy).length ||
252
- Object.keys(legacy).some((key) => !Object.hasOwn(actual, key)))
253
- throw new Error("Kit catalog keys differ from the legacy declaration.");
254
- return Object.fromEntries(Object.entries(legacy).map(([key, value]) => [key, catalogOrder(actual[key], value)]));
255
- }
256
- if (actual !== legacy)
257
- throw new Error("Kit catalog value differs from the legacy declaration.");
258
- return actual;
259
- }
260
- /** Four legacy tools, with kit leases, capacity, mapping and dispatch. */
261
- export function restKitTools(vendor) {
262
- const writeSteps = vendor.writeSteps === undefined ? undefined : Object.freeze({ ...vendor.writeSteps });
263
- const { index, title } = vendor;
264
- const tool = (kind) => `${vendor.vendor}_api_${kind}`;
265
- const PATH = pathSchema(vendor.pathExample);
266
- const headerNames = Object.keys(vendor.headers ?? {});
267
- const HEADERS = headerNames.length
268
- ? {
269
- headers: {
270
- type: "object",
271
- description: "Optional request headers this vendor's operations read; nothing else is accepted.",
272
- properties: Object.fromEntries(Object.entries(vendor.headers).map(([name, description]) => [
273
- name,
274
- { type: "string", minLength: 1, maxLength: 200, description },
275
- ])),
276
- additionalProperties: false,
277
- },
278
- }
279
- : {};
280
- const headersOf = (args) => isRecord(args["headers"]) ? { headers: args["headers"] } : {};
281
- const readPosts = vendor.readPosts ?? [];
282
- for (const [method, path] of readPosts) {
283
- // A reviewed exception the index no longer carries is a stale review.
284
- if (!index.operation(method, path)) {
285
- throw new Error(`${title} reviewed read-only ${method} ${path} is not in the pinned API index.`);
286
- }
287
- }
288
- const reads = (op) => op.method === "GET" ||
289
- op.method === "HEAD" ||
290
- readPosts.some(([method, path]) => method === op.method && path === op.path);
291
- const writeMethods = index.methods.filter((method) => method !== "GET" && method !== "HEAD");
292
- // The methods an idempotency key is sent with: never DELETE (see the handler).
293
- const keyed = vendor.idempotencyHeader ? writeMethods.filter((method) => method !== "DELETE") : [];
294
- const keyedText = keyed.join(" or ");
295
- const head = index.methods.includes("HEAD");
296
- const readMethods = [...new Set(["GET", ...(head ? ["HEAD"] : []), ...readPosts.map(([method]) => method)])];
297
- const routeOf = (op) => (reads(op) ? tool("read") : tool("write"));
298
- const guard = (call) => {
299
- const server = vendor.checkServer?.(call.op.server);
300
- if (server)
301
- invalid(server);
302
- const refusal = vendor.valueSafety.refusal(call.method, call.op.path) ?? vendor.refuse?.(call);
303
- if (refusal)
304
- invalid(refusal);
305
- };
306
- const declarations = [
307
- {
308
- name: tool("search"),
309
- description: `Find ${title} API operations by keyword or path in the pinned API index. Returns method, path, and summary; reads no account data.`,
310
- annotations: READ,
311
- inputSchema: closed({
312
- query: {
313
- type: "string",
314
- minLength: 1,
315
- maxLength: 200,
316
- description: "Keywords or a path, e.g. refund charge.",
317
- },
318
- method: { type: "string", enum: [...index.methods], description: "Only operations with this method." },
319
- limit: { type: "integer", minimum: 1, maximum: 50, description: "Most operations to return; default 10." },
320
- }, ["query"]),
321
- outputSchema: {
322
- type: "object",
323
- properties: {
324
- operations: {
325
- type: "array",
326
- items: {
327
- type: "object",
328
- properties: {
329
- method: { type: "string" },
330
- path: { type: "string" },
331
- summary: { type: "string" },
332
- tool: { type: "string", description: "The tool that calls it." },
333
- },
334
- required: ["method", "path", "tool"],
335
- },
336
- },
337
- revision: { type: "string", description: "Pinned index revision." },
338
- },
339
- required: ["operations", "revision"],
340
- },
341
- },
342
- {
343
- name: tool("details"),
344
- description: `Read one ${title} API operation's request contract: path, query, and body parameters with types, enums, and required names. Reads no account data.`,
345
- annotations: READ,
346
- inputSchema: closed({
347
- method: { type: "string", enum: [...index.methods], description: "The operation's HTTP method." },
348
- path: { ...PATH, description: "The path template from search, or a concrete path." },
349
- param: { type: "string", minLength: 1, maxLength: 200, description: "Return only this parameter." },
350
- }, ["method", "path"]),
351
- outputSchema: {
352
- type: "object",
353
- properties: {
354
- method: { type: "string" },
355
- path: { type: "string" },
356
- tool: { type: "string", description: "The tool that calls it." },
357
- parameters: { type: "array", description: "Path and query parameters." },
358
- body: {
359
- type: "object",
360
- description: "Body content type and schema; truncated: true marks unexpanded levels.",
361
- },
362
- revision: { type: "string" },
363
- },
364
- required: ["method", "path", "tool", "parameters", "revision"],
365
- },
366
- },
367
- {
368
- name: tool("read"),
369
- description: `Call a ${title} API GET${head ? " or HEAD" : ""} operation${readPosts.length ? ", or a reviewed read-only POST" : ""}. ` +
370
- "Arguments are checked against the pinned index before anything is sent.",
371
- annotations: READ,
372
- inputSchema: closed({
373
- path: PATH,
374
- ...(readMethods.length > 1
375
- ? {
376
- method: {
377
- type: "string",
378
- enum: readMethods,
379
- description: "GET by default; another method only for an operation search routes here.",
380
- },
381
- }
382
- : {}),
383
- ...(readPosts.length
384
- ? {
385
- body: {
386
- type: vendor.textBodies ? ["object", "string"] : "object",
387
- description: `Body for a reviewed read-only POST${vendor.textBodies ? "; a string for a text-framed operation" : ""}.`,
388
- },
389
- }
390
- : {}),
391
- query: QUERY,
392
- ...HEADERS,
393
- select: SELECT,
394
- }, ["path"]),
395
- outputSchema: envelope(),
396
- },
397
- {
398
- name: tool("write"),
399
- description: `Call a ${title} API write (${writeMethods.join(", ")}); method and path are stated for approval. ` +
400
- `Checked against the pinned index first.${keyed.length ? ` Pass idempotencyKey to retry a ${keyedText} safely${keyed.length < writeMethods.length ? "; a DELETE sends no key" : ""}.` : ""}`,
401
- annotations: DESTRUCTIVE,
402
- inputSchema: closed({
403
- method: { type: "string", enum: writeMethods, description: "The operation's HTTP method." },
404
- path: PATH,
405
- query: QUERY,
406
- body: {
407
- type: vendor.textBodies ? ["object", "array", "string"] : ["object", "array"],
408
- description: `Request body as JSON${vendor.bodyHint ? `; ${vendor.bodyHint}` : ""}.`,
409
- },
410
- ...HEADERS,
411
- ...(vendor.idempotencyHeader
412
- ? {
413
- idempotencyKey: {
414
- type: "string",
415
- minLength: 1,
416
- maxLength: IDEMPOTENCY_KEY_MAX_LENGTH,
417
- description: `Reuse to retry this exact ${keyedText}; one is generated and returned when omitted.`,
418
- },
419
- }
420
- : {}),
421
- select: SELECT,
422
- }, ["method", "path"]),
423
- outputSchema: envelope(vendor.idempotencyHeader
424
- ? {
425
- idempotencyKey: {
426
- type: "string",
427
- description: "The key sent; reuse it, with the same arguments, while the vendor retains it.",
428
- },
429
- }
430
- : {}),
431
- },
432
- ];
433
- const resolve = (args, method) => {
434
- const path = vendor.path ? vendor.path(String(args["path"])) : String(args["path"]);
435
- const { op, params } = index.resolve(method, path);
436
- const call = {
437
- method: op.method,
438
- path,
439
- op,
440
- params,
441
- query: isRecord(args["query"]) ? args["query"] : {},
442
- body: args["body"],
443
- ...headersOf(args),
444
- };
445
- return vendor.scope ? vendor.scope(call) : call;
446
- };
447
- const search = (args) => ({
448
- operations: index
449
- .search(String(args["query"]), {
450
- ...(typeof args["method"] === "string" ? { method: args["method"] } : {}),
451
- limit: typeof args["limit"] === "number" ? args["limit"] : 10,
452
- })
453
- .map((op) => ({
454
- method: op.method,
455
- path: op.path,
456
- operationId: op.operationId,
457
- summary: op.summary,
458
- tag: op.tag,
459
- tool: routeOf(op),
460
- })),
461
- revision: index.revision,
462
- });
463
- const details = (args) => {
464
- const method = String(args["method"]), path = String(args["path"]);
465
- const op = index.operation(method, path) ?? index.resolve(method, path).op;
466
- return {
467
- ...index.contract(op, typeof args["param"] === "string" ? args["param"] : undefined),
468
- tool: routeOf(op),
469
- revision: index.revision,
470
- };
471
- };
472
- const execute = (args, writing) => checked(() => {
473
- const call = resolve(args, (typeof args["method"] === "string" ? args["method"] : "GET"));
474
- if (writing && (call.method === "GET" || call.method === "HEAD"))
475
- invalid(`${call.method} operations are reads; call them with ${tool("read")}.`);
476
- if (!writing && !reads(call.op))
477
- invalid(`${call.method} ${call.op.path} is not a reviewed read; call it with ${tool("write")}.`);
478
- if (!writing && (call.method === "GET" || call.method === "HEAD") && call.body !== undefined)
479
- invalid(`A ${call.method} operation takes query, not body.`);
480
- guard(call);
481
- const framing = index.bodyType(call.op);
482
- if (writing && framing && /^multipart\/|octet-stream/.test(framing))
483
- invalid(`${call.method} ${call.op.path} takes a ${framing} upload, which ${tool("write")} does not send.` +
484
- (vendor.upload ? ` ${vendor.upload}` : ""));
485
- index.check(call.op, call.query, call.body);
486
- return call;
487
- }).pipe(Effect.flatMap((call) => Effect.gen(function* () {
488
- const header = writing && call.method !== "DELETE" ? vendor.idempotencyHeader : undefined;
489
- const given = typeof args["idempotencyKey"] === "string" ? args["idempotencyKey"] : undefined;
490
- if (writing && given !== undefined)
491
- yield* checked(() => {
492
- const key = given.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
493
- if (!key)
494
- invalid("idempotencyKey is blank; pass a key with at least one non-whitespace character, or omit it.");
495
- if (!/^[\t\x20-\x7e\x80-\xff]*$/.test(key))
496
- invalid("idempotencyKey contains a character an HTTP header cannot carry (a control character, or one past Latin-1); pass a printable key, or omit it.");
497
- });
498
- const key = header ? (given ?? crypto.randomUUID()) : undefined;
499
- if (key)
500
- yield* (yield* Recovery).record({ idempotencyKey: key });
501
- const classification = yield* Classification, dispatches = yield* Dispatches, signal = yield* Signal;
502
- return yield* vendor.send(call).pipe(Effect.provideService(IdempotencyKey, key && header ? { header, key } : undefined), Effect.map((result) => ({ ...selected(args, result), ...(key ? { idempotencyKey: key } : {}) })), Effect.catch((failure) => {
503
- if (!key || given)
504
- return Effect.fail(failure);
505
- const error = toConnectorCallError(failure, vendor.connection.errors, {
506
- classification,
507
- dispatches,
508
- withholds: vendor.connection.safety.withholdsErrors,
509
- signal,
510
- });
511
- const diagnostic = providerFailureMessage(error, `${error.message} ${header}: ${key}.`);
512
- const dispatched = dispatches.some((d) => d.request?.dispatched && d.request.key === key);
513
- return Effect.fail(new Refusal({
514
- error: dispatched
515
- ? keyResendAdvice(diagnostic, key, " Reusing it with the exact original arguments retries this write without repeating it only while the vendor retains the key; after that, look the object up before retrying.")
516
- : diagnostic,
517
- }));
518
- }));
519
- })));
520
- return declarations.map((declaration, i) => {
521
- const spec = {
522
- ...(writeSteps === undefined ? {} : { writeSteps }),
523
- name: declaration.name,
524
- description: declaration.description,
525
- input: schema(declaration.inputSchema),
526
- output: schema(declaration.outputSchema),
527
- run: (args) => i === 0
528
- ? checked(() => search(args))
529
- : i === 1
530
- ? checked(() => details(args))
531
- : execute(args, i === 3),
532
- };
533
- const built = i === 3
534
- ? write(vendor.connection, { ...spec, destructive: true })
535
- : read(vendor.connection, {
536
- ...spec,
537
- review: readPosts.map(([, path, reason]) => `${path}: ${reason}`).join("\n") ||
538
- "Only GET and HEAD operations are admitted.",
539
- });
540
- Object.assign(built, {
541
- inputSchema: catalogOrder(built.inputSchema, declaration.inputSchema),
542
- outputSchema: catalogOrder(built.outputSchema, declaration.outputSchema),
543
- });
544
- return built;
545
- });
546
- }
@@ -1,22 +1,6 @@
1
1
  // A provider's sensitive fields leave every result unless deployment code
2
2
  // returns them; no tool argument can. Shared by the providers whose raw rows
3
3
  // and hatches would otherwise reach what their projections drop.
4
- /** What `raw: true` and the hatches return, under the deployment's `sensitiveFields` choice. */
5
- export function unprojectedResults(sensitiveFields, names) {
6
- if (sensitiveFields === "return")
7
- return (body) => body;
8
- const dropped = new Set(names);
9
- const strip = (value) => {
10
- if (Array.isArray(value))
11
- return value.map(strip);
12
- if (!value || typeof value !== "object")
13
- return value;
14
- return Object.fromEntries(Object.entries(value)
15
- .filter(([name]) => !dropped.has(name))
16
- .map(([name, entry]) => [name, strip(entry)]));
17
- };
18
- return (body) => strip(body);
19
- }
20
4
  /** Refuse anything but the two choices, at construction (INV-11). */
21
5
  export function sensitiveFieldsOption(provider, value) {
22
6
  if (value === undefined)