@zackbart/connecta 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/README.md +3 -2
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/agent-routes.js +231 -0
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2382
- package/dist/auth/native.js +37 -0
- package/dist/auth/oauth-grant.js +13 -0
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth-elicitation.js +7 -1
- package/dist/authorization-handoff.js +11 -5
- package/dist/catalog-cache.js +219 -59
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +284 -47
- package/dist/catalog.js +18 -2
- package/dist/config.d.ts +16 -11
- package/dist/config.js +9 -6
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +7 -4
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +682 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +473 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +555 -0
- package/dist/connectors/remote-mcp.js +62 -2863
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +2 -0
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +152 -114
- package/dist/executor-result.js +3 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +60 -19
- package/dist/guest-types.d.ts +19 -5
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/errors.js +5 -4
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/oauth.js +3 -2
- package/dist/http/provider.js +32 -4
- package/dist/http/safety.js +8 -0
- package/dist/http/tools.js +44 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation.js +26 -8
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +54 -31
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +5 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +135 -13
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/contract.d.ts +10 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/provider-call-facts.js +6 -2
- package/dist/providers/_shared/rest/operation-index.js +48 -50
- package/dist/providers/_shared/rest/value-safety.js +80 -0
- package/dist/providers/breeze/api.js +7 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +4 -3
- package/dist/providers/ccb/api.js +2 -1
- package/dist/providers/ccb/index.js +5 -0
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +2 -4
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +11 -16
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +10 -7
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +7 -5
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +7 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/api.js +3 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +13 -14
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +15 -13
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +3 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +4 -3
- package/dist/registry.d.ts +10 -4
- package/dist/registry.js +41 -13
- package/dist/routes/credentials.js +129 -60
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +30 -14
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +6 -1
- package/dist/routes/shared.js +7 -1
- package/dist/routes/ui-config.js +2 -2
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +16 -0
- package/dist/runtime/call.js +7 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +207 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/storage.js +12 -0
- package/dist/skills.js +37 -20
- package/dist/storage/keys.js +53 -3
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +53 -1
- package/dist/storage/sql.js +12 -2
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/usage-guide.js +16 -35
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +13 -13
- package/documentation/architecture.md +385 -20
- package/documentation/auth.md +233 -64
- package/documentation/code-mode.md +122 -39
- package/documentation/deploying.md +6 -3
- package/documentation/integrating.md +16 -2
- package/documentation/meta-tools.md +240 -49
- package/documentation/migration-0.33.md +5 -1
- package/documentation/migration-0.34.md +3 -1
- package/documentation/operator-tests.md +1 -1
- package/documentation/operator-ui.md +105 -12
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +10 -10
- package/examples/worker/README.md +4 -3
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +2 -1
- package/guides/how-it-works.md +4 -2
- package/package.json +1 -1
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
|
@@ -20,8 +20,12 @@ export interface UiTool extends Pick<OperatorTool, "name" | "address" | "descrip
|
|
|
20
20
|
* (`credential_required`), or a secret that lives in deployment configuration
|
|
21
21
|
* (`auth_required`). Principal forwarding instead reports `caller_refused`,
|
|
22
22
|
* which has no secret-repair prompt.
|
|
23
|
+
*
|
|
24
|
+
* `connector_timeout` is a downstream that did not answer before the operator
|
|
25
|
+
* page's deadline: slow or unreachable, not known to be broken. The page
|
|
26
|
+
* stops retrying it on its own until someone asks with Refresh.
|
|
23
27
|
*/
|
|
24
|
-
export type UiProblem = "connector_unavailable" | "oauth_required" | "credential_required" | "auth_required" | "caller_refused" | "credential_mismatch" | "catalog_failed";
|
|
28
|
+
export type UiProblem = "connector_unavailable" | "connector_timeout" | "oauth_required" | "credential_required" | "auth_required" | "caller_refused" | "credential_mismatch" | "catalog_failed";
|
|
25
29
|
/** A stored credential that cannot be used, by cause. */
|
|
26
30
|
export type UiCredentialProblem = "credential_mismatch" | "credential_unreadable";
|
|
27
31
|
interface UiCredentialField {
|
|
@@ -35,6 +39,13 @@ interface UiCredentialField {
|
|
|
35
39
|
lastFour?: string;
|
|
36
40
|
updatedAt?: string;
|
|
37
41
|
}
|
|
42
|
+
/** Mirrors `CredentialVerification` in src/credential-verification.ts. */
|
|
43
|
+
export interface UiVerification {
|
|
44
|
+
outcome: "ok" | "unverified" | UiCredentialFailure;
|
|
45
|
+
observedAt: string;
|
|
46
|
+
}
|
|
47
|
+
/** Why a credential test failed, as the route reports it. */
|
|
48
|
+
export type UiCredentialFailure = "rejected" | "scope" | "unreachable" | "timeout" | "failed";
|
|
38
49
|
interface UiCredential {
|
|
39
50
|
label: string;
|
|
40
51
|
description?: string;
|
|
@@ -83,11 +94,21 @@ export interface UiConnector {
|
|
|
83
94
|
authorizationUrl?: string;
|
|
84
95
|
toolCount: number;
|
|
85
96
|
tools: UiTool[];
|
|
97
|
+
/** Age of the stored status observation this row shows; absent on a live read. */
|
|
98
|
+
observedAgeMs?: number;
|
|
99
|
+
/** Browser-only: the stored observation's time, fixed as the row lands. */
|
|
100
|
+
observedAt?: number;
|
|
86
101
|
/** This connector exposes manageable downstream OAuth lifecycle hooks. */
|
|
87
102
|
oauth?: boolean;
|
|
88
103
|
/** Selected downstream client identity mechanism; never a client ID or secret. */
|
|
89
104
|
registrationPath?: "cimd" | "dcr" | "static";
|
|
90
105
|
credential?: UiCredential;
|
|
106
|
+
/**
|
|
107
|
+
* The stored last verification of this connector's operator-managed
|
|
108
|
+
* credential. Absent when nothing has verified the save stored now. A
|
|
109
|
+
* typed outcome and a time; never vendor text.
|
|
110
|
+
*/
|
|
111
|
+
verification?: UiVerification;
|
|
91
112
|
/**
|
|
92
113
|
* Drift the last catalog refresh saw *in this runtime*
|
|
93
114
|
* ([#343](https://github.com/zackbart/connecta/issues/343)). Four counts and
|
|
@@ -133,9 +133,13 @@ export function renewedAfterUnauthorized(request, sent, fresh) {
|
|
|
133
133
|
export function refusalRequest(error) {
|
|
134
134
|
return error !== null && typeof error === "object" ? refusals.get(error) : undefined;
|
|
135
135
|
}
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
/** A local refusal may additionally name the physical request it prevented. */
|
|
137
|
+
export function knownProviderFailure(error, unsent) {
|
|
138
|
+
if (error !== null && typeof error === "object") {
|
|
138
139
|
knownFailures.add(error);
|
|
140
|
+
if (unsent && !unsent.dispatched)
|
|
141
|
+
refusals.set(error, unsent);
|
|
142
|
+
}
|
|
139
143
|
return error;
|
|
140
144
|
}
|
|
141
145
|
export function isKnownProviderFailure(error) {
|
|
@@ -11,7 +11,7 @@ import { parseIndexDetails } from "./compact-index.js";
|
|
|
11
11
|
// are refused with the accepted names, because a guessed argument is the most
|
|
12
12
|
// common way an agent wastes a call. Drift between the pin and the vendor is
|
|
13
13
|
// caught by `providers:check` and refreshed by `providers:spec`.
|
|
14
|
-
import { ConnectorCallError } from "../../../errors.js";
|
|
14
|
+
import { ConnectorCallError, issueSummary } from "../../../errors.js";
|
|
15
15
|
const METHODS = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"]);
|
|
16
16
|
const MAX_ISSUES = 20;
|
|
17
17
|
function words(text) {
|
|
@@ -21,11 +21,16 @@ function words(text) {
|
|
|
21
21
|
.split(/[^a-z0-9]+/)
|
|
22
22
|
.filter(Boolean);
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
/** Index findings name no example: the hatch schema cannot prove an operation's arguments safe to echo. */
|
|
25
|
+
function invalid(message, issues, details) {
|
|
26
|
+
throw new ConnectorCallError("invalid_args", message, issues
|
|
27
|
+
? {
|
|
28
|
+
validation: {
|
|
29
|
+
issues,
|
|
30
|
+
exampleUnavailable: `Examples are withheld for API operations; read ${details} for the contract.`,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
: {});
|
|
29
34
|
}
|
|
30
35
|
function typesOf(node) {
|
|
31
36
|
return node.t === undefined ? [] : typeof node.t === "string" ? [node.t] : node.t;
|
|
@@ -42,7 +47,7 @@ function distance(a, b) {
|
|
|
42
47
|
}
|
|
43
48
|
return previous[b.length];
|
|
44
49
|
}
|
|
45
|
-
/** The accepted names, bounded and led by the likeliest intended one;
|
|
50
|
+
/** The accepted names, bounded and led by the likeliest intended one; the full list is in the tool schema. */
|
|
46
51
|
function oneOf(names, given) {
|
|
47
52
|
const close = given === undefined
|
|
48
53
|
? undefined
|
|
@@ -245,7 +250,7 @@ export class OperationIndex {
|
|
|
245
250
|
best.rows.push(row);
|
|
246
251
|
});
|
|
247
252
|
if (best.rows.length === 0) {
|
|
248
|
-
invalid(`${this.#title} has no
|
|
253
|
+
invalid(`${this.#title} has no operation at that path in the pinned API index (${this.revision}). ` +
|
|
249
254
|
`Nearest: ${this.nearest(method, segments).join("; ")}. Find operations with ${this.#vendorTool("search")}.`);
|
|
250
255
|
}
|
|
251
256
|
const templates = new Set(best.rows.map((row) => this.ops[row].path));
|
|
@@ -279,17 +284,16 @@ export class OperationIndex {
|
|
|
279
284
|
}
|
|
280
285
|
});
|
|
281
286
|
const issues = [];
|
|
282
|
-
const repair = [];
|
|
283
287
|
for (const [name, at, , schema] of this.#row(op)?.[0] ?? []) {
|
|
284
288
|
if (at === "path" && params[name] !== undefined)
|
|
285
|
-
this.#check(schema, decodeURIComponent(params[name]), `/path/${name}`, issues
|
|
289
|
+
this.#check(schema, decodeURIComponent(params[name]), `/path/${name}`, issues);
|
|
286
290
|
}
|
|
287
291
|
if (issues.length) {
|
|
288
292
|
const shown = issues
|
|
289
293
|
.slice(0, 5)
|
|
290
|
-
.map((issue) =>
|
|
294
|
+
.map((issue) => issueSummary([issue]))
|
|
291
295
|
.join("; ");
|
|
292
|
-
invalid(`Not sent: the path does not match ${op.method} ${op.path} in the pinned API index: ${shown}. Substitute real ids matching ${this.#vendorTool("details")}.${issues.some((issue) => issue.expected?.includes("safe integer")) ? " Integer ids use canonical ASCII decimal notation within the safe-integer range." : ""}`, issues.slice(0, MAX_ISSUES));
|
|
296
|
+
invalid(`Not sent: the path does not match ${op.method} ${op.path} in the pinned API index: ${shown}. Substitute real ids matching ${this.#vendorTool("details")}.${issues.some((issue) => issue.expected?.includes("safe integer")) ? " Integer ids use canonical ASCII decimal notation within the safe-integer range." : ""}`, issues.slice(0, MAX_ISSUES), this.#vendorTool("details"));
|
|
293
297
|
}
|
|
294
298
|
return { op, params };
|
|
295
299
|
}
|
|
@@ -458,7 +462,7 @@ export class OperationIndex {
|
|
|
458
462
|
}
|
|
459
463
|
}
|
|
460
464
|
if (only !== undefined && parameters.length === 0 && body === undefined) {
|
|
461
|
-
invalid(`${op.method} ${op.path} has no parameter
|
|
465
|
+
invalid(`${op.method} ${op.path} has no parameter by that name.`);
|
|
462
466
|
}
|
|
463
467
|
return {
|
|
464
468
|
method: op.method,
|
|
@@ -480,7 +484,6 @@ export class OperationIndex {
|
|
|
480
484
|
check(op, query, body) {
|
|
481
485
|
const row = this.#row(op);
|
|
482
486
|
const issues = [];
|
|
483
|
-
const repair = [];
|
|
484
487
|
const declared = (row ? row[0] : []).filter(([, at]) => at === "query");
|
|
485
488
|
const accepted = declared.map(([name]) => name);
|
|
486
489
|
for (const [name, value] of Object.entries(query)) {
|
|
@@ -489,40 +492,39 @@ export class OperationIndex {
|
|
|
489
492
|
const param = declared.find(([key]) => key === name);
|
|
490
493
|
if (!param) {
|
|
491
494
|
issues.push({
|
|
492
|
-
path:
|
|
493
|
-
code: "additionalProperties",
|
|
495
|
+
path: "/query/<key>",
|
|
494
496
|
expected: accepted.length ? oneOf(accepted, name) : "no query parameters",
|
|
497
|
+
received: valueType(value),
|
|
495
498
|
});
|
|
496
|
-
repair.push({ path: `/query/${name}`, receivedType: valueType(value), acceptedKeys: accepted });
|
|
497
499
|
continue;
|
|
498
500
|
}
|
|
499
|
-
this.#check(param[3], value, `/query/${name}`, issues
|
|
501
|
+
this.#check(param[3], value, `/query/${name}`, issues);
|
|
500
502
|
}
|
|
501
503
|
for (const [name, , required] of declared) {
|
|
502
504
|
if (required === 1 && query[name] === undefined) {
|
|
503
|
-
issues.push({ path: `/query/${name}`,
|
|
505
|
+
issues.push({ path: `/query/${name}`, expected: "a value (required)", received: "missing" });
|
|
504
506
|
}
|
|
505
507
|
}
|
|
506
508
|
const bodyRow = row && row[1] !== 0 ? row[1] : undefined;
|
|
507
509
|
if (body !== undefined) {
|
|
508
510
|
if (!bodyRow) {
|
|
509
|
-
issues.push({ path: "/body",
|
|
511
|
+
issues.push({ path: "/body", expected: "no body for this operation" });
|
|
510
512
|
}
|
|
511
513
|
else {
|
|
512
|
-
this.#check(bodyRow[1], body, "/body", issues
|
|
514
|
+
this.#check(bodyRow[1], body, "/body", issues);
|
|
513
515
|
}
|
|
514
516
|
}
|
|
515
517
|
else if (bodyRow?.[2] === 1) {
|
|
516
518
|
// An optional body may be omitted whole; a required one may not.
|
|
517
|
-
issues.push({ path: "/body",
|
|
519
|
+
issues.push({ path: "/body", expected: `a ${bodyRow[0]} body (required)`, received: "missing" });
|
|
518
520
|
}
|
|
519
521
|
if (issues.length > 0) {
|
|
520
522
|
const shown = issues
|
|
521
523
|
.slice(0, 5)
|
|
522
|
-
.map((issue) =>
|
|
524
|
+
.map((issue) => issueSummary([issue]))
|
|
523
525
|
.join("; ");
|
|
524
526
|
invalid(`Not sent: the arguments do not match ${op.method} ${op.path} in the pinned API index: ${shown}` +
|
|
525
|
-
`${issues.length > 5 ? `; ${issues.length - 5} more` : ""}. Read ${this.#vendorTool("details")} for the contract.`, issues.slice(0, MAX_ISSUES),
|
|
527
|
+
`${issues.length > 5 ? `; ${issues.length - 5} more` : ""}. Read ${this.#vendorTool("details")} for the contract.`, issues.slice(0, MAX_ISSUES), this.#vendorTool("details"));
|
|
526
528
|
}
|
|
527
529
|
}
|
|
528
530
|
#vendorTool(kind) {
|
|
@@ -606,7 +608,7 @@ export class OperationIndex {
|
|
|
606
608
|
out["truncated"] = true;
|
|
607
609
|
return out;
|
|
608
610
|
}
|
|
609
|
-
#check(node, value, at, issues
|
|
611
|
+
#check(node, value, at, issues) {
|
|
610
612
|
if (issues.length >= MAX_ISSUES)
|
|
611
613
|
return;
|
|
612
614
|
const schema = this.#deref(node);
|
|
@@ -624,7 +626,6 @@ export class OperationIndex {
|
|
|
624
626
|
(integer && !Number.isSafeInteger(numeric))) {
|
|
625
627
|
issues.push({
|
|
626
628
|
path: at,
|
|
627
|
-
code: "type",
|
|
628
629
|
expected: integer ? "a canonical ASCII decimal safe integer" : "a canonical ASCII decimal number",
|
|
629
630
|
});
|
|
630
631
|
return;
|
|
@@ -635,7 +636,7 @@ export class OperationIndex {
|
|
|
635
636
|
if (types.includes("boolean") && (value === "true" || value === "false"))
|
|
636
637
|
value = value === "true";
|
|
637
638
|
else {
|
|
638
|
-
issues.push({ path: at,
|
|
639
|
+
issues.push({ path: at, expected: types.join(" or ") });
|
|
639
640
|
return;
|
|
640
641
|
}
|
|
641
642
|
}
|
|
@@ -644,7 +645,7 @@ export class OperationIndex {
|
|
|
644
645
|
// Conflicting composed constraints (disjoint enums or types) admit nothing,
|
|
645
646
|
// not even the null or "" that otherwise unsets a field.
|
|
646
647
|
if (schema.e?.length === 0) {
|
|
647
|
-
issues.push({ path: at,
|
|
648
|
+
issues.push({ path: at, expected: "no value: this field's combined constraints admit none" });
|
|
648
649
|
return;
|
|
649
650
|
}
|
|
650
651
|
// Only newly opted-in bounded schemas enforce scalar numeric values;
|
|
@@ -661,42 +662,40 @@ export class OperationIndex {
|
|
|
661
662
|
? Number(value)
|
|
662
663
|
: NaN;
|
|
663
664
|
if (!Number.isFinite(numeric) || (integer && !Number.isSafeInteger(numeric))) {
|
|
664
|
-
issues.push({ path: at,
|
|
665
|
+
issues.push({ path: at, expected: integer ? "integer" : "finite number" });
|
|
665
666
|
return;
|
|
666
667
|
}
|
|
667
668
|
if (schema.l !== undefined && numeric < schema.l)
|
|
668
|
-
issues.push({ path: at,
|
|
669
|
+
issues.push({ path: at, expected: `at least ${schema.l}`, received: "number" });
|
|
669
670
|
if (schema.u !== undefined && numeric > schema.u)
|
|
670
|
-
issues.push({ path: at,
|
|
671
|
+
issues.push({ path: at, expected: `at most ${schema.u}`, received: "number" });
|
|
671
672
|
}
|
|
672
673
|
if (value === null || value === "")
|
|
673
674
|
return;
|
|
674
675
|
if (schema.a) {
|
|
675
676
|
let fewest;
|
|
676
677
|
for (const branch of schema.a) {
|
|
677
|
-
const attempt =
|
|
678
|
-
this.#check(branch, value, at, attempt
|
|
679
|
-
if (attempt.
|
|
678
|
+
const attempt = [];
|
|
679
|
+
this.#check(branch, value, at, attempt);
|
|
680
|
+
if (attempt.length === 0)
|
|
680
681
|
return;
|
|
681
|
-
if (!fewest || attempt.
|
|
682
|
+
if (!fewest || attempt.length < fewest.length)
|
|
682
683
|
fewest = attempt;
|
|
683
684
|
}
|
|
684
|
-
if (fewest)
|
|
685
|
-
issues.push(...fewest
|
|
686
|
-
repair.push(...fewest.repair);
|
|
687
|
-
}
|
|
685
|
+
if (fewest)
|
|
686
|
+
issues.push(...fewest);
|
|
688
687
|
return;
|
|
689
688
|
}
|
|
690
689
|
if (schema.e && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")) {
|
|
691
690
|
if (!schema.e.some((option) => option === value || String(option) === String(value))) {
|
|
692
|
-
issues.push({ path: at,
|
|
691
|
+
issues.push({ path: at, expected: oneOf(schema.e.map(String)) });
|
|
693
692
|
}
|
|
694
693
|
return;
|
|
695
694
|
}
|
|
696
695
|
if (schema.p || types.includes("object")) {
|
|
697
696
|
if (!isRecord(value)) {
|
|
698
697
|
if (types.length === 0 || types.includes("object")) {
|
|
699
|
-
issues.push({ path: at,
|
|
698
|
+
issues.push({ path: at, expected: "object" });
|
|
700
699
|
}
|
|
701
700
|
return;
|
|
702
701
|
}
|
|
@@ -707,35 +706,34 @@ export class OperationIndex {
|
|
|
707
706
|
continue;
|
|
708
707
|
const child = properties[key];
|
|
709
708
|
if (child)
|
|
710
|
-
this.#check(child, item, `${at}/${key}`, issues
|
|
709
|
+
this.#check(child, item, `${at}/${key}`, issues);
|
|
711
710
|
else if (schema.m)
|
|
712
|
-
this.#check(schema.m, item, `${at}
|
|
711
|
+
this.#check(schema.m, item, `${at}/<key>`, issues);
|
|
713
712
|
else if (schema.p) {
|
|
714
713
|
issues.push({
|
|
715
|
-
path: `${at}
|
|
716
|
-
code: "additionalProperties",
|
|
714
|
+
path: `${at}/<key>`,
|
|
717
715
|
expected: accepted.length ? oneOf(accepted, key) : "no properties",
|
|
716
|
+
received: valueType(item),
|
|
718
717
|
});
|
|
719
|
-
repair.push({ path: `${at}/${key}`, receivedType: valueType(item), acceptedKeys: accepted });
|
|
720
718
|
}
|
|
721
719
|
}
|
|
722
720
|
for (const key of schema.r ?? []) {
|
|
723
721
|
if (value[key] === undefined)
|
|
724
|
-
issues.push({ path: `${at}/${key}`,
|
|
722
|
+
issues.push({ path: `${at}/${key}`, expected: "a value (required)", received: "missing" });
|
|
725
723
|
}
|
|
726
724
|
return;
|
|
727
725
|
}
|
|
728
726
|
if (schema.i || types.includes("array")) {
|
|
729
727
|
if (!Array.isArray(value)) {
|
|
730
|
-
issues.push({ path: at,
|
|
728
|
+
issues.push({ path: at, expected: "array" });
|
|
731
729
|
return;
|
|
732
730
|
}
|
|
733
731
|
if (schema.i)
|
|
734
|
-
value.forEach((item, index) => this.#check(schema.i, item, `${at}/${index}`, issues
|
|
732
|
+
value.forEach((item, index) => this.#check(schema.i, item, `${at}/${index}`, issues));
|
|
735
733
|
return;
|
|
736
734
|
}
|
|
737
735
|
if (types.length > 0 && (isRecord(value) || Array.isArray(value))) {
|
|
738
|
-
issues.push({ path: at,
|
|
736
|
+
issues.push({ path: at, expected: types.join(" or ") });
|
|
739
737
|
}
|
|
740
738
|
}
|
|
741
739
|
}
|
|
@@ -1338,6 +1338,53 @@ function acceptsCredential(schema, depth) {
|
|
|
1338
1338
|
acceptsCredential(node.items, depth + 1) ||
|
|
1339
1339
|
(node.anyOf ?? []).some((branch) => acceptsCredential(branch, depth + 1)));
|
|
1340
1340
|
}
|
|
1341
|
+
const ENV_VALUE_NAMES = ENV_VALUE_FIELDS.map(normalizedName);
|
|
1342
|
+
/** `data[].id` segments as tokens, a list's items as `[]`: `["data", "[]", "id"]`. */
|
|
1343
|
+
function tokensOf(segments) {
|
|
1344
|
+
return segments.flatMap((segment) => segment === "[]"
|
|
1345
|
+
? ["[]"]
|
|
1346
|
+
: segment.endsWith("[]")
|
|
1347
|
+
? [normalizedName(segment.slice(0, -2)), "[]"]
|
|
1348
|
+
: [normalizedName(segment)]);
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* A best-effort hint for discovery over rules that change every non-null value
|
|
1352
|
+
* they reach: a path is withheld when it sits at or inside a reviewed redaction
|
|
1353
|
+
* path (matched from the body's root, or at any depth with `anyDepth`), or any
|
|
1354
|
+
* of its names satisfies the name rule, which also sees the parent's name.
|
|
1355
|
+
* Paths carry their list markers: a reviewed `[]` needs a declared list there,
|
|
1356
|
+
* while a named segment also reaches every item of a list. Rules that can leave
|
|
1357
|
+
* a value unchanged (`url:`, `origin:` or `#url` sanitizing, `[?…]`, `@keys`)
|
|
1358
|
+
* are not hints. Names compare normalized, and `{}` or `*` matches any name.
|
|
1359
|
+
*/
|
|
1360
|
+
export function pathWithholds(rules) {
|
|
1361
|
+
const patterns = (rules.paths ?? []).flatMap((raw) => {
|
|
1362
|
+
const { mode, segments } = parsePath(raw);
|
|
1363
|
+
if (mode !== "redact" || segments.some((segment) => segment.startsWith("[?") || segment === "@keys"))
|
|
1364
|
+
return [];
|
|
1365
|
+
return [segments.map((segment) => (segment === "[]" ? "[]" : segment === "{}" ? "*" : normalizedName(segment)))];
|
|
1366
|
+
});
|
|
1367
|
+
const reaches = (pattern, tokens, from) => {
|
|
1368
|
+
let at = from;
|
|
1369
|
+
for (const part of pattern) {
|
|
1370
|
+
if (part !== "[]")
|
|
1371
|
+
while (tokens[at] === "[]")
|
|
1372
|
+
at += 1;
|
|
1373
|
+
if (at >= tokens.length || (part === "[]" ? tokens[at] !== "[]" : part !== "*" && part !== tokens[at]))
|
|
1374
|
+
return false;
|
|
1375
|
+
at += 1;
|
|
1376
|
+
}
|
|
1377
|
+
return true;
|
|
1378
|
+
};
|
|
1379
|
+
return (path) => {
|
|
1380
|
+
const tokens = tokensOf(path);
|
|
1381
|
+
const names = tokens.filter((token) => token !== "[]");
|
|
1382
|
+
if (rules.names && names.some((name, at) => rules.names(name, names[at - 1] ?? "")))
|
|
1383
|
+
return true;
|
|
1384
|
+
const starts = rules.anyDepth ? tokens.flatMap((token, at) => (token === "[]" ? [] : [at])) : [0];
|
|
1385
|
+
return patterns.some((pattern) => starts.some((at) => reaches(pattern, tokens, at)));
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1341
1388
|
/** A vendor's value-safety mechanism over its reviewed table and pinned index. */
|
|
1342
1389
|
export function valueSafety(table, index) {
|
|
1343
1390
|
const fields = table.fields ?? {};
|
|
@@ -1347,9 +1394,42 @@ export function valueSafety(table, index) {
|
|
|
1347
1394
|
const key = `${method} ${template}`;
|
|
1348
1395
|
return Object.hasOwn(table.operations, key) ? table.operations[key] : undefined;
|
|
1349
1396
|
};
|
|
1397
|
+
const hints = new Map();
|
|
1398
|
+
const redactedNames = Object.entries(fields)
|
|
1399
|
+
.filter(([, review]) => review.verdict === "redact")
|
|
1400
|
+
.map(([name]) => normalizedName(name));
|
|
1401
|
+
const keptNames = Object.entries(fields)
|
|
1402
|
+
.filter(([, review]) => review.verdict === "keep")
|
|
1403
|
+
.map(([name]) => normalizedName(name));
|
|
1404
|
+
const scopeParents = scopeMaps.map(normalizedName);
|
|
1350
1405
|
return {
|
|
1351
1406
|
table,
|
|
1352
1407
|
verdict,
|
|
1408
|
+
withholds(method, template) {
|
|
1409
|
+
const key = `${method} ${template}`;
|
|
1410
|
+
let hint = hints.get(key);
|
|
1411
|
+
if (!hint) {
|
|
1412
|
+
const reviewed = verdict(method, template);
|
|
1413
|
+
const env = table.envBodies?.test(template) === true;
|
|
1414
|
+
const kept = new Set([
|
|
1415
|
+
...keptNames,
|
|
1416
|
+
...(reviewed && reviewed.verdict !== "refuse" ? (reviewed.keep ?? []) : []).map((path) => trailOf(path).map(normalizedName).at(-1)),
|
|
1417
|
+
]);
|
|
1418
|
+
hint =
|
|
1419
|
+
reviewed?.verdict === "refuse"
|
|
1420
|
+
? () => true
|
|
1421
|
+
: pathWithholds({
|
|
1422
|
+
paths: reviewed?.verdict === "redact" ? reviewed.paths : [],
|
|
1423
|
+
names: (name, parent) => redactedNames.includes(name) ||
|
|
1424
|
+
(env && ENV_VALUE_NAMES.includes(name)) ||
|
|
1425
|
+
(parent === "deployhooks" && name === "url") ||
|
|
1426
|
+
// The suffix rule scrubs only strings, so only the whole-value credential rule hints.
|
|
1427
|
+
(!kept.has(name) && !scopeParents.includes(parent) && containsCredential(name)),
|
|
1428
|
+
});
|
|
1429
|
+
hints.set(key, hint);
|
|
1430
|
+
}
|
|
1431
|
+
return hint;
|
|
1432
|
+
},
|
|
1353
1433
|
refusal(method, template) {
|
|
1354
1434
|
const reviewed = verdict(method, template);
|
|
1355
1435
|
return reviewed?.verdict === "refuse" ? `Connecta refuses ${method} ${template}. ${reviewed.reason}` : undefined;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Routes, routePhrases } from "../../agent-routes.js";
|
|
2
|
+
import { pathWithholds } from "../_shared/rest/value-safety.js";
|
|
1
3
|
import { asRecord } from "../_shared/json.js";
|
|
2
4
|
// Breeze's reviewed GET-only vendor. HTTP methods do not establish safety: see tools.ts.
|
|
3
5
|
import { Effect, Schema } from "effect";
|
|
@@ -467,10 +469,11 @@ export const admit = (url) => {
|
|
|
467
469
|
};
|
|
468
470
|
export const authenticate = Effect.gen(function* () {
|
|
469
471
|
const values = yield* (yield* Credentials).values.pipe(Effect.mapError((error) => new Refusal({ error })));
|
|
472
|
+
const routes = yield* Routes;
|
|
470
473
|
const key = values?.["value"]?.trim();
|
|
471
474
|
if (!key)
|
|
472
475
|
return yield* Effect.fail(new Refusal({
|
|
473
|
-
error: new ConnectorCallError("auth_required",
|
|
476
|
+
error: new ConnectorCallError("auth_required", `No Breeze API key is configured for this connector. ${routePhrases(routes).authorizeFor("recovery options")}. When available, an operator can add the key in this connection in the operator UI.`),
|
|
474
477
|
}));
|
|
475
478
|
return { "Api-Key": key };
|
|
476
479
|
});
|
|
@@ -537,6 +540,9 @@ export const breezeSafety = {
|
|
|
537
540
|
})(),
|
|
538
541
|
}),
|
|
539
542
|
redact: (body, op) => withoutCalendarFeeds(op.template, body),
|
|
543
|
+
withholds: (op) => op.template === "/events/calendars/list"
|
|
544
|
+
? pathWithholds({ names: (name) => name === "address" || name === "embedkey" })
|
|
545
|
+
: () => false,
|
|
540
546
|
withholdsErrors: () => false,
|
|
541
547
|
},
|
|
542
548
|
sensitiveFields: { names: [], choice: "drop" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Church records\n\nBreeze ChMS (now Tithely Church Management) holds people, tags, events,\nattendance, forms, volunteers and giving. People carry contact information,\nbirthdates and family ties; giving reveals donors and amounts. Reduce records\nin code and return only the names, counts or totals the task needs.\n\n## Ids and joins\n\nIds are numeric strings. Resolve them from the relevant inventory before\nwriting. Events have an occurrence id and a series `eventId`; attendance,\nvolunteers and check-in use the occurrence id. Event and pledge lists can lag\nup to 15 minutes behind the site. An all-zero MySQL date means no time was\nrecorded, including no check-out.\n\nProfile `details` use field ids, not names. Resolve field and option ids from\n`list_profile_fields`; multiple-choice filters join option ids with `-`.\nA person-name lookup requires listing people and matching in code. Form entry\nresponses similarly join to that form's field ids.\n\n## Giving\n\nGiving, funds and pledges use archival endpoints removed from the public\nreference in 2023. They can change or vanish without notice. Amounts are\nexact decimal strings: reduce them with integer-cent arithmetic, and read the\ncurrency from `get_account_summary`. A split gift's fund amounts belong to\nits fund ids. Tithe.ly deposits sync in batches, so recent gifts can be absent.\nEditing a giving transaction may return a new payment id.\nUse `limit: 1000` when reducing broad giving ranges: each local page refetches\nthe bounded vendor list, so larger windows avoid repeated full-list fetches.\n\n## Required sequences\n\nCheck for an existing person before adding one: creation does not deduplicate.\nResolve profile fields before updating them. Text, dates (M/D/YYYY) and option\nids use `response`; email, phone, address and family-role values use `details`.\nNamed person writes take these entries in `values`.\nFamily changes can remove previous associations. Attendance check-out records\na departure time; removing attendance deletes the record instead.\n\nBreeze sends writes as GET requests too. Query names ending in `_json` take\nJSON-encoded strings. The named person writes encode their typed profile values\nfor you. The account-log `object_json` is a serialized affected-object value;\nits free-text description has no stable schema. Events and account logs have\nvendor caps: narrow the date range when the result fills its limit.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `breeze_api_details`, `breeze_api_search`, `list_attendance`, `list_contributions`, `list_funds`, `list_people`, `list_profile_fields`, `list_tags`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `breeze_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 100, at most 1000). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `breeze_api_search` finds this API's operations and reads no account data, `breeze_api_details` shows one operation's contract, and `breeze_api_read` calls a reviewed read. `breeze_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
3
|
+
"content": "## Church records\n\nBreeze ChMS (now Tithely Church Management) holds people, tags, events,\nattendance, forms, volunteers and giving. People carry contact information,\nbirthdates and family ties; giving reveals donors and amounts. Reduce records\nin code and return only the names, counts or totals the task needs.\n\n## Ids and joins\n\nIds are numeric strings. Resolve them from the relevant inventory before\nwriting. Events have an occurrence id and a series `eventId`; attendance,\nvolunteers and check-in use the occurrence id. Event and pledge lists can lag\nup to 15 minutes behind the site. An all-zero MySQL date means no time was\nrecorded, including no check-out.\n\nProfile `details` use field ids, not names. Resolve field and option ids from\n`list_profile_fields`; multiple-choice filters join option ids with `-`.\nA person-name lookup requires listing people and matching in code. Form entry\nresponses similarly join to that form's field ids.\n\n## Giving\n\nGiving, funds and pledges use archival endpoints removed from the public\nreference in 2023. They can change or vanish without notice. Amounts are\nexact decimal strings: reduce them with integer-cent arithmetic, and read the\ncurrency from `get_account_summary`. A split gift's fund amounts belong to\nits fund ids. Tithe.ly deposits sync in batches, so recent gifts can be absent.\nEditing a giving transaction may return a new payment id.\nUse `limit: 1000` when reducing broad giving ranges: each local page refetches\nthe bounded vendor list, so larger windows avoid repeated full-list fetches.\n\n## Required sequences\n\nCheck for an existing person before adding one: creation does not deduplicate.\nResolve profile fields before updating them. Text, dates (M/D/YYYY) and option\nids use `response`; email, phone, address and family-role values use `details`.\nNamed person writes take these entries in `values`.\nFamily changes can remove previous associations. Attendance check-out records\na departure time; removing attendance deletes the record instead.\n\nBreeze sends writes as GET requests too. Query names ending in `_json` take\nJSON-encoded strings. The named person writes encode their typed profile values\nfor you. The account-log `object_json` is a serialized affected-object value;\nits free-text description has no stable schema. Events and account logs have\nvendor caps: narrow the date range when the result fills its limit.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `breeze_api_details`, `breeze_api_search`, `list_attendance`, `list_contributions`, `list_funds`, `list_people`, `list_profile_fields`, `list_tags`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `breeze_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 100, at most 1000). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `breeze_api_search` finds this API's operations and reads no account data, `breeze_api_details` shows one operation's contract, and `breeze_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `breeze_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Church instructions"
|
|
5
5
|
};
|
|
@@ -6,7 +6,7 @@ import { ConnectorCallError } from "../../errors.js";
|
|
|
6
6
|
import { Refusal } from "../../http/errors.js";
|
|
7
7
|
import { Integer, isMinChars, isMaxChars } from "../../http/json-schema.js";
|
|
8
8
|
import { RequestSecrets, Classification } from "../../http/services.js";
|
|
9
|
-
import {
|
|
9
|
+
import { fieldsContract, selectFields } from "../../http/fields.js";
|
|
10
10
|
import { read, list, write as writeTool } from "../../http/tools.js";
|
|
11
11
|
import { READ_ENDPOINTS } from "./api.js";
|
|
12
12
|
const DEFAULT_EVENT_LIMIT = 100;
|
|
@@ -218,6 +218,7 @@ const Result_list_events = Schema.Struct({
|
|
|
218
218
|
});
|
|
219
219
|
const Row_list_events = Result_list_events.fields.events.value;
|
|
220
220
|
const SelectedEvent = Schema.Struct(Object.fromEntries(Object.entries(Row_list_events.fields).map(([name, schema]) => [name, Schema.optionalKey(schema)])));
|
|
221
|
+
const EVENT_FIELDS = fieldsContract("item", SelectedEvent);
|
|
221
222
|
const Result_list_attendance = Schema.Struct({
|
|
222
223
|
"attendance": Schema.Array(Schema.Struct({
|
|
223
224
|
"personId": Schema.String,
|
|
@@ -527,7 +528,7 @@ export function breezeTools({ connection, pageSize: defaultPageSize, }) {
|
|
|
527
528
|
description: "Instances returned, 1 to Breeze's 1000. Defaults to 100.",
|
|
528
529
|
default: 100,
|
|
529
530
|
})),
|
|
530
|
-
fields: Schema.optionalKey(
|
|
531
|
+
fields: Schema.optionalKey(EVENT_FIELDS.input),
|
|
531
532
|
}),
|
|
532
533
|
output: Schema.Struct({
|
|
533
534
|
items: Schema.Array(SelectedEvent),
|
|
@@ -535,7 +536,7 @@ export function breezeTools({ connection, pageSize: defaultPageSize, }) {
|
|
|
535
536
|
truncated: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
536
537
|
}),
|
|
537
538
|
run: (args) => Effect.gen(function* () {
|
|
538
|
-
const error =
|
|
539
|
+
const error = EVENT_FIELDS.problem(args.fields);
|
|
539
540
|
if (error)
|
|
540
541
|
return yield* Effect.fail(new Refusal({ error }));
|
|
541
542
|
const result = yield* load_list_events(args);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { asRecord, asArray } from "../_shared/json.js";
|
|
2
|
+
import { routePhrases } from "../../agent-routes.js";
|
|
2
3
|
import { CCB_MEDIA_TYPE, CCB_ENVIRONMENTS, CCB_READ_SCOPES, CCB_WRITE_SCOPES, CCB_SENSITIVE_FIELDS, } from "./lib/constants.js";
|
|
3
4
|
// Pushpay ChMS v2 JSON contracts, OAuth, status meanings, admission and privacy.
|
|
4
5
|
// The legacy per-church XML API is outside this provider's scope.
|
|
@@ -465,7 +466,7 @@ export const errors = {
|
|
|
465
466
|
statuses: {
|
|
466
467
|
401: () => ({
|
|
467
468
|
code: "auth_required",
|
|
468
|
-
words: (detail) => `${detail} CCB rejected the connection's access token.
|
|
469
|
+
words: (detail, routes) => `${detail} CCB rejected the connection's access token. ${routePhrases(routes).authorizeFor("this connector")}.`,
|
|
469
470
|
advice: ", then retry",
|
|
470
471
|
}),
|
|
471
472
|
403: () => ({
|
|
@@ -47,6 +47,11 @@ export const ccb = defineApiProvider({
|
|
|
47
47
|
note: "./providers/ccb merged GitHub/Cloudflare/CCB step-2 snapshot measures 391,649 B standalone gzip-9 before recording these bundle facts, Node 26.12.0 with zlib-ng 1.3.1; recomputed with the combined opt-in hatch support. Earlier step-2 standalone gzip snapshot at ffd64b04, CI run 40080865116 static job, Node 22.23.3, against main ce0c7492 (345,272 B). Includes the reviewed 385-operation request/response index. Shared Effect is paid once per deployment; provider entries are reported, uncapped and non-additive (#703) (#899).",
|
|
48
48
|
},
|
|
49
49
|
skill,
|
|
50
|
+
// The guide resolves a person from individuals and a household from families.
|
|
51
|
+
searchSynonyms: [
|
|
52
|
+
["person", "individual"],
|
|
53
|
+
["household", "family"],
|
|
54
|
+
],
|
|
50
55
|
options: CCB_OPTIONS,
|
|
51
56
|
displayTitle: (options) => defaultTitle(options.environment, options.mode, writable(options)),
|
|
52
57
|
description: (options) => `Church Community Builder (Pushpay ChMS) — ${routingLine(options.environment, options.mode, writable(options))}: ${options.purpose}`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## People, households and groups\n\nCCB v2 addresses records by integer ids. Resolve a person from the individual\nlist and a household from families; never guess ids. Family membership and\nchurch group membership join by individual id. A membership role belongs to\nthat group, rather than being a property of the person everywhere.\n\nIdentity Auth acts with the signed-in person's permissions. A permission\nrefusal can mean that person lacks access even when the connection is healthy.\n\n## Events and attendance\n\nAn event is a series; its dated occurrence is a separate identity. Attendance\njoins need both the event id and occurrence. Preserve the vendor's date and\ntime-zone meaning instead of treating every occurrence as UTC midnight.\n\n## Pastoral and financial records\n\nNotes, background checks, pledges and giving metrics can be sensitive. Read\nonly what the task needs and reduce those records inside execute_code before\nreturning an answer. Gift metrics are counts by period, with a range of at\nmost twelve months. Per-gift transactions and amounts belong to the legacy\nv1 XML API, which this v2 connector does not reach.\n\n## Vendor call rates and recovery\n\nCCB meters endpoints separately per API client. Individual profile reads have\nno burst; individual advanced searches allow one call per five seconds, and\ncategory schedule lists allow one per two seconds. Prefer list pages over a\nloop of individual gets. Other deployments sharing the client spend from the\nsame vendor allowance.\n\nFor a profile edit, resolve the individual first and preserve fields outside\nthe requested change. Use `ccb_api_write` for the edit; the host may ask the\nuser to confirm. After an ambiguous update, read that profile to determine\nwhat actually changed before deciding whether another write is needed.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `ccb_api_details`, `ccb_api_read`, `ccb_api_search`, `list_event_attendees`, `list_events`, `list_families`, `list_group_members`, `list_groups`, `list_individual_groups`, `list_individual_notes`, `list_individuals`, `search_records`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `ccb_api_search` finds this API's operations and reads no account data, `ccb_api_details` shows one operation's contract, and `ccb_api_read` calls a reviewed read. `ccb_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 14 operations are refused by design; use `ccb_api_details` with the method and path to
|
|
3
|
+
"content": "## People, households and groups\n\nCCB v2 addresses records by integer ids. Resolve a person from the individual\nlist and a household from families; never guess ids. Family membership and\nchurch group membership join by individual id. A membership role belongs to\nthat group, rather than being a property of the person everywhere.\n\nIdentity Auth acts with the signed-in person's permissions. A permission\nrefusal can mean that person lacks access even when the connection is healthy.\n\n## Events and attendance\n\nAn event is a series; its dated occurrence is a separate identity. Attendance\njoins need both the event id and occurrence. Preserve the vendor's date and\ntime-zone meaning instead of treating every occurrence as UTC midnight.\n\n## Pastoral and financial records\n\nNotes, background checks, pledges and giving metrics can be sensitive. Read\nonly what the task needs and reduce those records inside execute_code before\nreturning an answer. Gift metrics are counts by period, with a range of at\nmost twelve months. Per-gift transactions and amounts belong to the legacy\nv1 XML API, which this v2 connector does not reach.\n\n## Vendor call rates and recovery\n\nCCB meters endpoints separately per API client. Individual profile reads have\nno burst; individual advanced searches allow one call per five seconds, and\ncategory schedule lists allow one per two seconds. Prefer list pages over a\nloop of individual gets. Other deployments sharing the client spend from the\nsame vendor allowance.\n\nFor a profile edit, resolve the individual first and preserve fields outside\nthe requested change. Use `ccb_api_write` for the edit; the host may ask the\nuser to confirm. After an ambiguous update, read that profile to determine\nwhat actually changed before deciding whether another write is needed.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `ccb_api_details`, `ccb_api_read`, `ccb_api_search`, `list_event_attendees`, `list_events`, `list_families`, `list_group_members`, `list_groups`, `list_individual_groups`, `list_individual_notes`, `list_individuals`, `search_records`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `ccb_api_search` finds this API's operations and reads no account data, `ccb_api_details` shows one operation's contract, and `ccb_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `ccb_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 14 operations are refused by design; use `ccb_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Church instructions"
|
|
5
5
|
};
|
|
@@ -433,7 +433,7 @@ class CloudflareIndex extends OperationIndex {
|
|
|
433
433
|
for (const name of EXCLUSIVE_PATH_IDS[`${match.op.method} ${match.op.path}`] ?? []) {
|
|
434
434
|
if (Number(decodeURIComponent(match.params[name])) < 1) {
|
|
435
435
|
throw new ConnectorCallError("invalid_args", `Not sent: Cloudflare path parameter ${name} must be at least 1. Read cloudflare_api_details for the pinned contract.`, {
|
|
436
|
-
validation: { issues: [{ path: `/path/${name}`,
|
|
436
|
+
validation: { issues: [{ path: `/path/${name}`, expected: "at least 1" }] },
|
|
437
437
|
});
|
|
438
438
|
}
|
|
439
439
|
}
|
|
@@ -528,6 +528,13 @@ export const safety = () => {
|
|
|
528
528
|
}
|
|
529
529
|
: SAFETY.redact(body, operation.method, canonical(operation)),
|
|
530
530
|
withholdsErrors: (operation) => operation.method === "POST" && operation.template === "/graphql" ? false : reviewed.withholdsErrors(operation),
|
|
531
|
+
// The table applies to the envelope and again to its result, as redact does.
|
|
532
|
+
withholds: (operation) => {
|
|
533
|
+
const hint = reviewed.withholds(operation);
|
|
534
|
+
return (path) => hint(path) ||
|
|
535
|
+
(path[0] === "result" && hint(path.slice(1))) ||
|
|
536
|
+
(path[0] === "result[]" && hint(["[]", ...path.slice(1)]));
|
|
537
|
+
},
|
|
531
538
|
},
|
|
532
539
|
sensitiveFields: { names: [], choice: "drop" },
|
|
533
540
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Accounts, domains and records\n\nResolve account and zone ids before acting. A zone is a domain, found by its name;\nits account id joins it to the account inventory. Cloudflare ids are normally\n32-character hex strings. A default id routes a request; a pin limits its scope.\n\nDNS TTL is in seconds; TTL 1 means automatic. Read the record before changing it.\nPATCH changes supplied fields; PUT replaces the record, so preserve its type,\nname and content. Read state before replacing, rolling back or purging. Prefer\na targeted cache purge by files, tags, hosts or prefixes over purging everything.\n\nHatch data keeps Cloudflare's vendor envelope. For example, select DNS record\nidentities with `fields: [\"result.id\", \"result.name\"]`; a list stays under\n`data.result`. Named account and zone lists keep projected rows with camelCase\naccount and name-server fields.\n\n## Workers, storage and analytics\n\nWorker script source is code; secret binding values are separate and unavailable.\nKV values are application data. Encode a slash inside a KV key or R2 object key\nas `%2F`. R2 requires API-token authentication, rather than a Global API Key.\n\nGraphQL Analytics uses accountTag and zoneTag for account/zone identities.\nFilter every accounts/zones field by equality or an explicit set of those ids.\nA pinned query uses one operation per document and every declared fragment.\nAnalytics Engine SQL is read-only; D1 SQL and Durable Object queries can write.\nAI inference, previews and outbound checks can cost money or activate work.\n\n## Disqualifiers and recovery\n\nCredential minting, rotation, stored secrets, private session captures and signed\nupload or deployment links are unavailable. Use the Cloudflare dashboard for\ncredential administration. A Global API Key cannot change user settings or\nmemberships through this connection.\n\nAfter an authentication failure, use `verify_credential` once to distinguish a\nrejected credential from a permission gap. An operator replaces the credential;\nrepeating the failed request does not add permissions. Cloudflare's global limit\ncounts dashboard traffic and all tokens together, so reduce requests as well as\nhonoring the returned wait.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `cloudflare_api_details`, `cloudflare_api_search`, `list_accounts`, `list_zones`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `cloudflare_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 20, at most 50). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Envelope fields:** hatch data keeps the vendor envelope. Select declared fields with `fields: [\"result.id\", \"result.name\"]`; array traversal also accepts `result[].id`. Named reads keep their projected fields.\n- **Hatch:** `cloudflare_api_search` finds this API's operations and reads no account data, `cloudflare_api_details` shows one operation's contract, and `cloudflare_api_read` calls a reviewed read. `cloudflare_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 70 operations are refused by design; use `cloudflare_api_details` with the method and path to
|
|
3
|
+
"content": "## Accounts, domains and records\n\nResolve account and zone ids before acting. A zone is a domain, found by its name;\nits account id joins it to the account inventory. Cloudflare ids are normally\n32-character hex strings. A default id routes a request; a pin limits its scope.\n\nDNS TTL is in seconds; TTL 1 means automatic. Read the record before changing it.\nPATCH changes supplied fields; PUT replaces the record, so preserve its type,\nname and content. Read state before replacing, rolling back or purging. Prefer\na targeted cache purge by files, tags, hosts or prefixes over purging everything.\n\nHatch data keeps Cloudflare's vendor envelope. For example, select DNS record\nidentities with `fields: [\"result.id\", \"result.name\"]`; a list stays under\n`data.result`. Named account and zone lists keep projected rows with camelCase\naccount and name-server fields.\n\n## Workers, storage and analytics\n\nWorker script source is code; secret binding values are separate and unavailable.\nKV values are application data. Encode a slash inside a KV key or R2 object key\nas `%2F`. R2 requires API-token authentication, rather than a Global API Key.\n\nGraphQL Analytics uses accountTag and zoneTag for account/zone identities.\nFilter every accounts/zones field by equality or an explicit set of those ids.\nA pinned query uses one operation per document and every declared fragment.\nAnalytics Engine SQL is read-only; D1 SQL and Durable Object queries can write.\nAI inference, previews and outbound checks can cost money or activate work.\n\n## Disqualifiers and recovery\n\nCredential minting, rotation, stored secrets, private session captures and signed\nupload or deployment links are unavailable. Use the Cloudflare dashboard for\ncredential administration. A Global API Key cannot change user settings or\nmemberships through this connection.\n\nAfter an authentication failure, use `verify_credential` once to distinguish a\nrejected credential from a permission gap. An operator replaces the credential;\nrepeating the failed request does not add permissions. Cloudflare's global limit\ncounts dashboard traffic and all tokens together, so reduce requests as well as\nhonoring the returned wait.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `cloudflare_api_details`, `cloudflare_api_search`, `list_accounts`, `list_zones`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `cloudflare_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 20, at most 50). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Envelope fields:** hatch data keeps the vendor envelope. Select declared fields with `fields: [\"result.id\", \"result.name\"]`; array traversal also accepts `result[].id`. Named reads keep their projected fields.\n- **Hatch:** `cloudflare_api_search` finds this API's operations and reads no account data, `cloudflare_api_details` shows one operation's contract, and `cloudflare_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `cloudflare_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 70 operations are refused by design; use `cloudflare_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Account instructions"
|
|
5
5
|
};
|