@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
package/dist/guest-runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GUEST_FUNCTIONS } from "./guest-functions.js";
|
|
1
2
|
/** Capture the single-use runner before evaluating the guest expression. */
|
|
2
3
|
export function wrapGuestProgram(code) {
|
|
3
4
|
return `async () => globalThis.__connecta_run()(async (connecta) => await (\n${code}\n)())`;
|
|
@@ -62,6 +63,34 @@ export function guestPromiseInitializer() {
|
|
|
62
63
|
Object.freeze(NativePromise.prototype);
|
|
63
64
|
})()`;
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The frozen namespace, joined from the registry. A `wrapped` or `emission`
|
|
68
|
+
* function uses the initializer's local of the same name; an alias shares one
|
|
69
|
+
* function object. Only guest source enters this string.
|
|
70
|
+
*/
|
|
71
|
+
const GUEST_NAMESPACE = (() => {
|
|
72
|
+
const before = [];
|
|
73
|
+
const entries = ["__proto__: null"];
|
|
74
|
+
for (const fn of GUEST_FUNCTIONS) {
|
|
75
|
+
if (fn.budget === "once")
|
|
76
|
+
continue;
|
|
77
|
+
if (fn.budget === "emission") {
|
|
78
|
+
entries.push(`${fn.name}(block) {
|
|
79
|
+
const ref = block && typeof block === "object" ? weakGet(nativeRefs, block) : undefined;
|
|
80
|
+
return trackEmission(${fn.name}(ref || block));
|
|
81
|
+
}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const value = `hosted(${"wrapped" in fn ? fn.name : `provider.${fn.name}`})`;
|
|
85
|
+
if (!("alias" in fn)) {
|
|
86
|
+
entries.push(`${fn.name}: ${value}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
before.push(` const ${fn.name} = ${value};\n`);
|
|
90
|
+
entries.push(fn.name, `// Undeclared alias: ${fn.alias.note}\n ${fn.alias.name}: ${fn.name}`);
|
|
91
|
+
}
|
|
92
|
+
return `${before.join("")} const namespace = freeze({\n ${entries.join(",\n ")}\n });`;
|
|
93
|
+
})();
|
|
65
94
|
/** Capture a private initializer before a Worker evaluates its guest module. */
|
|
66
95
|
export function guestInitializer() {
|
|
67
96
|
return `(() => {
|
|
@@ -152,19 +181,17 @@ export function guestInitializer() {
|
|
|
152
181
|
}
|
|
153
182
|
});
|
|
154
183
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
});
|
|
184
|
+
// Result fields read off an unawaited host call flag the run, still undefined.
|
|
185
|
+
let unawaited = false;
|
|
186
|
+
const fields = {};
|
|
187
|
+
for (const key of ["data", "format", "content", "tools", "catalogErrors", "total", "text", "resultId", "hasMore", "nextOffset"])
|
|
188
|
+
fields[key] = { get() { unawaited = true; } };
|
|
189
|
+
const hosted = (fn) => function () {
|
|
190
|
+
const promise = apply(fn, undefined, arguments);
|
|
191
|
+
if (promise && typeof promise === "object") defineProperties(promise, fields);
|
|
192
|
+
return promise;
|
|
193
|
+
};
|
|
194
|
+
${GUEST_NAMESPACE}
|
|
168
195
|
globalThis.connecta = namespace;
|
|
169
196
|
defineProperties(globalThis, {
|
|
170
197
|
Error: { value: NativeError, writable: false, configurable: false },
|
|
@@ -174,7 +201,13 @@ export function guestInitializer() {
|
|
|
174
201
|
return async (program) => {
|
|
175
202
|
if (used) throw new NativeError("Guest runner initialization is single-use.");
|
|
176
203
|
used = true;
|
|
177
|
-
|
|
204
|
+
let result;
|
|
205
|
+
try {
|
|
206
|
+
result = await program(namespace);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (unawaited) try { await provider.unawaited(); } catch {}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
178
211
|
for (let i = 0; i < pending.length; i++) {
|
|
179
212
|
const report = await pending[i].settled;
|
|
180
213
|
if (!report.ok && !pending[i].handled) throw report.error;
|
|
@@ -198,8 +231,15 @@ const ERROR_NAMES = new Set([
|
|
|
198
231
|
]);
|
|
199
232
|
/** Fixed repair guidance, used only in agent-facing results. */
|
|
200
233
|
export function programError(raw, code) {
|
|
201
|
-
const
|
|
202
|
-
|
|
234
|
+
const text = typeof raw.message === "string" ? raw.message : "Program failed.";
|
|
235
|
+
// Executors may report "TypeError: …" as the message itself; name it once.
|
|
236
|
+
const prefix = /^([A-Za-z]*Error): /.exec(text)?.[1];
|
|
237
|
+
const name = typeof raw.name === "string" && ERROR_NAMES.has(raw.name)
|
|
238
|
+
? raw.name
|
|
239
|
+
: prefix && ERROR_NAMES.has(prefix)
|
|
240
|
+
? prefix
|
|
241
|
+
: "Error";
|
|
242
|
+
const message = prefix === name ? text.slice(name.length + 2) : text;
|
|
203
243
|
const stack = typeof raw.stack === "string" ? raw.stack : "";
|
|
204
244
|
// Compare QuickJS/V8 locations to the wrapper baseline after the source.
|
|
205
245
|
const location = /:(\d+)(?::\d+)?\)?(?:\n|$)/.exec(stack);
|
|
@@ -215,7 +255,10 @@ export function programError(raw, code) {
|
|
|
215
255
|
? Math.max(1, Number(location[1]) - Number(baseline[1]) + code.split("\n").length + 1)
|
|
216
256
|
: null;
|
|
217
257
|
let hint = "Use plain JavaScript in one async () => { ... } expression and the connecta global.";
|
|
218
|
-
if (
|
|
258
|
+
if (raw.unawaited === true) {
|
|
259
|
+
hint = "connecta.* returns a Promise; await it: (await connecta.call(address, args)).data.";
|
|
260
|
+
}
|
|
261
|
+
else if (/\brequire\b|\b[Ii]mports?\b|\bfs\b|filesystem|node:/.test(message) ||
|
|
219
262
|
(name === "SyntaxError" && /\bimport\b/.test(code))) {
|
|
220
263
|
hint =
|
|
221
264
|
"Imports, require, and filesystem access are outside the guest API. Use connecta.call to access configured services.";
|
|
@@ -224,10 +267,10 @@ export function programError(raw, code) {
|
|
|
224
267
|
/(?:const|let|var|function|\()\s*connecta\b/.test(code)) {
|
|
225
268
|
hint = "Do not shadow connecta. Use the host-provided connecta global.";
|
|
226
269
|
}
|
|
227
|
-
else if (/callTool|\bskills\b|connecta\.
|
|
228
|
-
(["TypeError", "ReferenceError"].includes(name) && /connecta\.
|
|
270
|
+
else if (/callTool|\bskills\b|connecta\.guide/.test(message) ||
|
|
271
|
+
(["TypeError", "ReferenceError"].includes(name) && /connecta\.guide\b/.test(code))) {
|
|
229
272
|
hint =
|
|
230
|
-
|
|
273
|
+
'Use connecta.call(address, args), connecta.search, connecta.describe, connecta.result, and connecta.skill("usage"). Services are addressed by connector.tool, never guest globals.';
|
|
231
274
|
}
|
|
232
275
|
return {
|
|
233
276
|
code: "program_error",
|
package/dist/guest-types.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ export interface GuestApi {
|
|
|
25
25
|
text: string;
|
|
26
26
|
format: "text";
|
|
27
27
|
}>;
|
|
28
|
-
|
|
28
|
+
/** Forwards an authored block, a block from result.content, or an inline result; a paged handle is refused. */
|
|
29
|
+
emit(block: GuestNativeBlock | Exclude<GuestResult, GuestResultHandle>): PromiseLike<void>;
|
|
29
30
|
}
|
|
30
31
|
export interface GuestResourceResult {
|
|
31
32
|
contents: Array<{
|
|
@@ -103,7 +104,7 @@ export type GuestBlock = {
|
|
|
103
104
|
mimeType: string;
|
|
104
105
|
};
|
|
105
106
|
type SchemaFormat = "json" | "compact" | "typescript";
|
|
106
|
-
|
|
107
|
+
interface CatalogSearchArgs {
|
|
107
108
|
query?: string;
|
|
108
109
|
connector?: string;
|
|
109
110
|
safety?: "readOnly" | "approvalRequired" | "all";
|
|
@@ -113,9 +114,9 @@ export interface CatalogSearchArgs {
|
|
|
113
114
|
includeSchemas?: SchemaFormat;
|
|
114
115
|
includeSchemaKeys?: boolean;
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
-
address?:
|
|
118
|
-
addresses?:
|
|
117
|
+
interface CatalogDescribeArgs {
|
|
118
|
+
address?: string;
|
|
119
|
+
addresses?: string[];
|
|
119
120
|
format?: SchemaFormat;
|
|
120
121
|
fullDescriptions?: boolean;
|
|
121
122
|
}
|
|
@@ -140,7 +141,7 @@ interface CatalogSearchFailure extends CatalogFailureDetail {
|
|
|
140
141
|
}>;
|
|
141
142
|
retry?: string;
|
|
142
143
|
}
|
|
143
|
-
|
|
144
|
+
interface CatalogSearchPage {
|
|
144
145
|
catalogErrors: CatalogSearchFailure[];
|
|
145
146
|
absence?: {
|
|
146
147
|
service: string;
|
|
@@ -170,14 +171,27 @@ export interface CatalogSearchPage {
|
|
|
170
171
|
guidance?: string;
|
|
171
172
|
};
|
|
172
173
|
}
|
|
173
|
-
|
|
174
|
-
tools:
|
|
175
|
-
|
|
174
|
+
type CatalogSearchResult = CatalogSearchPage & {
|
|
175
|
+
tools: CatalogSearchTool[];
|
|
176
|
+
/** Complete operation reads: pass args unchanged to connecta.call(address, args). */
|
|
177
|
+
readyCalls?: Array<{
|
|
178
|
+
address: string;
|
|
179
|
+
args: {
|
|
180
|
+
path: string;
|
|
181
|
+
};
|
|
182
|
+
classification: "read";
|
|
183
|
+
summary: string;
|
|
184
|
+
guideRequired?: true;
|
|
185
|
+
guideRequiredReasons?: GuideRequiredReason[];
|
|
186
|
+
}>;
|
|
187
|
+
/** Facts shared by a connector's rows, keyed by the id before the address's first dot. */
|
|
188
|
+
connectors?: Record<string, {
|
|
189
|
+
title?: string;
|
|
176
190
|
guide?: string;
|
|
177
191
|
guideSummary?: string;
|
|
178
192
|
}>;
|
|
179
193
|
};
|
|
180
|
-
|
|
194
|
+
interface CatalogDescription {
|
|
181
195
|
address: string;
|
|
182
196
|
schemaFormat?: "json" | "text";
|
|
183
197
|
name?: string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Internal provider kit (decision 0011 S5). Unreachable from src/index.ts.
|
|
2
|
+
//
|
|
3
|
+
// A provider's own argument checks record every problem before refusing, so
|
|
4
|
+
// one invalid_args carries every finding and an agent repairs the call in one
|
|
5
|
+
// turn instead of one constraint per turn.
|
|
6
|
+
import { ConnectorCallError, issueSummary } from "../errors.js";
|
|
7
|
+
import { schemaExample } from "../validate.js";
|
|
8
|
+
/** Every argument problem one call has, then a single refusal. */
|
|
9
|
+
export class ArgumentProblems {
|
|
10
|
+
#issues = [];
|
|
11
|
+
/**
|
|
12
|
+
* Record one problem. A refusal holds only what the published schema
|
|
13
|
+
* declares and fixed text: `path` names declared properties and indices
|
|
14
|
+
* (`<key>` for any other key), `expected` states the allowed shape or bound,
|
|
15
|
+
* and `received` is a JSON type. Never a submitted string or key.
|
|
16
|
+
*/
|
|
17
|
+
add(path, expected, received) {
|
|
18
|
+
this.#issues.push({ path, expected, ...(received === undefined ? {} : { received }) });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The one refusal for every recorded problem, or undefined when there is
|
|
22
|
+
* none. With the tool's schema it carries the schema's synthesized example;
|
|
23
|
+
* `withheld` states why a tool offers none.
|
|
24
|
+
*/
|
|
25
|
+
refusal(lead, schema, withheld) {
|
|
26
|
+
if (!this.#issues.length)
|
|
27
|
+
return undefined;
|
|
28
|
+
return new ConnectorCallError("invalid_args", `${lead}: ${issueSummary(this.#issues)}`, {
|
|
29
|
+
validation: {
|
|
30
|
+
issues: this.#issues,
|
|
31
|
+
...(withheld ? { exampleUnavailable: withheld } : schema ? schemaExample(schema) : {}),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/http/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { confinementBase, REDIRECT_STATUSES, BODILESS_STATUSES } from "../http-policy.js";
|
|
2
2
|
import { guardedTransport, sendGuarded } from "../guarded-transport.js";
|
|
3
|
-
// Guarded HTTP transport
|
|
3
|
+
// Guarded HTTP transport. Unreachable from src/index.ts.
|
|
4
4
|
//
|
|
5
5
|
// The guarded transport as an Effect `HttpClient`, with the behaviour of
|
|
6
6
|
// src/connectors/guarded-fetch.ts: requests confined beneath one base URL,
|
package/dist/http/errors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Internal HTTP failure contract
|
|
1
|
+
// Internal HTTP failure contract. Unreachable from src/index.ts.
|
|
2
2
|
//
|
|
3
3
|
// Every way a kit request can fail, as a tagged error, and the one mapping
|
|
4
4
|
// from those to the wire `ConnectorCallError`. A provider supplies meaning
|
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
// refusing status counts only for the request that received it. Advice to
|
|
27
27
|
// send a call again is a segment of its own (`resendAdvice`), never part of
|
|
28
28
|
// the message, so output composes it only where the outcome allows.
|
|
29
|
-
// Q8 policy is decided once by the core's `settleWrite
|
|
29
|
+
// Q8 policy is decided once by the core's `settleWrite`, from the
|
|
30
30
|
// ledger entries this kit forwards its matched proof and renewals to. A
|
|
31
31
|
// write answered 401 stays unknown; only renewal removes its handoff, and
|
|
32
32
|
// only a reviewed authentication-before-execution declaration proves refusal.
|
|
33
|
+
import { routesOf } from "../agent-routes.js";
|
|
33
34
|
import { Data } from "effect";
|
|
34
35
|
import { ConnectorCallError, isConnectorCallErrorCode, resendAdvice, unavailableCallError, } from "../errors.js";
|
|
35
36
|
import { attachFailureFacts } from "../operator-record.js";
|
|
@@ -429,7 +430,7 @@ function verdict(failure, vendor, options, dispatch = options.dispatches.find((e
|
|
|
429
430
|
body: undefined,
|
|
430
431
|
unreadable: true,
|
|
431
432
|
operation: dispatch.operation ?? { method: "", template: "" },
|
|
432
|
-
}), vendor, options.withholds?.(dispatch.operation ?? { method: "", template: "" }) === true, !dispatch.write);
|
|
433
|
+
}), vendor, options.withholds?.(dispatch.operation ?? { method: "", template: "" }) === true, !dispatch.write, routesOf(options));
|
|
433
434
|
return providerFailureMessage(mapped, failure.detail);
|
|
434
435
|
}
|
|
435
436
|
const uncertainWrite = options.classification === "write" &&
|
|
@@ -455,10 +456,10 @@ function verdict(failure, vendor, options, dispatch = options.dispatches.find((e
|
|
|
455
456
|
: unreadable, { retryable: false });
|
|
456
457
|
}
|
|
457
458
|
case "StatusFailure":
|
|
458
|
-
return statusVerdict(failure, vendor, options.withholds?.(failure.operation) === true, options.dispatches.some((dispatch) => dispatch.failure === failure && !dispatch.write));
|
|
459
|
+
return statusVerdict(failure, vendor, options.withholds?.(failure.operation) === true, options.dispatches.some((dispatch) => dispatch.failure === failure && !dispatch.write), routesOf(options));
|
|
459
460
|
}
|
|
460
461
|
}
|
|
461
|
-
function statusVerdict(failure, vendor, withheld, read) {
|
|
462
|
+
function statusVerdict(failure, vendor, withheld, read, routes) {
|
|
462
463
|
const input = classificationInput(failure, vendor, withheld);
|
|
463
464
|
const { headers, presence } = input;
|
|
464
465
|
// Classification first, over facts and headers only.
|
|
@@ -477,7 +478,7 @@ function statusVerdict(failure, vendor, withheld, read) {
|
|
|
477
478
|
withheld,
|
|
478
479
|
...(withheld ? {} : { body: failure.body }),
|
|
479
480
|
});
|
|
480
|
-
const error = new ConnectorCallError(code, words ? words(detail) : detail, {
|
|
481
|
+
const error = new ConnectorCallError(code, words ? words(detail, routes) : detail, {
|
|
481
482
|
...(retryable === undefined ? {} : { retryable }),
|
|
482
483
|
...(wait === undefined ? {} : { retryAfterMs: wait }),
|
|
483
484
|
...(current ? { current } : {}),
|
package/dist/http/fields.js
CHANGED
|
@@ -3,10 +3,29 @@
|
|
|
3
3
|
// `fields` narrows a result to the dot paths an agent names. It runs on the
|
|
4
4
|
// result a tool already built (after value safety, the privacy projection and
|
|
5
5
|
// any named projection), so it can only remove. A list narrows each item.
|
|
6
|
+
//
|
|
7
|
+
// Every `fields` argument states where its paths start, its anchor, in the
|
|
8
|
+
// schema description, derived here from the same contract that validates it:
|
|
9
|
+
// a named read's result, each item of a list's `items`, or a hatch's response
|
|
10
|
+
// body in `data`. A path is checked against the published output schema as
|
|
11
|
+
// deep as that schema is closed, so a path outside the declared contract is
|
|
12
|
+
// refused before anything is sent. A declared path the response happens to
|
|
13
|
+
// lack is never refused: absent optional values and empty lists are ordinary.
|
|
6
14
|
import { Schema } from "effect";
|
|
7
|
-
import {
|
|
8
|
-
import { isMaxChars, isMinChars } from "./json-schema.js";
|
|
15
|
+
import { ArgumentProblems } from "./argument-problems.js";
|
|
16
|
+
import { catalogSchema, isMaxChars, isMinChars } from "./json-schema.js";
|
|
9
17
|
const MAX_FIELDS = 50;
|
|
18
|
+
/** Choices listed in a refusal or a warning; more read as "a declared property". */
|
|
19
|
+
const MAX_CHOICES = 40;
|
|
20
|
+
const ANCHORS = {
|
|
21
|
+
result: "the result",
|
|
22
|
+
item: "each item in items",
|
|
23
|
+
data: "the response body in data, each item when the body is a list",
|
|
24
|
+
};
|
|
25
|
+
/** The description of a `fields` argument: its anchor, one valid example, then any tool note. */
|
|
26
|
+
export function fieldsDescription(anchor, example, note = "It only narrows the result.") {
|
|
27
|
+
return `Dot paths to keep. Paths start at ${ANCHORS[anchor]}, e.g. ${JSON.stringify([example])}. ${note}`;
|
|
28
|
+
}
|
|
10
29
|
/** The `fields` argument: at most 50 dot paths of 1 to 200 characters. */
|
|
11
30
|
export function fieldsInput(description) {
|
|
12
31
|
return Schema.Array(Schema.String.check(isMinChars(1), isMaxChars(200)))
|
|
@@ -16,18 +35,97 @@ export function fieldsInput(description) {
|
|
|
16
35
|
function record(value) {
|
|
17
36
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
37
|
}
|
|
38
|
+
/** A dot path's property names; `a[].b` and `a.b` both select `b` in each item of `a`. */
|
|
39
|
+
const segments = (path) => path.replace(/\[\](?=\.|$)/g, "").split(".");
|
|
40
|
+
/**
|
|
41
|
+
* Collect the properties a path segment may name at a schema position into
|
|
42
|
+
* `into`, false when the position is open (any key may appear: `{}`, a bare
|
|
43
|
+
* object or array, a record, a reference). Arrays are transparent, as
|
|
44
|
+
* selection narrows them item by item; a leaf adds nothing and stays closed.
|
|
45
|
+
* Below the first segment, JSON Schema's default applies: an object is closed
|
|
46
|
+
* only when it says `additionalProperties: false`. The first segment keeps the
|
|
47
|
+
* declared properties of the result or item as its closed set.
|
|
48
|
+
*/
|
|
49
|
+
function propertiesAt(node, into, root) {
|
|
50
|
+
if (!record(node))
|
|
51
|
+
return node === false;
|
|
52
|
+
const additional = node["additionalProperties"];
|
|
53
|
+
if ("$ref" in node || "allOf" in node || (additional !== undefined && additional !== false))
|
|
54
|
+
return false;
|
|
55
|
+
const branches = [node["anyOf"], node["oneOf"]].flatMap((value) => (Array.isArray(value) ? value : []));
|
|
56
|
+
if (!branches.every((branch) => propertiesAt(branch, into, root)))
|
|
57
|
+
return false;
|
|
58
|
+
if (node["items"] !== undefined && !propertiesAt(node["items"], into, root))
|
|
59
|
+
return false;
|
|
60
|
+
const properties = node["properties"];
|
|
61
|
+
if (record(properties)) {
|
|
62
|
+
for (const [name, child] of Object.entries(properties))
|
|
63
|
+
into.set(name, [...(into.get(name) ?? []), child]);
|
|
64
|
+
return root || additional === false;
|
|
65
|
+
}
|
|
66
|
+
if (branches.length || node["items"] !== undefined || "enum" in node || "const" in node)
|
|
67
|
+
return true;
|
|
68
|
+
const types = [node["type"]].flat();
|
|
69
|
+
return node["type"] !== undefined && !types.includes("object") && !types.includes("array");
|
|
70
|
+
}
|
|
71
|
+
const choices = (names) => names.length > 0 && names.length <= MAX_CHOICES ? `one of ${names.join(", ")}` : "a declared property";
|
|
72
|
+
/**
|
|
73
|
+
* Record every `fields` path the contract cannot carry. A JSON Schema is the
|
|
74
|
+
* published shape at the anchor (a result, or a list's item), checked as deep
|
|
75
|
+
* as it is closed; a set names only the first segments, which is all a hatch's
|
|
76
|
+
* pinned index can promise. A finding names the entry's index, its segment
|
|
77
|
+
* position and the declared properties there, never the submitted text.
|
|
78
|
+
*/
|
|
79
|
+
function fieldsIssues(problems, fields, contract, of) {
|
|
80
|
+
const root = contract instanceof Set
|
|
81
|
+
? { type: "object", properties: Object.fromEntries([...contract].map((n) => [n, {}])) }
|
|
82
|
+
: contract;
|
|
83
|
+
fields?.forEach((field, index) => {
|
|
84
|
+
let nodes = [root];
|
|
85
|
+
for (const [depth, name] of segments(field).entries()) {
|
|
86
|
+
const at = new Map();
|
|
87
|
+
// Selection keeps a scalar a path runs past, so below the first segment only a closed object refuses.
|
|
88
|
+
if (!nodes.every((node) => propertiesAt(node, at, depth === 0)) || (depth > 0 && at.size === 0))
|
|
89
|
+
return;
|
|
90
|
+
const next = at.get(name);
|
|
91
|
+
if (next) {
|
|
92
|
+
nodes = next;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const declared = [...at.keys()];
|
|
96
|
+
problems.add(`/fields/${index}`, depth === 0
|
|
97
|
+
? `a path beginning with a property of ${of}: ${choices(declared)}`
|
|
98
|
+
: `a declared path of ${of}; segment ${depth + 1} names a property declared at that level: ${choices(declared)}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** The single refusal naming every bad `fields` path, if any; `withheld` says why it has no example. */
|
|
104
|
+
export function fieldsProblem(fields, contract, of, withheld) {
|
|
105
|
+
const problems = new ArgumentProblems();
|
|
106
|
+
fieldsIssues(problems, fields, contract, of);
|
|
107
|
+
return problems.refusal("Invalid fields", undefined, withheld);
|
|
108
|
+
}
|
|
19
109
|
/**
|
|
20
|
-
* The
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
110
|
+
* The `fields` contract for a named read's result (`"result"`) or a list's
|
|
111
|
+
* item (`"item"`). `shape` is that result or item: a Schema rendered as the
|
|
112
|
+
* catalog renders it, or the catalog JSON Schema itself. Its first declared
|
|
113
|
+
* property is the description's example, so the example is always valid.
|
|
24
114
|
*/
|
|
25
|
-
export function
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
115
|
+
export function fieldsContract(anchor, shape, note, of = anchor === "item" ? "an item" : "the result") {
|
|
116
|
+
const schema = "ast" in shape ? catalogSchema(shape, "output") : shape;
|
|
117
|
+
const at = new Map();
|
|
118
|
+
propertiesAt(schema, at, true);
|
|
119
|
+
const example = [...at.keys()][0];
|
|
120
|
+
if (example === undefined)
|
|
121
|
+
throw new Error("fields needs a contract that declares at least one property.");
|
|
122
|
+
const description = fieldsDescription(anchor, example, note);
|
|
123
|
+
return {
|
|
124
|
+
description,
|
|
125
|
+
input: fieldsInput(description),
|
|
126
|
+
issues: (problems, fields) => fieldsIssues(problems, fields, schema, of),
|
|
127
|
+
problem: (fields) => fieldsProblem(fields, schema, of),
|
|
128
|
+
};
|
|
31
129
|
}
|
|
32
130
|
/** Keep only the named dot paths; arrays are narrowed item by item. */
|
|
33
131
|
export function selectFields(data, fields) {
|
|
@@ -40,10 +138,91 @@ export function selectFields(data, fields) {
|
|
|
40
138
|
return value;
|
|
41
139
|
const children = new Map();
|
|
42
140
|
for (const path of paths) {
|
|
43
|
-
const [key, ...tail] = path
|
|
141
|
+
const [key, ...tail] = segments(path);
|
|
44
142
|
children.set(key, [...(children.get(key) ?? []), tail.join(".")]);
|
|
45
143
|
}
|
|
46
144
|
return Object.fromEntries([...children].flatMap(([key, tails]) => Object.hasOwn(value, key) ? [[key, tails.includes("") ? value[key] : pick(value[key], tails)]] : []));
|
|
47
145
|
};
|
|
48
146
|
return pick(data, fields);
|
|
49
147
|
}
|
|
148
|
+
const empty = (value) => value === null ||
|
|
149
|
+
value === undefined ||
|
|
150
|
+
(Array.isArray(value) ? value.length === 0 : record(value) && !Object.keys(value).length);
|
|
151
|
+
/** Warning bounds: each warning's characters, the count kept, and every kept warning's characters together. */
|
|
152
|
+
export const WARNING_BUDGET = { chars: 512, warnings: 10, totalChars: 4096 };
|
|
153
|
+
/**
|
|
154
|
+
* Keep warnings within WARNING_BUDGET in order, cutting a long one with an
|
|
155
|
+
* ellipsis, then one count of what was omitted. Merged reviewed warnings and
|
|
156
|
+
* `fields` reports share the budget.
|
|
157
|
+
*/
|
|
158
|
+
/** At most `max` UTF-16 units, never splitting a surrogate pair. */
|
|
159
|
+
function codePointSlice(text, max) {
|
|
160
|
+
const end = max > 0 && /[\uD800-\uDBFF]/.test(text[max - 1] ?? "") ? max - 1 : max;
|
|
161
|
+
return text.slice(0, end);
|
|
162
|
+
}
|
|
163
|
+
export function boundWarnings(warnings) {
|
|
164
|
+
const kept = [];
|
|
165
|
+
let total = 0;
|
|
166
|
+
for (const warning of warnings) {
|
|
167
|
+
const text = warning.length > WARNING_BUDGET.chars ? `${codePointSlice(warning, WARNING_BUDGET.chars - 1)}…` : warning;
|
|
168
|
+
// Leave room for the omitted count.
|
|
169
|
+
if (kept.length >= WARNING_BUDGET.warnings || total + text.length > WARNING_BUDGET.totalChars - 64)
|
|
170
|
+
break;
|
|
171
|
+
kept.push(text);
|
|
172
|
+
total += text.length;
|
|
173
|
+
}
|
|
174
|
+
const omitted = warnings.length - kept.length;
|
|
175
|
+
return omitted ? [...kept, `${omitted} more warning${omitted === 1 ? "" : "s"} omitted.`] : kept;
|
|
176
|
+
}
|
|
177
|
+
/** Declared names listed in one warning, within its character budget. */
|
|
178
|
+
function named(names) {
|
|
179
|
+
let text = "";
|
|
180
|
+
let shown = 0;
|
|
181
|
+
for (const name of names) {
|
|
182
|
+
const next = text ? `${text}, ${name}` : name;
|
|
183
|
+
if (next.length > WARNING_BUDGET.chars - 200)
|
|
184
|
+
break;
|
|
185
|
+
text = next;
|
|
186
|
+
shown += 1;
|
|
187
|
+
}
|
|
188
|
+
return shown < names.length ? `${text}${text ? " and " : ""}${names.length - shown} more` : text;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* One warning per `fields` entry that matched nothing in a nonempty response,
|
|
192
|
+
* for contracts that cannot refuse it in advance. It names the entry's index,
|
|
193
|
+
* the segment where matching stopped and the properties the contract declares
|
|
194
|
+
* there, from `declared` (paths in `fields` coordinates). It never lists the
|
|
195
|
+
* response's own keys, which in an open map are data, nor the submitted text.
|
|
196
|
+
* A path some item matched, a path past a scalar (kept whole), or any path of
|
|
197
|
+
* an empty response, is no warning.
|
|
198
|
+
*/
|
|
199
|
+
export function fieldsMisses(data, fields, declared = []) {
|
|
200
|
+
if (!fields?.length || empty(data))
|
|
201
|
+
return [];
|
|
202
|
+
const flat = (values) => values.flatMap((value) => (Array.isArray(value) ? flat(value) : [value]));
|
|
203
|
+
const contract = declared.map((path) => segments(path.replace(/^\[\]\.?/, "")));
|
|
204
|
+
return fields.flatMap((field, index) => {
|
|
205
|
+
const path = segments(field);
|
|
206
|
+
let values = flat([data]);
|
|
207
|
+
for (const [depth, name] of path.entries()) {
|
|
208
|
+
const holders = values.filter((value) => record(value) && Object.hasOwn(value, name));
|
|
209
|
+
if (holders.length) {
|
|
210
|
+
values = flat(holders.map((value) => value[name]));
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// Selection keeps a scalar or null a path runs past, so only an object lacking the key is a miss.
|
|
214
|
+
if (!values.some(record))
|
|
215
|
+
return [];
|
|
216
|
+
const prefix = path.slice(0, depth);
|
|
217
|
+
const names = [
|
|
218
|
+
...new Set(contract
|
|
219
|
+
.filter((parts) => parts.length > depth && prefix.every((part, at) => parts[at] === part))
|
|
220
|
+
.map((parts) => parts[depth])),
|
|
221
|
+
];
|
|
222
|
+
return [
|
|
223
|
+
`fields/${index} matched nothing in this response after value safety and privacy filtering, at segment ${depth + 1}; ${names.length ? `declared there: ${named(names)}` : "the operation declares no properties there"}.`,
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
return [];
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* At most 100 paths and 4 KiB of path JSON, `withheld` included, an eighth of
|
|
3
|
+
* the 32 KiB details budget. That holds every path of nine in ten operations
|
|
4
|
+
* in each maintained index; larger responses keep their shallowest paths,
|
|
5
|
+
* roots first. A path over 200 characters, longer than one `fields` entry may
|
|
6
|
+
* be, is never listed.
|
|
7
|
+
*/
|
|
8
|
+
export const RESPONSE_BUDGET = { paths: 100, chars: 4096, pathChars: 200 };
|
|
9
|
+
/** `a[].b` → `["a", "b"]`. */
|
|
10
|
+
function names(path) {
|
|
11
|
+
return path.split(".").map((part) => part.replace(/\[\]$/, ""));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The capped response contract for one operation, or undefined when it
|
|
15
|
+
* declares no response properties (`fields` is then refused). Deterministic:
|
|
16
|
+
* paths keep the index's order within each depth.
|
|
17
|
+
*/
|
|
18
|
+
export function responseContract(index, op, safety) {
|
|
19
|
+
const declared = index.fields(op).filter((field) => field.in === "response");
|
|
20
|
+
if (declared.length === 0)
|
|
21
|
+
return undefined;
|
|
22
|
+
const anchor = declared.some((field) => field.path.startsWith("[]")) ? "data[]" : "data";
|
|
23
|
+
// The coordinates `fields` validates: an item path of a list body drops its leading `[].`.
|
|
24
|
+
const paths = [
|
|
25
|
+
...new Set(declared.map((field) => field.path.replace(/^\[\]\.?/, "")).filter((path) => /^[^.[]/.test(path))),
|
|
26
|
+
];
|
|
27
|
+
// Every root `fields` accepts is listed, including one the index names only beneath it.
|
|
28
|
+
const spelled = new Map();
|
|
29
|
+
for (const path of [...paths.map((path) => names(path)[0]), ...paths]) {
|
|
30
|
+
const key = names(path).join(".");
|
|
31
|
+
if (!spelled.has(key))
|
|
32
|
+
spelled.set(key, path);
|
|
33
|
+
}
|
|
34
|
+
let withholds;
|
|
35
|
+
try {
|
|
36
|
+
withholds = safety.withholds({ method: op.method, template: op.path });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
withholds = () => false;
|
|
40
|
+
}
|
|
41
|
+
// The hint reads paths with their list markers, a list body's items included.
|
|
42
|
+
const known = new Set([...spelled]
|
|
43
|
+
.filter(([, path]) => withholds([...(anchor === "data[]" ? ["[]"] : []), ...path.split(".")]))
|
|
44
|
+
.map(([key]) => key));
|
|
45
|
+
const depth = (path) => path.split(".").length;
|
|
46
|
+
const ordered = [...spelled.values()]
|
|
47
|
+
.map((path, at) => ({ path, at }))
|
|
48
|
+
.sort((a, b) => depth(a.path) - depth(b.path) || a.at - b.at);
|
|
49
|
+
const fields = [];
|
|
50
|
+
const withheld = [];
|
|
51
|
+
let chars = 0;
|
|
52
|
+
let kept = 0;
|
|
53
|
+
for (const { path } of ordered) {
|
|
54
|
+
// A path longer than a fields input can be is counted, never listed.
|
|
55
|
+
if (path.length > RESPONSE_BUDGET.pathChars)
|
|
56
|
+
continue;
|
|
57
|
+
const hinted = known.has(names(path).join("."));
|
|
58
|
+
const cost = (JSON.stringify(path).length + 1) * (hinted ? 2 : 1);
|
|
59
|
+
if (kept >= RESPONSE_BUDGET.paths || chars + cost > RESPONSE_BUDGET.chars)
|
|
60
|
+
break;
|
|
61
|
+
fields.push(path);
|
|
62
|
+
if (hinted)
|
|
63
|
+
withheld.push(path);
|
|
64
|
+
chars += cost;
|
|
65
|
+
kept += 1;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
anchor,
|
|
69
|
+
fields,
|
|
70
|
+
...(withheld.length ? { withheld } : {}),
|
|
71
|
+
...(kept < ordered.length ? { omitted: ordered.length - kept } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|