@zackbart/connecta 0.37.1 → 0.39.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 +94 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/agent-routes.js +231 -0
- package/dist/auth/clerk-sdk/generated.js +2 -1
- 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 -2516
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/native.js +27 -0
- package/dist/auth/oauth-grant.js +24 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +9 -3
- package/dist/authorization-handoff.js +11 -5
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-cache.js +130 -43
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +285 -48
- package/dist/catalog.js +18 -2
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +57 -25
- package/dist/config.js +30 -77
- 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 +11 -7
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- 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 +671 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +469 -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 +544 -0
- package/dist/connectors/remote-mcp.js +61 -2898
- package/dist/connectors/resource-uri.js +0 -4
- 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/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- 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 +4 -2
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +172 -125
- package/dist/executor-result.js +5 -2
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +63 -20
- package/dist/guest-types.d.ts +24 -10
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +7 -6
- 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/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +4 -3
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +33 -5
- package/dist/http/safety.js +9 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +45 -30
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +21 -6
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +50 -35
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +6 -2
- package/dist/oauth-handoff.js +2 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +136 -14
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -49
- package/dist/operator-ui/contract.d.ts +12 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +49 -53
- package/dist/providers/_shared/rest/value-safety.js +106 -28
- package/dist/providers/breeze/api.js +9 -8
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +7 -7
- package/dist/providers/ccb/api.js +3 -7
- package/dist/providers/ccb/index.js +7 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +10 -3
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- 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 +4 -14
- 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 +23 -41
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +11 -16
- 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 +12 -26
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +8 -4
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +5 -2
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/planning-center/tools.js +2 -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 +14 -23
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +19 -26
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +5 -8
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +2 -7
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +6 -4
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +11 -5
- package/dist/registry.js +46 -20
- package/dist/request-principal.js +1 -1
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +130 -61
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +34 -17
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +6 -5
- package/dist/routes/shared.d.ts +13 -8
- package/dist/routes/shared.js +15 -9
- package/dist/routes/ui-config.js +3 -3
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +17 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +8 -3
- package/dist/runtime/native-storage.js +9 -0
- package/dist/runtime/request-reads.js +182 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/runtime/storage.js +12 -0
- package/dist/sent-secrets.d.ts +1 -2
- package/dist/sent-secrets.js +3 -8
- package/dist/server.js +4 -4
- package/dist/skills.js +44 -54
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +69 -128
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +27 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +13 -14
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +33 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +18 -37
- 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/worker.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +19 -55
- package/documentation/architecture.md +436 -515
- package/documentation/auth.md +229 -88
- package/documentation/code-mode.md +201 -94
- package/documentation/deploying.md +25 -43
- package/documentation/integrating.md +18 -19
- package/documentation/meta-tools.md +225 -51
- package/documentation/migration-0.33.md +10 -6
- package/documentation/migration-0.34.md +5 -5
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +20 -120
- package/documentation/operator-ui.md +108 -16
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +14 -16
- package/examples/worker/README.md +24 -118
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +9 -31
- package/guides/how-it-works.md +6 -4
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { isRecord } from "../json.js";
|
|
2
|
+
// Value safety for every REST vendor: no tool response,
|
|
2
3
|
// success or error, returns a credential (API, service, or bypass tokens,
|
|
3
4
|
// signing secrets, bearer URLs, private keys, temporary credentials, auth
|
|
4
5
|
// headers, authorization verifiers) or a stored secret value (environment
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
// Sent-secret redaction runs first, in the provider kit's parse pass, so a
|
|
35
36
|
// rule that decides on a value can find the placeholder where the vendor
|
|
36
37
|
// wrote what the rule matches. Every such decision input fails closed
|
|
37
|
-
|
|
38
|
+
//: an altered type discriminator applies every resource rule; an
|
|
38
39
|
// altered label or type makes a secret record or a header rule; an altered
|
|
39
40
|
// URL scheme or parameter name withholds the whole URL or its value; an
|
|
40
41
|
// altered key withholds its value; a placeholder that may stand for a
|
|
@@ -204,10 +205,7 @@ export function credentialName(name) {
|
|
|
204
205
|
const normalized = normalizedName(name);
|
|
205
206
|
return containsCredential(normalized) || CREDENTIAL_SUFFIX.test(normalized);
|
|
206
207
|
}
|
|
207
|
-
|
|
208
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
209
|
-
}
|
|
210
|
-
/** A rule's test over one decision input; an input redaction altered passes it (#703). */
|
|
208
|
+
/** A rule's test over one decision input; an input redaction altered passes it. */
|
|
211
209
|
function decides(input, test) {
|
|
212
210
|
return typeof input === "string" && (redactionAltered(input) || test(input));
|
|
213
211
|
}
|
|
@@ -215,7 +213,7 @@ function decides(input, test) {
|
|
|
215
213
|
const PLACEHOLDER_KEYS = new WeakSet();
|
|
216
214
|
/**
|
|
217
215
|
* Whether redaction altered one of a record's keys, so no key-based decision
|
|
218
|
-
* about it can be read
|
|
216
|
+
* about it can be read. Parsed JSON keeps its keys; text the kit
|
|
219
217
|
* redacted whole and a result hook parsed after it (NDJSON, log streams) can
|
|
220
218
|
* carry the placeholder in one.
|
|
221
219
|
*/
|
|
@@ -226,7 +224,7 @@ function keysAltered(value) {
|
|
|
226
224
|
const SCHEME_PREFIX = /^(?:[a-z][a-z0-9+.-]*(?::\/{0,2})?)?$/i;
|
|
227
225
|
/**
|
|
228
226
|
* Whether redaction altered the scheme a whole-string URL decision reads
|
|
229
|
-
|
|
227
|
+
*: the text is no longer URL-shaped, everything before its first
|
|
230
228
|
* placeholder could begin a URL, and more than placeholders follows it. A
|
|
231
229
|
* URL a vendor sends holds no whitespace.
|
|
232
230
|
*/
|
|
@@ -434,7 +432,7 @@ function endsValue(text, at) {
|
|
|
434
432
|
/**
|
|
435
433
|
* Where a word at `start` holds a placeholder in place of a link's scheme
|
|
436
434
|
* and host (`[redacted]/pkg?token=…`, `https:[redacted]…`), or `-1`:
|
|
437
|
-
* redaction took the authority that makes text a link
|
|
435
|
+
* redaction took the authority that makes text a link. A Markdown
|
|
438
436
|
* label (`[redacted](…)`) is not one; its destination is read as a link.
|
|
439
437
|
*/
|
|
440
438
|
function markerLed(text, start) {
|
|
@@ -524,7 +522,7 @@ function linkSpan(text, start, host, inline) {
|
|
|
524
522
|
/**
|
|
525
523
|
* Visit each destination once, respecting its CommonMark close and encoded
|
|
526
524
|
* delimiters. Redaction (`failClosed`) also reads a link whose scheme and
|
|
527
|
-
* host redaction took
|
|
525
|
+
* host redaction took; admission never does.
|
|
528
526
|
*/
|
|
529
527
|
function* linkSpans(text, failClosed = false) {
|
|
530
528
|
const markers = failClosed && redactionAltered(text);
|
|
@@ -585,7 +583,7 @@ function credentialQuery(text) {
|
|
|
585
583
|
const fragment = decoded.indexOf("#");
|
|
586
584
|
if (fragment >= 0) {
|
|
587
585
|
const words = decodeLinkText(decoded.slice(fragment + 1));
|
|
588
|
-
// A fragment redaction altered cannot be read
|
|
586
|
+
// A fragment redaction altered cannot be read.
|
|
589
587
|
if (/token|secret|key|code|auth/i.test(words) || alteredFragment(words))
|
|
590
588
|
return true;
|
|
591
589
|
}
|
|
@@ -597,7 +595,7 @@ function credentialQuery(text) {
|
|
|
597
595
|
const equals = pair.indexOf("=");
|
|
598
596
|
at = end + 1;
|
|
599
597
|
// A component redaction altered with no readable name (its name, or its `=`, taken) may be a
|
|
600
|
-
// credential parameter's value, however encoded
|
|
598
|
+
// credential parameter's value, however encoded.
|
|
601
599
|
if (pair !== REDACTED && redactionAltered(equals < 0 ? pair : pair.slice(0, equals)))
|
|
602
600
|
return true;
|
|
603
601
|
if (equals < 0)
|
|
@@ -615,7 +613,7 @@ function withholdLinkQueries(text) {
|
|
|
615
613
|
let out = "";
|
|
616
614
|
let copied = 0;
|
|
617
615
|
for (const { cut, open, end, start, host } of linkSpans(text, true)) {
|
|
618
|
-
// A parameter name redaction altered cannot be read, so the whole link goes
|
|
616
|
+
// A parameter name redaction altered cannot be read, so the whole link goes.
|
|
619
617
|
const whole = alteredLink(text.slice(host, end));
|
|
620
618
|
if (!whole && (cut < 0 || !credentialQuery(text.slice(cut, end))))
|
|
621
619
|
continue;
|
|
@@ -640,7 +638,7 @@ function withholdLinkQueries(text) {
|
|
|
640
638
|
}
|
|
641
639
|
return copied === 0 ? text : out + text.slice(copied);
|
|
642
640
|
}
|
|
643
|
-
/** Whether redaction altered a parameter name in a link's own path, query, or fragment
|
|
641
|
+
/** Whether redaction altered a parameter name in a link's own path, query, or fragment. */
|
|
644
642
|
function alteredLink(link) {
|
|
645
643
|
return redactionAltered(link) && parameterNames(decodeLinkText(link)).some(redactionAltered);
|
|
646
644
|
}
|
|
@@ -676,7 +674,7 @@ function withholdSigningParams(anchored) {
|
|
|
676
674
|
}
|
|
677
675
|
const NAME_CHAR = /[\w.%~;-]/;
|
|
678
676
|
const NAME_ESCAPE = new RegExp(Array.from("abcdefghijklmnopqrstuvwxyz0123456789_.~-", signingName).join("|"), "iy");
|
|
679
|
-
/** Where a parameter's value ends, reading placeholders and character references whole
|
|
677
|
+
/** Where a parameter's value ends, reading placeholders and character references whole. */
|
|
680
678
|
function valueEnd(text, at, inferred) {
|
|
681
679
|
while (at < text.length) {
|
|
682
680
|
const reference = Math.max(characterReferenceEnd(text, at, inferred), bareReferenceEnd(text, at, inferred));
|
|
@@ -693,7 +691,7 @@ function valueEnd(text, at, inferred) {
|
|
|
693
691
|
}
|
|
694
692
|
/**
|
|
695
693
|
* The next placeholder that may hide a signing parameter, at or after `from`,
|
|
696
|
-
* and its value boundary when a name is recognized
|
|
694
|
+
* and its value boundary when a name is recognized. Redaction may have taken a signing name, its
|
|
697
695
|
* `=`, and the start of its value, so a placeholder followed by any of a
|
|
698
696
|
* value's base64 or base64url characters (`+`, `/`, `=`, `-`, `_` included)
|
|
699
697
|
* or `%` escapes goes with them, however short. References count when they
|
|
@@ -956,7 +954,7 @@ function labelsOf(value) {
|
|
|
956
954
|
}
|
|
957
955
|
/**
|
|
958
956
|
* Whether a record's label names a credential, so its value or args are one.
|
|
959
|
-
* A label redaction altered, or keys that may hide one, does too
|
|
957
|
+
* A label redaction altered, or keys that may hide one, does too.
|
|
960
958
|
*/
|
|
961
959
|
function credentialLabelled(value) {
|
|
962
960
|
if (!isRecord(value))
|
|
@@ -966,17 +964,17 @@ function credentialLabelled(value) {
|
|
|
966
964
|
(isRecord(target) && keysAltered(target)) ||
|
|
967
965
|
labelsOf(value).some((label) => decides(label, credentialName)));
|
|
968
966
|
}
|
|
969
|
-
/** A record typed as a secret (`secret_text`); a type redaction altered may have been one
|
|
967
|
+
/** A record typed as a secret (`secret_text`); a type redaction altered may have been one. */
|
|
970
968
|
function typedSecret(value) {
|
|
971
969
|
return keysAltered(value) || decides(value["type"], (type) => /secret/i.test(type));
|
|
972
970
|
}
|
|
973
|
-
/** A list item typed as an environment variable; a type redaction altered may have been one
|
|
971
|
+
/** A list item typed as an environment variable; a type redaction altered may have been one. */
|
|
974
972
|
function envTyped(value) {
|
|
975
973
|
if (!isRecord(value))
|
|
976
974
|
return false;
|
|
977
975
|
return keysAltered(value) || decides(String(value["type"]), (type) => /env/i.test(type));
|
|
978
976
|
}
|
|
979
|
-
/** A header, cookie, or query rule (`request.headers`, `header`, `cookie`, `query`, …); likewise
|
|
977
|
+
/** A header, cookie, or query rule (`request.headers`, `header`, `cookie`, `query`, …); likewise. */
|
|
980
978
|
function headerLike(value) {
|
|
981
979
|
return isRecord(value) && (keysAltered(value) || decides(value["type"], (type) => /header|cookie|query/i.test(type)));
|
|
982
980
|
}
|
|
@@ -989,7 +987,7 @@ function headerValues(value) {
|
|
|
989
987
|
if (isRecord(value))
|
|
990
988
|
return Object.fromEntries(Object.keys(value).map((name) => [name, REDACTED]));
|
|
991
989
|
if (Array.isArray(value)) {
|
|
992
|
-
// A pair whose keys redaction altered may hold its value under any of them
|
|
990
|
+
// A pair whose keys redaction altered may hold its value under any of them.
|
|
993
991
|
return value.map((item) => !isRecord(item)
|
|
994
992
|
? item
|
|
995
993
|
: keysAltered(item)
|
|
@@ -1120,7 +1118,7 @@ function scrubEnv(value, depth) {
|
|
|
1120
1118
|
if (depth > 32)
|
|
1121
1119
|
return REDACTED;
|
|
1122
1120
|
if (Array.isArray(value)) {
|
|
1123
|
-
// A `NAME=value` string, or one whose name and `=` redaction may have taken
|
|
1121
|
+
// A `NAME=value` string, or one whose name and `=` redaction may have taken.
|
|
1124
1122
|
return value.map((item) => typeof item === "string" && (item.includes("=") || redactionAltered(item)) ? REDACTED : scrubEnv(item, depth + 1));
|
|
1125
1123
|
}
|
|
1126
1124
|
if (!isRecord(value))
|
|
@@ -1159,7 +1157,7 @@ function scrubValues(value, depth) {
|
|
|
1159
1157
|
}
|
|
1160
1158
|
return out;
|
|
1161
1159
|
}
|
|
1162
|
-
/** Every rule's paths and withheld fields: the rule for an object whose type redaction altered
|
|
1160
|
+
/** Every rule's paths and withheld fields: the rule for an object whose type redaction altered. */
|
|
1163
1161
|
const ANY_RULE = new WeakMap();
|
|
1164
1162
|
function anyRule(resources) {
|
|
1165
1163
|
let rule = ANY_RULE.get(resources);
|
|
@@ -1172,7 +1170,7 @@ function anyRule(resources) {
|
|
|
1172
1170
|
}
|
|
1173
1171
|
/**
|
|
1174
1172
|
* What a record's type discriminator selects. A type redaction altered, or
|
|
1175
|
-
* one an altered key may hide, selects every rule at once
|
|
1173
|
+
* one an altered key may hide, selects every rule at once.
|
|
1176
1174
|
*/
|
|
1177
1175
|
function typed(record, key, rules, any) {
|
|
1178
1176
|
const type = record[key];
|
|
@@ -1247,7 +1245,7 @@ function heuristic(value, exempt) {
|
|
|
1247
1245
|
if (!exempt.reviewedUrls)
|
|
1248
1246
|
return URL_SHAPE.test(current) ? sanitizeUrl(current) : current;
|
|
1249
1247
|
// A placeholder's own brackets are not Markdown; a URL whose scheme redaction
|
|
1250
|
-
// altered may still carry userinfo, so it is withheld whole
|
|
1248
|
+
// altered may still carry userinfo, so it is withheld whole.
|
|
1251
1249
|
const altered = redactionAltered(current);
|
|
1252
1250
|
if (TEXT.test(altered ? current.replaceAll(REDACTED, "") : current))
|
|
1253
1251
|
return current;
|
|
@@ -1269,7 +1267,7 @@ function heuristic(value, exempt) {
|
|
|
1269
1267
|
const normalized = normalizedName(key);
|
|
1270
1268
|
const review = Object.hasOwn(exempt.fields, key) ? exempt.fields[key] : undefined;
|
|
1271
1269
|
if (!placeholders && redactionAltered(key)) {
|
|
1272
|
-
// A key redaction altered may have named a credential
|
|
1270
|
+
// A key redaction altered may have named a credential.
|
|
1273
1271
|
out[key] = withheld(item);
|
|
1274
1272
|
}
|
|
1275
1273
|
else if (review?.verdict === "redact") {
|
|
@@ -1313,7 +1311,7 @@ function heuristic(value, exempt) {
|
|
|
1313
1311
|
typeof item === "string" &&
|
|
1314
1312
|
(URL_SHAPE.test(item) || alteredScheme(item))) {
|
|
1315
1313
|
// An external destination can carry its secret in the path; relative paths stay.
|
|
1316
|
-
// One whose scheme redaction altered does not parse, so it is withheld
|
|
1314
|
+
// One whose scheme redaction altered does not parse, so it is withheld.
|
|
1317
1315
|
out[key] = originOnly(item);
|
|
1318
1316
|
}
|
|
1319
1317
|
else if (typeof item === "string" && verbatim?.has(key)) {
|
|
@@ -1340,6 +1338,53 @@ function acceptsCredential(schema, depth) {
|
|
|
1340
1338
|
acceptsCredential(node.items, depth + 1) ||
|
|
1341
1339
|
(node.anyOf ?? []).some((branch) => acceptsCredential(branch, depth + 1)));
|
|
1342
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
|
+
}
|
|
1343
1388
|
/** A vendor's value-safety mechanism over its reviewed table and pinned index. */
|
|
1344
1389
|
export function valueSafety(table, index) {
|
|
1345
1390
|
const fields = table.fields ?? {};
|
|
@@ -1349,9 +1394,42 @@ export function valueSafety(table, index) {
|
|
|
1349
1394
|
const key = `${method} ${template}`;
|
|
1350
1395
|
return Object.hasOwn(table.operations, key) ? table.operations[key] : undefined;
|
|
1351
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);
|
|
1352
1405
|
return {
|
|
1353
1406
|
table,
|
|
1354
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
|
+
},
|
|
1355
1433
|
refusal(method, template) {
|
|
1356
1434
|
const reviewed = verdict(method, template);
|
|
1357
1435
|
return reviewed?.verdict === "refuse" ? `Connecta refuses ${method} ${template}. ${reviewed.reason}` : undefined;
|
|
@@ -1367,7 +1445,7 @@ export function valueSafety(table, index) {
|
|
|
1367
1445
|
}
|
|
1368
1446
|
const resources = table.embeddedUrls.resources;
|
|
1369
1447
|
if (resources) {
|
|
1370
|
-
// A type redaction altered takes every resource's reviewed text paths
|
|
1448
|
+
// A type redaction altered takes every resource's reviewed text paths.
|
|
1371
1449
|
const all = [...new Set(Object.values(resources.paths).flat())];
|
|
1372
1450
|
const any = () => all;
|
|
1373
1451
|
const scan = (value, depth) => {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Routes, routePhrases } from "../../agent-routes.js";
|
|
2
|
+
import { pathWithholds } from "../_shared/rest/value-safety.js";
|
|
3
|
+
import { asRecord } from "../_shared/json.js";
|
|
1
4
|
// Breeze's reviewed GET-only vendor. HTTP methods do not establish safety: see tools.ts.
|
|
2
5
|
import { Effect, Schema } from "effect";
|
|
3
6
|
import { ConnectorCallError, resendAdvice } from "../../errors.js";
|
|
@@ -9,12 +12,6 @@ import { openapi } from "./openapi.generated.js";
|
|
|
9
12
|
export const DEFAULT_PAGE_SIZE = 100;
|
|
10
13
|
export const MAX_PAGE_SIZE = 1000;
|
|
11
14
|
export const BREEZE_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
12
|
-
export function asRecord(value) {
|
|
13
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
14
|
-
}
|
|
15
|
-
export function asArray(value) {
|
|
16
|
-
return Array.isArray(value) ? value : [];
|
|
17
|
-
}
|
|
18
15
|
// --- The read hatch's allowlist ----------------------------------------------
|
|
19
16
|
/** Reviewed observational endpoints and their documented query names. GET alone establishes nothing. */
|
|
20
17
|
export const READ_ENDPOINTS = new Map([
|
|
@@ -472,10 +469,11 @@ export const admit = (url) => {
|
|
|
472
469
|
};
|
|
473
470
|
export const authenticate = Effect.gen(function* () {
|
|
474
471
|
const values = yield* (yield* Credentials).values.pipe(Effect.mapError((error) => new Refusal({ error })));
|
|
472
|
+
const routes = yield* Routes;
|
|
475
473
|
const key = values?.["value"]?.trim();
|
|
476
474
|
if (!key)
|
|
477
475
|
return yield* Effect.fail(new Refusal({
|
|
478
|
-
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.`),
|
|
479
477
|
}));
|
|
480
478
|
return { "Api-Key": key };
|
|
481
479
|
});
|
|
@@ -542,6 +540,9 @@ export const breezeSafety = {
|
|
|
542
540
|
})(),
|
|
543
541
|
}),
|
|
544
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,
|
|
545
546
|
withholdsErrors: () => false,
|
|
546
547
|
},
|
|
547
548
|
sensitiveFields: { names: [], choice: "drop" },
|
|
@@ -551,7 +552,7 @@ function shortText(value, secrets) {
|
|
|
551
552
|
return value.trim() || undefined;
|
|
552
553
|
if (value === undefined || value === null)
|
|
553
554
|
return undefined;
|
|
554
|
-
// The kit redacted the answer's values and kept its keys
|
|
555
|
+
// The kit redacted the answer's values and kept its keys; serialized, a key is text
|
|
555
556
|
// the cut below could split, so the whole text is checked before it is cut.
|
|
556
557
|
const text = secrets.redact(JSON.stringify(value));
|
|
557
558
|
return text.length > 300 ? `${text.slice(0, 300)}…` : text;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { readOnlyNotice } from "../_shared/guide.js";
|
|
2
|
+
import { asRecord } from "../_shared/json.js";
|
|
1
3
|
import { skill } from "./skill.generated.js";
|
|
2
4
|
import { keys, optionsOf } from "../../config-schema.js";
|
|
3
5
|
import { PROVIDER_COMMON } from "../../connectors/option-shapes.js";
|
|
4
6
|
import { defineApiProvider } from "../../http/provider.js";
|
|
5
|
-
import { accountSummary, admit,
|
|
7
|
+
import { accountSummary, admit, authenticate, BreezeApi, breezeSafety, BREEZE_MAX_RESPONSE_BYTES, BREEZE_INDEX, BREEZE_VERDICTS, BREEZE_NAMED, breezePeoplePage, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, errors, result, } from "./api.js";
|
|
6
8
|
import { breezeTools } from "./tools.js";
|
|
7
9
|
export const BREEZE_HOST_SUFFIX = ".breezechms.com";
|
|
8
10
|
const SUBDOMAIN_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -68,7 +70,7 @@ export const breeze = defineApiProvider({
|
|
|
68
70
|
`Church database: ${normalizeSubdomain(options.subdomain)}${BREEZE_HOST_SUFFIX}.`,
|
|
69
71
|
`Church purpose: ${options.purpose}`,
|
|
70
72
|
"Calendar feed addresses and embed keys are always removed; profile details follow the requested vendor detail level.",
|
|
71
|
-
...(options.readOnly
|
|
73
|
+
...readOnlyNotice(options.readOnly),
|
|
72
74
|
...(options.defaultPageSize !== undefined
|
|
73
75
|
? [`Paged lists return ${options.defaultPageSize} items by default on this connection.`]
|
|
74
76
|
: []),
|
|
@@ -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
|
};
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
+
import { asArray, asRecord } from "../_shared/json.js";
|
|
2
|
+
import { compact } from "../_shared/json.js";
|
|
1
3
|
// Breeze's curated surface: projections and profile encoders earn named tools; the standard hatch supplies coverage.
|
|
2
4
|
import { Effect, Schema } from "effect";
|
|
3
5
|
import { ConnectorCallError } from "../../errors.js";
|
|
4
6
|
import { Refusal } from "../../http/errors.js";
|
|
5
7
|
import { Integer, isMinChars, isMaxChars } from "../../http/json-schema.js";
|
|
6
8
|
import { RequestSecrets, Classification } from "../../http/services.js";
|
|
7
|
-
import {
|
|
9
|
+
import { fieldsContract, selectFields } from "../../http/fields.js";
|
|
8
10
|
import { read, list, write as writeTool } from "../../http/tools.js";
|
|
9
|
-
import {
|
|
11
|
+
import { READ_ENDPOINTS } from "./api.js";
|
|
10
12
|
const DEFAULT_EVENT_LIMIT = 100;
|
|
11
13
|
const ObjectValue = Schema.Record(Schema.String, Schema.Unknown);
|
|
12
|
-
function compact(value) {
|
|
13
|
-
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
14
|
-
}
|
|
15
14
|
function timestamp(value) {
|
|
16
15
|
if (value === undefined)
|
|
17
16
|
return undefined;
|
|
@@ -219,6 +218,7 @@ const Result_list_events = Schema.Struct({
|
|
|
219
218
|
});
|
|
220
219
|
const Row_list_events = Result_list_events.fields.events.value;
|
|
221
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);
|
|
222
222
|
const Result_list_attendance = Schema.Struct({
|
|
223
223
|
"attendance": Schema.Array(Schema.Struct({
|
|
224
224
|
"personId": Schema.String,
|
|
@@ -528,7 +528,7 @@ export function breezeTools({ connection, pageSize: defaultPageSize, }) {
|
|
|
528
528
|
description: "Instances returned, 1 to Breeze's 1000. Defaults to 100.",
|
|
529
529
|
default: 100,
|
|
530
530
|
})),
|
|
531
|
-
fields: Schema.optionalKey(
|
|
531
|
+
fields: Schema.optionalKey(EVENT_FIELDS.input),
|
|
532
532
|
}),
|
|
533
533
|
output: Schema.Struct({
|
|
534
534
|
items: Schema.Array(SelectedEvent),
|
|
@@ -536,7 +536,7 @@ export function breezeTools({ connection, pageSize: defaultPageSize, }) {
|
|
|
536
536
|
truncated: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
537
537
|
}),
|
|
538
538
|
run: (args) => Effect.gen(function* () {
|
|
539
|
-
const error =
|
|
539
|
+
const error = EVENT_FIELDS.problem(args.fields);
|
|
540
540
|
if (error)
|
|
541
541
|
return yield* Effect.fail(new Refusal({ error }));
|
|
542
542
|
const result = yield* load_list_events(args);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { asRecord, asArray } from "../_shared/json.js";
|
|
2
|
+
import { routePhrases } from "../../agent-routes.js";
|
|
1
3
|
import { CCB_MEDIA_TYPE, CCB_ENVIRONMENTS, CCB_READ_SCOPES, CCB_WRITE_SCOPES, CCB_SENSITIVE_FIELDS, } from "./lib/constants.js";
|
|
2
4
|
// Pushpay ChMS v2 JSON contracts, OAuth, status meanings, admission and privacy.
|
|
3
5
|
// The legacy per-church XML API is outside this provider's scope.
|
|
@@ -56,12 +58,6 @@ export const CCB_ADMISSION = {
|
|
|
56
58
|
},
|
|
57
59
|
],
|
|
58
60
|
};
|
|
59
|
-
function asRecord(value) {
|
|
60
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
61
|
-
}
|
|
62
|
-
function asArray(value) {
|
|
63
|
-
return Array.isArray(value) ? value : [];
|
|
64
|
-
}
|
|
65
61
|
// ---------------------------------------------------------------------------
|
|
66
62
|
// Admission partitions
|
|
67
63
|
// ---------------------------------------------------------------------------
|
|
@@ -470,7 +466,7 @@ export const errors = {
|
|
|
470
466
|
statuses: {
|
|
471
467
|
401: () => ({
|
|
472
468
|
code: "auth_required",
|
|
473
|
-
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")}.`,
|
|
474
470
|
advice: ", then retry",
|
|
475
471
|
}),
|
|
476
472
|
403: () => ({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readOnlyNotice } from "../_shared/guide.js";
|
|
1
2
|
import { CCB_ENVIRONMENTS, CCB_MEDIA_TYPE } from "./lib/constants.js";
|
|
2
3
|
import { skill } from "./skill.generated.js";
|
|
3
4
|
import { keys, optionsOf } from "../../config-schema.js";
|
|
@@ -46,6 +47,11 @@ export const ccb = defineApiProvider({
|
|
|
46
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).",
|
|
47
48
|
},
|
|
48
49
|
skill,
|
|
50
|
+
// The guide resolves a person from individuals and a household from families.
|
|
51
|
+
searchSynonyms: [
|
|
52
|
+
["person", "individual"],
|
|
53
|
+
["household", "family"],
|
|
54
|
+
],
|
|
49
55
|
options: CCB_OPTIONS,
|
|
50
56
|
displayTitle: (options) => defaultTitle(options.environment, options.mode, writable(options)),
|
|
51
57
|
description: (options) => `Church Community Builder (Pushpay ChMS) — ${routingLine(options.environment, options.mode, writable(options))}: ${options.purpose}`,
|
|
@@ -70,7 +76,7 @@ export const ccb = defineApiProvider({
|
|
|
70
76
|
...(!writable(options)
|
|
71
77
|
? ["Configured scopes permit only reads: ccb_api_write is omitted and writes are refused."]
|
|
72
78
|
: []),
|
|
73
|
-
...(options.readOnly
|
|
79
|
+
...readOnlyNotice(options.readOnly),
|
|
74
80
|
...(options.defaultPerPage !== undefined
|
|
75
81
|
? [`Named lists default to ${options.defaultPerPage} rows on this connection.`]
|
|
76
82
|
: []),
|
|
@@ -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
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asRecord, asArray } from "../_shared/json.js";
|
|
1
2
|
// CCB v2 curated projections, bound numbered pages and reviewed POST data search.
|
|
2
3
|
import { Effect, Schema } from "effect";
|
|
3
4
|
import { ConnectorCallError } from "../../errors.js";
|
|
@@ -7,12 +8,6 @@ import { Refusal } from "../../http/errors.js";
|
|
|
7
8
|
import { Integer, isMinChars, isMaxChars } from "../../http/json-schema.js";
|
|
8
9
|
import { read, list } from "../../http/tools.js";
|
|
9
10
|
import { callCcb, DEFAULT_PER_PAGE, SEARCH_DOMAINS, CCB_INDEX, ccbPageInfo, } from "./api.js";
|
|
10
|
-
function asRecord(value) {
|
|
11
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
12
|
-
}
|
|
13
|
-
function asArray(value) {
|
|
14
|
-
return Array.isArray(value) ? value : [];
|
|
15
|
-
}
|
|
16
11
|
function compact(value) {
|
|
17
12
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined && entry !== null));
|
|
18
13
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeBase64 } from "../../bytes.js";
|
|
1
2
|
// Cloudflare's pinned REST contracts, confinement, framing and failure semantics.
|
|
2
3
|
import { responseMedia } from "../_shared/rest/response-media.js";
|
|
3
4
|
import { EXCLUSIVE_PATH_IDS } from "./lib/path-bounds.js";
|
|
@@ -396,8 +397,7 @@ export class RawUpload {
|
|
|
396
397
|
}
|
|
397
398
|
export function bytesFromBase64(value, field) {
|
|
398
399
|
try {
|
|
399
|
-
|
|
400
|
-
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
400
|
+
return decodeBase64(value);
|
|
401
401
|
}
|
|
402
402
|
catch {
|
|
403
403
|
return invalid(`${field} must be valid base64.`);
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { readOnlyNotice } from "../_shared/guide.js";
|
|
1
2
|
import { skill } from "./skill.generated.js";
|
|
2
3
|
import { keys, optionsOf, variants } from "../../config-schema.js";
|
|
3
4
|
import { CALL_ADMISSION } from "../../provider.js";
|
|
@@ -90,7 +91,7 @@ function usageGuide(options) {
|
|
|
90
91
|
: ["No additional deployment pin: the credential's vendor permissions define its reachable estate."]),
|
|
91
92
|
...(accountId ? [`Default account id: ${accountId}.`] : []),
|
|
92
93
|
...(zoneId ? [`Default zone id: ${zoneId}.`] : []),
|
|
93
|
-
...(options.readOnly
|
|
94
|
+
...readOnlyNotice(options.readOnly),
|
|
94
95
|
],
|
|
95
96
|
required: true,
|
|
96
97
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Cloudflare's reviewed value-safety table
|
|
1
|
+
// Cloudflare's reviewed value-safety table, on the shared mechanism in
|
|
2
2
|
// `../_shared/rest/value-safety.ts`: every operation the detector flags in
|
|
3
3
|
// the pinned spec (`value-safety.candidates.json`) has one verdict here, and
|
|
4
4
|
// `value-safety.node.test.ts` runs the shared harness over it.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// Paths are dot paths into the data a tool returns (Cloudflare's `result`,
|
|
17
17
|
// or the whole body when it is not an envelope); lists are traversed
|
|
18
18
|
// implicitly and `*` matches every key of an object.
|
|
19
|
-
// Embedded URL verdict
|
|
19
|
+
// Embedded URL verdict: reviewed paths + global signing parameters.
|
|
20
20
|
// Evidence: digest-verified schema revision 843eae2fbf4fe774ef9c91021dd34017ac9c970a:
|
|
21
21
|
// builds_BuildLogsResponse.lines (timestamp/log-message tuples),
|
|
22
22
|
// pages_deployment_log.data.line and workers-observability_query_results
|
|
@@ -869,7 +869,7 @@ const OPERATIONS = {
|
|
|
869
869
|
key,
|
|
870
870
|
safe(DESCRIBED_ONLY, "geo_restrictions", "policy_restrictions", "certificates.geo_restrictions"),
|
|
871
871
|
])),
|
|
872
|
-
// Round 3
|
|
872
|
+
// Round 3: candidates the consolidated detector surfaced (the union
|
|
873
873
|
// of the two former detectors, with transfer, claim, invite, key, signing,
|
|
874
874
|
// and webhook families).
|
|
875
875
|
[`GET ${A}/abuse-reports/submitted/{report_id}`]: safe("An abuse report; the authorization statement is the reporter's text."),
|