@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
package/dist/program-writes.js
CHANGED
|
@@ -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
|
|
9
|
-
import { Effect, Option
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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)
|
|
384
|
+
.pipe(Effect.catch((failure) => Effect.fail(settled(failure, dispatches.find((entry) => entry.failure === failure)))));
|
|
410
385
|
});
|
|
411
|
-
function settled(failure, own
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
//
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
/**
|
|
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)
|