@zackbart/connecta 0.30.0 → 0.32.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 +66 -0
- package/README.md +21 -16
- package/bin/connecta.mjs +1 -0
- package/bin/version.mjs +1 -1
- package/dist/auth-elicitation.js +1 -1
- package/dist/call-recovery.js +25 -0
- package/dist/catalog-service.js +7 -2
- package/dist/config-schema.d.ts +14 -7
- package/dist/config-schema.js +29 -10
- package/dist/config-value-sources.js +6 -3
- package/dist/config.d.ts +26 -8
- package/dist/config.js +38 -16
- package/dist/connector-access.d.ts +2 -1
- package/dist/connectors/guarded-fetch.d.ts +18 -1
- package/dist/connectors/guarded-fetch.js +21 -2
- package/dist/describe-config.d.ts +6 -3
- package/dist/describe-config.js +3 -2
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.js +4 -3
- package/dist/invocation.js +39 -4
- package/dist/meta-tools.js +17 -19
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -0
- package/dist/operator-ui/contract.d.ts +5 -4
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +1 -1
- package/dist/operator-ui/view.d.ts +9 -0
- package/dist/operator-ui/view.js +12 -0
- package/dist/program-writes.js +4 -49
- package/dist/provider.d.ts +6 -5
- package/dist/provider.js +3 -4
- package/dist/providers/_shared/google/workspace.js +1 -1
- package/dist/providers/_shared/rest/dispatch.js +41 -0
- package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
- package/dist/providers/_shared/rest/operation-index.js +527 -0
- package/dist/providers/_shared/rest/tools.d.ts +141 -0
- package/dist/providers/_shared/rest/tools.js +569 -0
- package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
- package/dist/providers/_shared/rest/value-safety.js +580 -0
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/cloudflare/graphql.js +385 -0
- package/dist/providers/cloudflare/index.d.ts +47 -51
- package/dist/providers/cloudflare/index.js +188 -1997
- package/dist/providers/cloudflare/openapi.generated.js +3347 -0
- package/dist/providers/cloudflare/rest.d.ts +37 -0
- package/dist/providers/cloudflare/rest.js +1056 -0
- package/dist/providers/cloudflare/skill.generated.js +7 -4
- package/dist/providers/cloudflare/value-safety.js +1247 -0
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +274 -23
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.d.ts +35 -31
- package/dist/providers/notion/index.js +76 -1499
- package/dist/providers/notion/openapi.generated.js +75 -0
- package/dist/providers/notion/rest.d.ts +24 -0
- package/dist/providers/notion/rest.js +1186 -0
- package/dist/providers/notion/skill.generated.js +5 -4
- package/dist/providers/notion/value-safety.js +31 -0
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +286 -24
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +1 -1
- package/dist/providers/sheets/index.js +3 -3
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/stripe/index.d.ts +26 -20
- package/dist/providers/stripe/index.js +103 -130
- package/dist/providers/stripe/openapi.generated.js +649 -0
- package/dist/providers/stripe/rest.d.ts +14 -0
- package/dist/providers/stripe/rest.js +395 -0
- package/dist/providers/stripe/skill.generated.js +7 -5
- package/dist/providers/stripe/value-safety.js +705 -0
- package/dist/providers/vercel/index.d.ts +31 -22
- package/dist/providers/vercel/index.js +96 -853
- package/dist/providers/vercel/openapi.generated.js +456 -0
- package/dist/providers/vercel/rest.d.ts +20 -0
- package/dist/providers/vercel/rest.js +1002 -0
- package/dist/providers/vercel/skill.generated.js +4 -5
- package/dist/providers/vercel/value-safety.js +328 -0
- package/dist/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/routes/mcp.js +30 -10
- package/dist/routes/operator.js +14 -7
- package/dist/routes/ui-config.js +2 -2
- package/dist/server.js +3 -0
- package/dist/skills.js +5 -4
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1 -1
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +47 -0
- package/documentation/README.md +1 -0
- package/documentation/architecture.md +155 -28
- package/documentation/auth.md +86 -27
- package/documentation/code-mode.md +81 -72
- package/documentation/deploying.md +10 -5
- package/documentation/integrating.md +72 -9
- package/documentation/meta-tools.md +52 -38
- package/documentation/operating.md +11 -8
- package/documentation/operator-tests.md +2 -3
- package/documentation/operator-ui.md +9 -3
- package/documentation/provider-auth-migration.md +308 -0
- package/documentation/provider-migration-0.29.md +18 -6
- package/examples/worker/AGENTS.md +5 -0
- package/examples/worker/README.md +2 -2
- package/examples/worker/src/connecta.config.ts +5 -0
- package/package.json +7 -2
- package/templates/node/AGENTS.md +6 -4
- package/templates/node/README.md +6 -4
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +5 -0
- package/dist/providers/vercel/mcp-ownership.js +0 -212
- package/dist/tool-safety.d.ts +0 -9
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/** What a removed secret reads as. */
|
|
2
|
+
export const REDACTED = "[redacted]";
|
|
3
|
+
/** Credential vocabulary over normalized names, matched anywhere; mirrors `scripts/value-safety.mjs`. */
|
|
4
|
+
export const CREDENTIAL_WORDS = [
|
|
5
|
+
"token",
|
|
6
|
+
"secret",
|
|
7
|
+
"password",
|
|
8
|
+
"passphrase",
|
|
9
|
+
"privatekey",
|
|
10
|
+
"apikey",
|
|
11
|
+
"accesskey",
|
|
12
|
+
"authorization",
|
|
13
|
+
"cookie",
|
|
14
|
+
"signature",
|
|
15
|
+
"jwt",
|
|
16
|
+
"credential",
|
|
17
|
+
"bypass",
|
|
18
|
+
"verifier",
|
|
19
|
+
"devicecode",
|
|
20
|
+
"authcode",
|
|
21
|
+
"keyvalue",
|
|
22
|
+
"invitecode",
|
|
23
|
+
"joincode",
|
|
24
|
+
"claimcode",
|
|
25
|
+
"transfercode",
|
|
26
|
+
"accesscode",
|
|
27
|
+
];
|
|
28
|
+
/** Credential vocabulary matched at the end of a normalized name; mirrors the script. */
|
|
29
|
+
export const CREDENTIAL_SUFFIX = /(tokens?|secrets?|passwords?|passphrases?|privatekeys?|apikeys?|authkey|authorization|cookies?|signature|jwts?|credentials?|psks?|streamkeys?|uploadurl|signedurl|jwks?|verifier|devicecode|bypass)$/;
|
|
30
|
+
/** Request fields that carry secrets into a vendor without naming a credential: headers, environment values, bindings. */
|
|
31
|
+
const INPUT_CONTAINERS = /(envvars|environmentvariables|bindings|headers?)$/;
|
|
32
|
+
/** Suffixes of metadata about a credential (ids, prefixes, last four, counts, expiry, scopes); mirrors the script. */
|
|
33
|
+
export const METADATA_SUFFIXES = [
|
|
34
|
+
"id",
|
|
35
|
+
"ids",
|
|
36
|
+
"prefix",
|
|
37
|
+
"suffix",
|
|
38
|
+
"lastfour",
|
|
39
|
+
"lastfourchars",
|
|
40
|
+
"count",
|
|
41
|
+
"type",
|
|
42
|
+
"types",
|
|
43
|
+
"name",
|
|
44
|
+
"names",
|
|
45
|
+
"at",
|
|
46
|
+
"expiry",
|
|
47
|
+
"expires",
|
|
48
|
+
"scope",
|
|
49
|
+
"scopes",
|
|
50
|
+
"enabled",
|
|
51
|
+
"changed",
|
|
52
|
+
"protection",
|
|
53
|
+
"details",
|
|
54
|
+
"length",
|
|
55
|
+
"mode",
|
|
56
|
+
"kind",
|
|
57
|
+
"status",
|
|
58
|
+
];
|
|
59
|
+
/** Exact metadata names that contain a credential word; mirrors the script. */
|
|
60
|
+
export const METADATA_NAMES = [
|
|
61
|
+
"partialkeyvalue",
|
|
62
|
+
"partialkey",
|
|
63
|
+
"inputtokens",
|
|
64
|
+
"outputtokens",
|
|
65
|
+
"totaltokens",
|
|
66
|
+
"cachecreationinputtokens",
|
|
67
|
+
"cachereadinputtokens",
|
|
68
|
+
"usedapptoken",
|
|
69
|
+
"includesrefreshtoken",
|
|
70
|
+
"hasauthorizationdetails",
|
|
71
|
+
"tokensdeleted",
|
|
72
|
+
"istokenexpired",
|
|
73
|
+
"tokenclaims",
|
|
74
|
+
"oidctokenclaims",
|
|
75
|
+
"preauthorizationamount",
|
|
76
|
+
"secretrotation",
|
|
77
|
+
"secretssync",
|
|
78
|
+
"strictpasswordprotectionsettings",
|
|
79
|
+
"disjunctiveproductionsecretpolicy",
|
|
80
|
+
"stripesharedpaymenttokenused",
|
|
81
|
+
"bypassall",
|
|
82
|
+
"bypasssystem",
|
|
83
|
+
];
|
|
84
|
+
/** Keys whose contents are environment variables, whatever their `key` or `type` says. */
|
|
85
|
+
const ENV_CONTAINERS = ["env", "envs", "envvar", "envvars", "newenvvar", "oldenvvar", "sharedenvvar", "sharedenvvars"];
|
|
86
|
+
const ENV_VALUE_FIELDS = ["value", "vsmValue", "legacyValue", "decryptedValue"];
|
|
87
|
+
/** The fields of a labelled or typed secret record that hold what the label names. */
|
|
88
|
+
const LABELLED_VALUE_FIELDS = [...ENV_VALUE_FIELDS, "args", "text"];
|
|
89
|
+
/** Query parameters that carry a credential in a URL: either rule. */
|
|
90
|
+
const URL_PARAM_CONTAINS = /token|secret|password|passwd|signature|^sig$|key|auth|code|jwt|credential|bypass/i;
|
|
91
|
+
const URL_PARAM_SUFFIX = /(token|secret|password|passphrase|key|signature|sig|credential|auth|authorization|jwt)$/;
|
|
92
|
+
const URL_SHAPE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
93
|
+
/** The fields of a header, cookie, or query rule that hold the matched or set value. */
|
|
94
|
+
const HEADER_RULE_VALUES = ["value", "values", "args"];
|
|
95
|
+
/** Keys whose absolute URL is a route or redirect destination. */
|
|
96
|
+
const DESTINATION_KEYS = ["dest", "destination", "location"];
|
|
97
|
+
const MAX_DEPTH = 48;
|
|
98
|
+
/** Deny by default: an object is a guest's only when no binding field holds anything but a reviewed guest value. */
|
|
99
|
+
function guestOnly(resource, guest) {
|
|
100
|
+
for (const [field, allowed] of Object.entries(guest.bindings)) {
|
|
101
|
+
const value = resource[field];
|
|
102
|
+
if (value === undefined || value === null || value === "")
|
|
103
|
+
continue;
|
|
104
|
+
if (typeof value === "string" && allowed.includes(value))
|
|
105
|
+
continue;
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
for (const [field, allowed] of Object.entries(guest.requires ?? {})) {
|
|
109
|
+
const value = resource[field];
|
|
110
|
+
if (typeof value !== "string" || !allowed.includes(value))
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
export const refuse = (reason) => ({ verdict: "refuse", reason });
|
|
116
|
+
export const redact = (reason, paths, keep = []) => ({
|
|
117
|
+
verdict: "redact",
|
|
118
|
+
reason,
|
|
119
|
+
paths,
|
|
120
|
+
...(keep.length ? { keep } : {}),
|
|
121
|
+
});
|
|
122
|
+
export const safe = (reason, keep = []) => ({
|
|
123
|
+
verdict: "safe",
|
|
124
|
+
reason,
|
|
125
|
+
...(keep.length ? { keep } : {}),
|
|
126
|
+
});
|
|
127
|
+
/** A verdict whose failures keep the vendor's text: reviewed, for operations whose errors echo no stored secret. */
|
|
128
|
+
export const vendorErrors = (verdict) => verdict.verdict === "refuse" ? verdict : { ...verdict, errors: "vendor" };
|
|
129
|
+
/** Paths under each prefix; a `url:` or `origin:` directive stays in front. */
|
|
130
|
+
export function under(prefixes, paths) {
|
|
131
|
+
return prefixes.flatMap((prefix) => paths.map((path) => {
|
|
132
|
+
if (!prefix)
|
|
133
|
+
return path;
|
|
134
|
+
const directive = /^(?:url|origin):/.exec(path)?.[0] ?? "";
|
|
135
|
+
return `${directive}${prefix}.${path.slice(directive.length)}`;
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
function normalizedName(name) {
|
|
139
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
140
|
+
}
|
|
141
|
+
function metadataName(normalized) {
|
|
142
|
+
return (METADATA_NAMES.includes(normalized) ||
|
|
143
|
+
METADATA_SUFFIXES.some((suffix) => normalized.endsWith(suffix) && normalized !== suffix));
|
|
144
|
+
}
|
|
145
|
+
/** Vercel's former rule: a credential word anywhere in a name that is not metadata. Its value goes whole. */
|
|
146
|
+
function containsCredential(normalized) {
|
|
147
|
+
return CREDENTIAL_WORDS.some((word) => normalized.includes(word)) && !metadataName(normalized);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Whether a key names a credential: a credential word anywhere in a name
|
|
151
|
+
* that is not metadata (Vercel's former rule), or a credential suffix
|
|
152
|
+
* (Cloudflare's).
|
|
153
|
+
*/
|
|
154
|
+
export function credentialName(name) {
|
|
155
|
+
const normalized = normalizedName(name);
|
|
156
|
+
return containsCredential(normalized) || CREDENTIAL_SUFFIX.test(normalized);
|
|
157
|
+
}
|
|
158
|
+
function isRecord(value) {
|
|
159
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
160
|
+
}
|
|
161
|
+
/** A URL without userinfo, credential query parameters, or a credential-looking fragment; anything unparseable is withheld. */
|
|
162
|
+
function sanitizeUrl(value) {
|
|
163
|
+
let url;
|
|
164
|
+
try {
|
|
165
|
+
url = new URL(value);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return REDACTED;
|
|
169
|
+
}
|
|
170
|
+
let changed = false;
|
|
171
|
+
if (url.username || url.password) {
|
|
172
|
+
url.username = "redacted";
|
|
173
|
+
url.password = "";
|
|
174
|
+
changed = true;
|
|
175
|
+
}
|
|
176
|
+
// A snapshot of the names: setting a parameter while iterating the live list is unsafe.
|
|
177
|
+
for (const name of Array.from(url.searchParams.keys())) {
|
|
178
|
+
if (URL_PARAM_CONTAINS.test(name) || URL_PARAM_SUFFIX.test(name.toLowerCase().replace(/[_-]/g, ""))) {
|
|
179
|
+
url.searchParams.set(name, REDACTED);
|
|
180
|
+
changed = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (url.hash && /token|secret|key|code|auth/i.test(url.hash)) {
|
|
184
|
+
url.hash = "";
|
|
185
|
+
changed = true;
|
|
186
|
+
}
|
|
187
|
+
// An ordinary URL comes back exactly as the vendor sent it.
|
|
188
|
+
return changed ? url.toString() : value;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* A destination URL reduced to its origin: webhook, drain, and bearer URLs
|
|
192
|
+
* can carry a secret in the path itself (`hooks.slack.com/services/…`), so
|
|
193
|
+
* only the scheme and host come back.
|
|
194
|
+
*/
|
|
195
|
+
function originOnly(value) {
|
|
196
|
+
if (value === null || value === undefined)
|
|
197
|
+
return value;
|
|
198
|
+
// A destination given as several URLs (`{ traces: "https://…" }`) keeps its shape.
|
|
199
|
+
if (Array.isArray(value))
|
|
200
|
+
return value.map(originOnly);
|
|
201
|
+
if (isRecord(value))
|
|
202
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, originOnly(item)]));
|
|
203
|
+
if (typeof value !== "string")
|
|
204
|
+
return REDACTED;
|
|
205
|
+
try {
|
|
206
|
+
const url = new URL(value);
|
|
207
|
+
const bare = (url.pathname === "/" || url.pathname === "") && !url.search && !url.hash && !url.username;
|
|
208
|
+
// Scheme and host, never userinfo; `origin` reads "null" for non-web schemes (s3://).
|
|
209
|
+
const base = `${url.protocol}//${url.host}`;
|
|
210
|
+
return bare ? base : `${base}/${REDACTED}`;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return REDACTED;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Every label a record carries for its value: `key`, `name`, or a transform's `target.key`. */
|
|
217
|
+
function labelsOf(value) {
|
|
218
|
+
const labels = [];
|
|
219
|
+
if (typeof value["key"] === "string")
|
|
220
|
+
labels.push(value["key"]);
|
|
221
|
+
if (typeof value["name"] === "string")
|
|
222
|
+
labels.push(value["name"]);
|
|
223
|
+
const target = value["target"];
|
|
224
|
+
if (isRecord(target) && typeof target["key"] === "string")
|
|
225
|
+
labels.push(target["key"]);
|
|
226
|
+
return labels;
|
|
227
|
+
}
|
|
228
|
+
/** Whether a record's label names a credential, so its value or args are one. */
|
|
229
|
+
function credentialLabelled(value) {
|
|
230
|
+
return isRecord(value) && labelsOf(value).some(credentialName);
|
|
231
|
+
}
|
|
232
|
+
/** A header, cookie, or query rule (`request.headers`, `header`, `cookie`, `query`, …). */
|
|
233
|
+
function headerLike(value) {
|
|
234
|
+
return isRecord(value) && typeof value["type"] === "string" && /header|cookie|query/i.test(value["type"]);
|
|
235
|
+
}
|
|
236
|
+
/** A route-like object: its `headers` are set on requests or responses. */
|
|
237
|
+
function routeLike(value) {
|
|
238
|
+
return ["src", "source", "dest", "destination"].some((key) => key in value);
|
|
239
|
+
}
|
|
240
|
+
/** Every value of a headers map, or of a list of `{ key, value }` header pairs, withheld. */
|
|
241
|
+
function headerValues(value) {
|
|
242
|
+
if (isRecord(value))
|
|
243
|
+
return Object.fromEntries(Object.keys(value).map((name) => [name, REDACTED]));
|
|
244
|
+
if (Array.isArray(value)) {
|
|
245
|
+
return value.map((item) => (isRecord(item) && "value" in item ? { ...item, value: REDACTED } : item));
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
/** A credential-named key's value: everything but null goes. */
|
|
250
|
+
function withheld(value) {
|
|
251
|
+
return value === null || value === undefined ? value : REDACTED;
|
|
252
|
+
}
|
|
253
|
+
/** Every string under a credential-suffixed key, keeping the subtree's shape (Cloudflare's former rule). */
|
|
254
|
+
function scrubStrings(value) {
|
|
255
|
+
if (typeof value === "string")
|
|
256
|
+
return REDACTED;
|
|
257
|
+
if (Array.isArray(value))
|
|
258
|
+
return value.map(scrubStrings);
|
|
259
|
+
if (isRecord(value))
|
|
260
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, scrubStrings(item)]));
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
/** One reviewed path, split into segments; `#url` is the same as an `origin:` prefix. */
|
|
264
|
+
function parsePath(raw) {
|
|
265
|
+
let path = raw;
|
|
266
|
+
let mode = "redact";
|
|
267
|
+
if (path.startsWith("url:")) {
|
|
268
|
+
mode = "url";
|
|
269
|
+
path = path.slice(4);
|
|
270
|
+
}
|
|
271
|
+
else if (path.startsWith("origin:")) {
|
|
272
|
+
mode = "origin";
|
|
273
|
+
path = path.slice(7);
|
|
274
|
+
}
|
|
275
|
+
if (path.endsWith("#url")) {
|
|
276
|
+
mode = "origin";
|
|
277
|
+
path = path.slice(0, -4);
|
|
278
|
+
}
|
|
279
|
+
const segments = path.split(".").flatMap((part) => {
|
|
280
|
+
if (part === "")
|
|
281
|
+
return [];
|
|
282
|
+
const match = /^([^[{@]*)((?:\[\??[a-z]*\]|\{\}|@keys)*)$/.exec(part);
|
|
283
|
+
if (!match)
|
|
284
|
+
return [part];
|
|
285
|
+
const out = [];
|
|
286
|
+
if (match[1])
|
|
287
|
+
out.push(match[1] === "*" ? "{}" : match[1]);
|
|
288
|
+
for (const token of match[2].match(/\[\??[a-z]*\]|\{\}|@keys/g) ?? [])
|
|
289
|
+
out.push(token);
|
|
290
|
+
return out;
|
|
291
|
+
});
|
|
292
|
+
return { mode, segments };
|
|
293
|
+
}
|
|
294
|
+
const SEGMENT = /^(?:\[\]|\[\?(?:env|credential|header)\]|\{\}|@keys|[^[\]{}@]+)$/;
|
|
295
|
+
/** Whether every segment of a reviewed path is in the path language; the harness checks each table. */
|
|
296
|
+
export function validPath(raw) {
|
|
297
|
+
const { segments } = parsePath(raw);
|
|
298
|
+
return segments.length > 0 && segments.every((segment) => SEGMENT.test(segment));
|
|
299
|
+
}
|
|
300
|
+
/** Apply one reviewed path to a body in place of its value. */
|
|
301
|
+
function applyPath(body, raw) {
|
|
302
|
+
const { mode, segments } = parsePath(raw);
|
|
303
|
+
const step = (current, index) => {
|
|
304
|
+
if (index === segments.length) {
|
|
305
|
+
if (current === undefined || current === null)
|
|
306
|
+
return current;
|
|
307
|
+
if (mode === "url")
|
|
308
|
+
return typeof current === "string" ? sanitizeUrl(current) : REDACTED;
|
|
309
|
+
if (mode === "origin")
|
|
310
|
+
return originOnly(current);
|
|
311
|
+
return REDACTED;
|
|
312
|
+
}
|
|
313
|
+
const segment = segments[index];
|
|
314
|
+
if (segment === "[]" || segment === "[?env]" || segment === "[?credential]" || segment === "[?header]") {
|
|
315
|
+
if (!Array.isArray(current))
|
|
316
|
+
return current;
|
|
317
|
+
return current.map((item) => {
|
|
318
|
+
if (segment === "[?env]" && !/env/i.test(String(isRecord(item) ? item["type"] : "")))
|
|
319
|
+
return item;
|
|
320
|
+
if (segment === "[?credential]" && !credentialLabelled(item))
|
|
321
|
+
return item;
|
|
322
|
+
if (segment === "[?header]" && !headerLike(item))
|
|
323
|
+
return item;
|
|
324
|
+
return step(item, index + 1);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// A named segment, a map, or its keys reached through a list applies to every item.
|
|
328
|
+
if (Array.isArray(current))
|
|
329
|
+
return current.map((item) => step(item, index));
|
|
330
|
+
if (segment === "{}") {
|
|
331
|
+
if (!isRecord(current))
|
|
332
|
+
return current;
|
|
333
|
+
return Object.fromEntries(Object.entries(current).map(([key, item]) => [key, step(item, index + 1)]));
|
|
334
|
+
}
|
|
335
|
+
if (segment === "@keys") {
|
|
336
|
+
if (!isRecord(current))
|
|
337
|
+
return current;
|
|
338
|
+
return Object.fromEntries(Object.values(current).map((item, position) => [`${REDACTED} ${position + 1}`, item]));
|
|
339
|
+
}
|
|
340
|
+
if (!isRecord(current) || !Object.hasOwn(current, segment))
|
|
341
|
+
return current;
|
|
342
|
+
return { ...current, [segment]: step(current[segment], index + 1) };
|
|
343
|
+
};
|
|
344
|
+
return step(body, 0);
|
|
345
|
+
}
|
|
346
|
+
/** A reviewed path as the key trail the heuristic walks: list markers dropped, `*` and `{}` match any key. */
|
|
347
|
+
function trailOf(path) {
|
|
348
|
+
return parsePath(path).segments.filter((segment) => !segment.startsWith("[") && segment !== "@keys");
|
|
349
|
+
}
|
|
350
|
+
function matches(paths, trail) {
|
|
351
|
+
return paths.some((path) => path.length === trail.length && path.every((part, index) => part === "{}" || part === trail[index]));
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The contents of an environment container: every variable loses its value
|
|
355
|
+
* fields whether or not it has a `key` or `type`, a `NAME → value` map (no
|
|
356
|
+
* `key` or `id`, only strings) loses every value, and `NAME=value` strings go.
|
|
357
|
+
*/
|
|
358
|
+
function scrubEnv(value, depth) {
|
|
359
|
+
if (depth > 32)
|
|
360
|
+
return REDACTED;
|
|
361
|
+
if (Array.isArray(value)) {
|
|
362
|
+
return value.map((item) => (typeof item === "string" && item.includes("=") ? REDACTED : scrubEnv(item, depth + 1)));
|
|
363
|
+
}
|
|
364
|
+
if (!isRecord(value))
|
|
365
|
+
return value;
|
|
366
|
+
const entries = Object.entries(value);
|
|
367
|
+
const map = !("key" in value) &&
|
|
368
|
+
!("id" in value) &&
|
|
369
|
+
!ENV_VALUE_FIELDS.some((field) => field in value) &&
|
|
370
|
+
entries.length > 0 &&
|
|
371
|
+
entries.every(([, item]) => typeof item === "string");
|
|
372
|
+
if (map)
|
|
373
|
+
return Object.fromEntries(entries.map(([key]) => [key, REDACTED]));
|
|
374
|
+
const out = {};
|
|
375
|
+
for (const [key, item] of entries) {
|
|
376
|
+
// A value is replaced, so the caller can see one exists without reading it.
|
|
377
|
+
if (ENV_VALUE_FIELDS.includes(key)) {
|
|
378
|
+
out[key] = withheld(item);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
out[key] = scrubEnv(item, depth + 1);
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
/** An operation body that is environment variables: values go, nothing else changes. */
|
|
386
|
+
function scrubValues(value, depth) {
|
|
387
|
+
if (depth > 32)
|
|
388
|
+
return REDACTED;
|
|
389
|
+
if (Array.isArray(value))
|
|
390
|
+
return value.map((item) => scrubValues(item, depth + 1));
|
|
391
|
+
if (!isRecord(value))
|
|
392
|
+
return value;
|
|
393
|
+
const out = {};
|
|
394
|
+
for (const [key, item] of Object.entries(value)) {
|
|
395
|
+
if (!ENV_VALUE_FIELDS.includes(key))
|
|
396
|
+
out[key] = scrubValues(item, depth + 1);
|
|
397
|
+
}
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Apply resource rules to every discriminated object in a body, innermost
|
|
402
|
+
* first, and to the partial copies events carry beside one. Records the
|
|
403
|
+
* reviewed payer-facing URL fields of objects that keep them.
|
|
404
|
+
*/
|
|
405
|
+
function resourcePass(value, resources, verbatim, depth) {
|
|
406
|
+
if (depth > MAX_DEPTH)
|
|
407
|
+
return value;
|
|
408
|
+
if (Array.isArray(value))
|
|
409
|
+
return value.map((item) => resourcePass(item, resources, verbatim, depth + 1));
|
|
410
|
+
if (!isRecord(value))
|
|
411
|
+
return value;
|
|
412
|
+
let out = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resourcePass(item, resources, verbatim, depth + 1)]));
|
|
413
|
+
const ruleFor = (type) => typeof type === "string" && Object.hasOwn(resources.rules, type) ? resources.rules[type] : undefined;
|
|
414
|
+
// An event's `previous_attributes` is a partial copy of `data.object`: every reviewed path applies to it.
|
|
415
|
+
const inner = out[resources.key];
|
|
416
|
+
const innerRule = isRecord(inner) ? ruleFor(inner[resources.key]) : undefined;
|
|
417
|
+
if (innerRule) {
|
|
418
|
+
for (const partial of resources.partials ?? []) {
|
|
419
|
+
let copy = out[partial];
|
|
420
|
+
if (!isRecord(copy))
|
|
421
|
+
continue;
|
|
422
|
+
for (const path of [...(innerRule.paths ?? []), ...(innerRule.withheld ?? [])])
|
|
423
|
+
copy = applyPath(copy, path);
|
|
424
|
+
out = { ...out, [partial]: copy };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const rule = ruleFor(out[resources.key]);
|
|
428
|
+
if (!rule)
|
|
429
|
+
return out;
|
|
430
|
+
for (const path of rule.paths ?? [])
|
|
431
|
+
out = applyPath(out, path);
|
|
432
|
+
if (rule.guest && !guestOnly(out, rule.guest)) {
|
|
433
|
+
for (const path of rule.withheld ?? [])
|
|
434
|
+
out = applyPath(out, path);
|
|
435
|
+
}
|
|
436
|
+
else if (rule.verbatim?.length) {
|
|
437
|
+
verbatim.set(out, new Set(rule.verbatim));
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The defense-in-depth pass over any body: credential-named subtrees,
|
|
443
|
+
* labelled and typed secret records, environment containers, deploy hook
|
|
444
|
+
* URLs, destination headers, header/cookie/query rule values, route headers,
|
|
445
|
+
* external route destinations reduced to their origin, and other URLs
|
|
446
|
+
* sanitized. Reviewed `keep` paths and fields are exempt from the name rule
|
|
447
|
+
* only; a resource rule's reviewed payer-facing URLs come back verbatim.
|
|
448
|
+
*/
|
|
449
|
+
function heuristic(value, exempt) {
|
|
450
|
+
const walk = (current, trail, parent, depth) => {
|
|
451
|
+
if (depth > MAX_DEPTH)
|
|
452
|
+
return REDACTED;
|
|
453
|
+
if (typeof current === "string")
|
|
454
|
+
return URL_SHAPE.test(current) ? sanitizeUrl(current) : current;
|
|
455
|
+
if (Array.isArray(current))
|
|
456
|
+
return current.map((item) => walk(item, trail, parent, depth + 1));
|
|
457
|
+
if (!isRecord(current))
|
|
458
|
+
return current;
|
|
459
|
+
const labelled = credentialLabelled(current) || (typeof current["type"] === "string" && /secret/i.test(current["type"]));
|
|
460
|
+
const destination = parent === "delivery" || "endpoint" in current || "deliveryFormat" in current;
|
|
461
|
+
const out = {};
|
|
462
|
+
const verbatim = exempt.verbatim.get(current);
|
|
463
|
+
for (const [key, item] of Object.entries(current)) {
|
|
464
|
+
const path = [...trail, key];
|
|
465
|
+
const normalized = normalizedName(key);
|
|
466
|
+
const review = Object.hasOwn(exempt.fields, key) ? exempt.fields[key] : undefined;
|
|
467
|
+
if (review?.verdict === "redact") {
|
|
468
|
+
out[key] = withheld(item);
|
|
469
|
+
}
|
|
470
|
+
else if (labelled && LABELLED_VALUE_FIELDS.includes(key)) {
|
|
471
|
+
out[key] = withheld(item);
|
|
472
|
+
}
|
|
473
|
+
else if (exempt.scopeMaps.includes(parent) &&
|
|
474
|
+
Array.isArray(item) &&
|
|
475
|
+
item.every((entry) => typeof entry === "string")) {
|
|
476
|
+
// Permission scopes: action names under a resource name, never secrets.
|
|
477
|
+
out[key] = item;
|
|
478
|
+
}
|
|
479
|
+
else if (review?.verdict !== "keep" && !matches(exempt.keep, path) && containsCredential(normalized)) {
|
|
480
|
+
// Both former rules apply: a value either would have removed is removed.
|
|
481
|
+
out[key] = withheld(item);
|
|
482
|
+
}
|
|
483
|
+
else if (review?.verdict !== "keep" && !matches(exempt.keep, path) && CREDENTIAL_SUFFIX.test(normalized)) {
|
|
484
|
+
out[key] = scrubStrings(item);
|
|
485
|
+
}
|
|
486
|
+
else if (ENV_CONTAINERS.includes(normalized)) {
|
|
487
|
+
// Values go structurally; the rest of the container is still walked.
|
|
488
|
+
out[key] = walk(scrubEnv(item, depth + 1), path, normalized, depth + 1);
|
|
489
|
+
}
|
|
490
|
+
else if (parent === "deployhooks" && key === "url") {
|
|
491
|
+
out[key] = withheld(item);
|
|
492
|
+
}
|
|
493
|
+
else if (key === "headers" && destination && isRecord(item)) {
|
|
494
|
+
out[key] = Object.fromEntries(Object.keys(item).map((header) => [header, REDACTED]));
|
|
495
|
+
}
|
|
496
|
+
else if (HEADER_RULE_VALUES.includes(key) && headerLike(current)) {
|
|
497
|
+
// Any header, cookie, or query rule matches or sets this value,
|
|
498
|
+
// wherever the rule sits and whatever its key's form.
|
|
499
|
+
out[key] = withheld(item);
|
|
500
|
+
}
|
|
501
|
+
else if (key === "headers" && routeLike(current)) {
|
|
502
|
+
out[key] = headerValues(item);
|
|
503
|
+
}
|
|
504
|
+
else if (DESTINATION_KEYS.includes(key) && typeof item === "string" && URL_SHAPE.test(item)) {
|
|
505
|
+
// An external destination can carry its secret in the path; relative paths stay.
|
|
506
|
+
out[key] = originOnly(item);
|
|
507
|
+
}
|
|
508
|
+
else if (typeof item === "string" && verbatim?.has(key)) {
|
|
509
|
+
out[key] = item;
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
out[key] = walk(item, path, normalized, depth + 1);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return out;
|
|
516
|
+
};
|
|
517
|
+
return walk(value, [], "", 0);
|
|
518
|
+
}
|
|
519
|
+
/** Whether a request field submits a credential or a stored secret. */
|
|
520
|
+
function credentialInput(name) {
|
|
521
|
+
return credentialName(name) || INPUT_CONTAINERS.test(normalizedName(name));
|
|
522
|
+
}
|
|
523
|
+
/** Whether a request-side schema names a credential input, to a fixed depth. */
|
|
524
|
+
function acceptsCredential(schema, depth) {
|
|
525
|
+
if (depth > 4 || typeof schema !== "object" || schema === null)
|
|
526
|
+
return false;
|
|
527
|
+
const node = schema;
|
|
528
|
+
return (Object.entries(node.properties ?? {}).some(([name, child]) => credentialInput(name) || acceptsCredential(child, depth + 1)) ||
|
|
529
|
+
acceptsCredential(node.items, depth + 1) ||
|
|
530
|
+
(node.anyOf ?? []).some((branch) => acceptsCredential(branch, depth + 1)));
|
|
531
|
+
}
|
|
532
|
+
/** A vendor's value-safety mechanism over its reviewed table and pinned index. */
|
|
533
|
+
export function valueSafety(table, index) {
|
|
534
|
+
const fields = table.fields ?? {};
|
|
535
|
+
const scopeMaps = table.scopeMaps ?? [];
|
|
536
|
+
const inputs = new Map();
|
|
537
|
+
const verdict = (method, template) => {
|
|
538
|
+
const key = `${method} ${template}`;
|
|
539
|
+
return Object.hasOwn(table.operations, key) ? table.operations[key] : undefined;
|
|
540
|
+
};
|
|
541
|
+
return {
|
|
542
|
+
table,
|
|
543
|
+
verdict,
|
|
544
|
+
refusal(method, template) {
|
|
545
|
+
const reviewed = verdict(method, template);
|
|
546
|
+
return reviewed?.verdict === "refuse" ? `Connecta refuses ${method} ${template}. ${reviewed.reason}` : undefined;
|
|
547
|
+
},
|
|
548
|
+
redact(data, method, template) {
|
|
549
|
+
const reviewed = verdict(method, template);
|
|
550
|
+
let out = data;
|
|
551
|
+
if (reviewed?.verdict === "redact")
|
|
552
|
+
for (const path of reviewed.paths)
|
|
553
|
+
out = applyPath(out, path);
|
|
554
|
+
if (table.envBodies?.test(template))
|
|
555
|
+
out = scrubValues(out, 0);
|
|
556
|
+
const verbatim = new WeakMap();
|
|
557
|
+
if (table.resources)
|
|
558
|
+
out = resourcePass(out, table.resources, verbatim, 0);
|
|
559
|
+
const keep = reviewed && reviewed.verdict !== "refuse" ? (reviewed.keep ?? []) : [];
|
|
560
|
+
return heuristic(out, { keep: keep.map(trailOf), verbatim, fields, scopeMaps });
|
|
561
|
+
},
|
|
562
|
+
withholdsErrors(op) {
|
|
563
|
+
const reviewed = verdict(op.method, op.path);
|
|
564
|
+
if (reviewed !== undefined)
|
|
565
|
+
return reviewed.verdict === "refuse" || reviewed.errors !== "vendor";
|
|
566
|
+
if (op.row < 0)
|
|
567
|
+
return false;
|
|
568
|
+
let known = inputs.get(op.row);
|
|
569
|
+
if (known === undefined) {
|
|
570
|
+
const contract = index().contract(op);
|
|
571
|
+
known =
|
|
572
|
+
contract.parameters.some((parameter) => parameter.in === "query" && credentialInput(parameter.name)) ||
|
|
573
|
+
(contract.body !== undefined &&
|
|
574
|
+
(typeof contract.body.schema !== "object" || acceptsCredential(contract.body.schema, 0)));
|
|
575
|
+
inputs.set(op.row, known);
|
|
576
|
+
}
|
|
577
|
+
return known;
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": ".breezechms.com (Breeze ChMS, now Tithely Church Management): people, tags, events, attendance, forms, volunteers, and giving.\n\nChurch purpose: \n\n## Sensitive data: reduce in code\n\nPeople records carry home addresses, phones, birthdates, and family ties, and\ngiving records reveal who gives and how much — religious affiliation by proxy.\nRead them inside `execute_code` and return only the counts, totals, or the\nfew fields the task needs. Never echo whole profiles or donor lists.\n\n## Ids and profile fields\n\n- Ids are numeric strings. Read them from a list tool; never guess one.\n- Events have two ids: an instance id (`list_events` `id`) for one\n occurrence, and a series `eventId`. Attendance, volunteers, and\n check-in take the instance id.\n- Person `details` are keyed by profile `fieldId`, not by name. Call\n `list_profile_fields` once and map ids to names in code. Multiple-choice\n values and `list_people` filters use option ids from the same call;\n join several with `-`.\n- `list_people` has no name search. List without details and match names\n in code, then `get_person` the hits.\n- Projections rename Breeze's snake_case fields to camelCase and drop photo\n paths and internal columns; `raw: true` returns Breeze's rows.\n\n## Pagination\n\n`list_people` pages by `cursor`: continue while `page.hasMore`. Breeze\nreports no total, so a full page sets `hasMore` and the next may be empty.\n`list_events` and `list_account_log` do not page: `truncated: true`\nmeans narrow the date range. Other lists return everything.\n\n## Giving\n\n`list_contributions` and `list_funds` call endpoints Breeze removed from\nits public reference in 2023 but still serves. They may change or vanish\nwithout notice; a failure there is not your argument's fault. Amounts are\ndecimal strings; `totalAmount` is an exact sum. Online gifts processed by\nTithe.ly arrive through its sync, batched per deposit, so the latest days\nmay be missing. Read the currency from `get_account_summary`.\n\n## Writes and the hatches\n\n- Every write, `add_person`, `assign_tag`, and `record_check_in`\n included, follows the routing in the `usage` skill. Fetch it with\n `skills({ name: \"usage\" })` for guest API and
|
|
3
|
+
"content": ".breezechms.com (Breeze ChMS, now Tithely Church Management): people, tags, events, attendance, forms, volunteers, and giving.\n\nChurch purpose: \n\n## Sensitive data: reduce in code\n\nPeople records carry home addresses, phones, birthdates, and family ties, and\ngiving records reveal who gives and how much — religious affiliation by proxy.\nRead them inside `execute_code` and return only the counts, totals, or the\nfew fields the task needs. Never echo whole profiles or donor lists.\n\n## Ids and profile fields\n\n- Ids are numeric strings. Read them from a list tool; never guess one.\n- Events have two ids: an instance id (`list_events` `id`) for one\n occurrence, and a series `eventId`. Attendance, volunteers, and\n check-in take the instance id.\n- Person `details` are keyed by profile `fieldId`, not by name. Call\n `list_profile_fields` once and map ids to names in code. Multiple-choice\n values and `list_people` filters use option ids from the same call;\n join several with `-`.\n- `list_people` has no name search. List without details and match names\n in code, then `get_person` the hits.\n- Projections rename Breeze's snake_case fields to camelCase and drop photo\n paths and internal columns; `raw: true` returns Breeze's rows.\n\n## Pagination\n\n`list_people` pages by `cursor`: continue while `page.hasMore`. Breeze\nreports no total, so a full page sets `hasMore` and the next may be empty.\n`list_events` and `list_account_log` do not page: `truncated: true`\nmeans narrow the date range. Other lists return everything.\n\n## Giving\n\n`list_contributions` and `list_funds` call endpoints Breeze removed from\nits public reference in 2023 but still serves. They may change or vanish\nwithout notice; a failure there is not your argument's fault. Amounts are\ndecimal strings; `totalAmount` is an exact sum. Online gifts processed by\nTithe.ly arrive through its sync, batched per deposit, so the latest days\nmay be missing. Read the currency from `get_account_summary`.\n\n## Writes and the hatches\n\n- Every write, `add_person`, `assign_tag`, and `record_check_in`\n included, follows the routing in the `usage` skill. Fetch it with\n `skills({ name: \"usage\" })` for guest API and write rules.\n Those three only add, and `add_person` never deduplicates.\n- `update_person` field values: text, date (M/D/YYYY), or option id in\n `response`; email, phone, address, and family_role values in `details`.\n- Every Breeze call is a GET, writes included, so the hatches split by\n endpoint. `breeze_api_get` admits only reviewed read endpoints and their\n documented parameters, such as `/tags/list_folders`,\n `/events/list_event`, `/events/attendance/eligible`, `/giving/view`,\n and `/pledges/list_campaigns`; it refuses anything else. Everything else\n — event, family, volunteer, and form writes, giving writes, check-out,\n undocumented parameters — is `breeze_api_mutate`. Paths are below\n `/api`; `*_json` values may be passed as objects.\n- `list_account_log` actions include person_created, person_updated,\n person_deleted, tag_assign, tag_unassign, event_created,\n attendance_deleted, form_entry_updated, contribution_added,\n contribution_updated, contribution_deleted, and batch_updated.\n\n## Rate limits\n\nBreeze publishes no limit and sends no rate headers; a third party reports\nabout 20 requests per minute. Keep loops sequential and prefer one list call\nover many single reads.\n",
|
|
4
4
|
"instructionsHeading": "Church instructions",
|
|
5
5
|
"fragments": {
|
|
6
6
|
"guide_0": ".breezechms.com (Breeze ChMS, now Tithely Church Management): people, tags, events, attendance, forms, volunteers, and giving.\n\nChurch purpose: ",
|
|
7
|
-
"guide_1": "\n\n## Sensitive data: reduce in code\n\nPeople records carry home addresses, phones, birthdates, and family ties, and\ngiving records reveal who gives and how much — religious affiliation by proxy.\nRead them inside `execute_code` and return only the counts, totals, or the\nfew fields the task needs. Never echo whole profiles or donor lists.\n\n## Ids and profile fields\n\n- Ids are numeric strings. Read them from a list tool; never guess one.\n- Events have two ids: an instance id (`list_events` `id`) for one\n occurrence, and a series `eventId`. Attendance, volunteers, and\n check-in take the instance id.\n- Person `details` are keyed by profile `fieldId`, not by name. Call\n `list_profile_fields` once and map ids to names in code. Multiple-choice\n values and `list_people` filters use option ids from the same call;\n join several with `-`.\n- `list_people` has no name search. List without details and match names\n in code, then `get_person` the hits.\n- Projections rename Breeze's snake_case fields to camelCase and drop photo\n paths and internal columns; `raw: true` returns Breeze's rows.\n\n## Pagination\n\n`list_people` pages by `cursor`: continue while `page.hasMore`. Breeze\nreports no total, so a full page sets `hasMore` and the next may be empty.\n`list_events` and `list_account_log` do not page: `truncated: true`\nmeans narrow the date range. Other lists return everything.\n\n## Giving\n\n`list_contributions` and `list_funds` call endpoints Breeze removed from\nits public reference in 2023 but still serves. They may change or vanish\nwithout notice; a failure there is not your argument's fault. Amounts are\ndecimal strings; `totalAmount` is an exact sum. Online gifts processed by\nTithe.ly arrive through its sync, batched per deposit, so the latest days\nmay be missing. Read the currency from `get_account_summary`.\n\n## Writes and the hatches\n\n- Every write, `add_person`, `assign_tag`, and `record_check_in`\n included, follows the routing in the `usage` skill. Fetch it with\n `skills({ name: \"usage\" })` for guest API and
|
|
7
|
+
"guide_1": "\n\n## Sensitive data: reduce in code\n\nPeople records carry home addresses, phones, birthdates, and family ties, and\ngiving records reveal who gives and how much — religious affiliation by proxy.\nRead them inside `execute_code` and return only the counts, totals, or the\nfew fields the task needs. Never echo whole profiles or donor lists.\n\n## Ids and profile fields\n\n- Ids are numeric strings. Read them from a list tool; never guess one.\n- Events have two ids: an instance id (`list_events` `id`) for one\n occurrence, and a series `eventId`. Attendance, volunteers, and\n check-in take the instance id.\n- Person `details` are keyed by profile `fieldId`, not by name. Call\n `list_profile_fields` once and map ids to names in code. Multiple-choice\n values and `list_people` filters use option ids from the same call;\n join several with `-`.\n- `list_people` has no name search. List without details and match names\n in code, then `get_person` the hits.\n- Projections rename Breeze's snake_case fields to camelCase and drop photo\n paths and internal columns; `raw: true` returns Breeze's rows.\n\n## Pagination\n\n`list_people` pages by `cursor`: continue while `page.hasMore`. Breeze\nreports no total, so a full page sets `hasMore` and the next may be empty.\n`list_events` and `list_account_log` do not page: `truncated: true`\nmeans narrow the date range. Other lists return everything.\n\n## Giving\n\n`list_contributions` and `list_funds` call endpoints Breeze removed from\nits public reference in 2023 but still serves. They may change or vanish\nwithout notice; a failure there is not your argument's fault. Amounts are\ndecimal strings; `totalAmount` is an exact sum. Online gifts processed by\nTithe.ly arrive through its sync, batched per deposit, so the latest days\nmay be missing. Read the currency from `get_account_summary`.\n\n## Writes and the hatches\n\n- Every write, `add_person`, `assign_tag`, and `record_check_in`\n included, follows the routing in the `usage` skill. Fetch it with\n `skills({ name: \"usage\" })` for guest API and write rules.\n Those three only add, and `add_person` never deduplicates.\n- `update_person` field values: text, date (M/D/YYYY), or option id in\n `response`; email, phone, address, and family_role values in `details`.\n- Every Breeze call is a GET, writes included, so the hatches split by\n endpoint. `breeze_api_get` admits only reviewed read endpoints and their\n documented parameters, such as `/tags/list_folders`,\n `/events/list_event`, `/events/attendance/eligible`, `/giving/view`,\n and `/pledges/list_campaigns`; it refuses anything else. Everything else\n — event, family, volunteer, and form writes, giving writes, check-out,\n undocumented parameters — is `breeze_api_mutate`. Paths are below\n `/api`; `*_json` values may be passed as objects.\n- `list_account_log` actions include person_created, person_updated,\n person_deleted, tag_assign, tag_unassign, event_created,\n attendance_deleted, form_entry_updated, contribution_added,\n contribution_updated, contribution_deleted, and batch_updated.\n\n## Rate limits\n\nBreeze publishes no limit and sends no rate headers; a third party reports\nabout 20 requests per minute. Keep loops sequential and prefer one list call\nover many single reads.\n"
|
|
8
8
|
}
|
|
9
9
|
};
|