@zackbart/connecta 0.24.2 → 0.24.4
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/AGENTS.md +18 -20
- package/CHANGELOG.md +205 -1
- package/README.md +5 -6
- package/dist/auth/bearer.js +2 -0
- package/dist/auth/downstream-oauth.d.ts +12 -1
- package/dist/auth/downstream-oauth.js +147 -35
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/call-admission.d.ts +4 -0
- package/dist/call-admission.js +26 -0
- package/dist/catalog-drift.js +9 -4
- package/dist/catalog-service.d.ts +2 -0
- package/dist/catalog-service.js +25 -8
- package/dist/catalog.d.ts +2 -0
- package/dist/catalog.js +246 -121
- package/dist/connectors/api.d.ts +1 -1
- package/dist/connectors/api.js +21 -3
- package/dist/connectors/guarded-fetch.d.ts +6 -2
- package/dist/connectors/guarded-fetch.js +61 -24
- package/dist/connectors/remote-mcp.js +92 -57
- package/dist/errors.d.ts +28 -3
- package/dist/errors.js +60 -1
- package/dist/execute.js +85 -23
- package/dist/executor-result.js +3 -1
- package/dist/executors/quickjs-child.js +5 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -0
- package/dist/executors/quickjs-runtime.d.ts +1 -1
- package/dist/executors/quickjs-runtime.js +38 -21
- package/dist/executors/quickjs.js +68 -27
- package/dist/index.d.ts +15 -1
- package/dist/index.js +36 -4
- package/dist/invocation.js +134 -93
- package/dist/mcp-result.js +3 -2
- package/dist/meta-tools.js +191 -36
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +38 -1
- package/dist/operator-ui/view.js +71 -0
- package/dist/providers/cloudflare.d.ts +14 -2
- package/dist/providers/cloudflare.js +107 -16
- package/dist/providers/linear.d.ts +26 -4
- package/dist/providers/linear.js +19 -4
- package/dist/providers/mixpanel.d.ts +16 -3
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/notion.d.ts +8 -1
- package/dist/providers/notion.js +83 -10
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +42 -4
- package/dist/providers/stripe.d.ts +7 -1
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +11 -1
- package/dist/registry.d.ts +22 -2
- package/dist/registry.js +101 -13
- package/dist/routes/mcp.d.ts +4 -1
- package/dist/routes/mcp.js +84 -13
- package/dist/routes/oauth.js +4 -0
- package/dist/routes/shared.d.ts +1 -0
- package/dist/routes/shared.js +4 -4
- package/dist/server.js +15 -3
- package/dist/skills.js +6 -5
- package/dist/storage/file.d.ts +6 -2
- package/dist/storage/file.js +312 -34
- package/dist/storage/memory.js +12 -1
- package/dist/types.d.ts +37 -0
- package/dist/ui.js +18 -10
- package/dist/validate.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +193 -165
- package/documentation/auth.md +199 -145
- package/documentation/code-mode.md +433 -316
- package/documentation/meta-tools.md +363 -355
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +12 -10
- package/examples/worker/src/index.ts +12 -15
- package/package.json +1 -2
- package/templates/node/.env.example +3 -3
- package/templates/node/AGENTS.md +5 -4
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +23 -22
- package/documentation/call-admission.md +0 -142
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -389
- package/documentation/linear.md +0 -193
- package/documentation/mixpanel.md +0 -160
- package/documentation/notion.md +0 -308
- package/documentation/operations.md +0 -359
- package/documentation/operator-ui.md +0 -135
- package/documentation/optional-modules-upgrade.md +0 -243
- package/documentation/provider-conventions.md +0 -722
- package/documentation/request-admission.md +0 -170
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -201
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -754
- package/documentation/vercel.md +0 -241
package/dist/meta-tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { boundedDiscoveryText, CatalogService, DEFAULT_SEARCH_LIMIT, DiscoveryPolicyError, groupedSearchResult, MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT, } from "./catalog-service.js";
|
|
3
3
|
import { resolveDiscoveryConcurrency } from "./concurrency.js";
|
|
4
|
-
import { msg } from "./errors.js";
|
|
4
|
+
import { boundedEchoText, msg } from "./errors.js";
|
|
5
5
|
import { serializeResultText } from "./executor-result.js";
|
|
6
6
|
import { InvocationService, } from "./invocation.js";
|
|
7
7
|
import { isValidMaxResultBytes, MIN_MAX_RESULT_BYTES, resolveMaxResultBytes, } from "./registry.js";
|
|
@@ -37,7 +37,12 @@ async function discoveryResult(operation, hint) {
|
|
|
37
37
|
try {
|
|
38
38
|
const value = await operation();
|
|
39
39
|
const text = boundedDiscoveryText(value, hint);
|
|
40
|
-
|
|
40
|
+
const result = jsonResult(value, text);
|
|
41
|
+
const bytes = enc.encode(JSON.stringify(result)).length;
|
|
42
|
+
if (bytes > MAX_DISCOVERY_RESULT_BYTES) {
|
|
43
|
+
throw new DiscoveryPolicyError("result_too_large", `Discovery result is ${bytes} UTF-8 bytes, over the ${MAX_DISCOVERY_RESULT_BYTES}-byte ceiling. ${hint}`);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
41
46
|
}
|
|
42
47
|
catch (err) {
|
|
43
48
|
if (err instanceof DiscoveryPolicyError) {
|
|
@@ -94,29 +99,78 @@ export function alignEndToCharBoundary(bytes, offset, end, total) {
|
|
|
94
99
|
}
|
|
95
100
|
return e;
|
|
96
101
|
}
|
|
97
|
-
|
|
102
|
+
/** Prefix of the chunked paging envelope; `v1`'s single inline key still reads. */
|
|
103
|
+
const RESULT_ENVELOPE_V2 = "connecta-result-v2:";
|
|
104
|
+
/** `<total bytes>:<bytes per chunk>:` follow the prefix, then chunk 0's base64. */
|
|
105
|
+
const RESULT_ENVELOPE_V2_HEADER = new RegExp(`^${RESULT_ENVELOPE_V2}(\\d+):(\\d+):`);
|
|
98
106
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* at all (a BigInt) still throws, as before, and is reported as a failure.
|
|
107
|
+
* Smallest chunk of result text stored under one key. A multiple of three so
|
|
108
|
+
* every chunk's base64 stands alone and a byte offset inside it lands on a
|
|
109
|
+
* whole quad, and a little under the 50,000-byte default page so a default page
|
|
110
|
+
* reads two or three chunks rather than dozens.
|
|
111
|
+
*/
|
|
112
|
+
const RESULT_CHUNK_BYTES = 49_152;
|
|
113
|
+
/**
|
|
114
|
+
* Keys one stashed result may occupy. Chunking trades write count for read
|
|
115
|
+
* count, and both are real: every chunk is a storage write at stash time, and
|
|
116
|
+
* `fileStorage` rewrites its whole file per write. Above roughly 1.5 MB the
|
|
117
|
+
* chunks widen instead of multiplying, so a result costs a bounded number of
|
|
118
|
+
* writes and a page still reads a small fraction of it.
|
|
112
119
|
*/
|
|
120
|
+
const RESULT_MAX_CHUNKS = 32;
|
|
121
|
+
/** Chunk width for a result of `totalBytes`, always a multiple of three. */
|
|
122
|
+
function resultChunkBytes(totalBytes) {
|
|
123
|
+
return Math.max(RESULT_CHUNK_BYTES, Math.ceil(totalBytes / RESULT_MAX_CHUNKS / 3) * 3);
|
|
124
|
+
}
|
|
113
125
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
126
|
+
* Base64 of `bytes`, in three-byte-aligned batches so the argument list of one
|
|
127
|
+
* spread never grows with the result. Alignment matters: an unaligned batch
|
|
128
|
+
* would pad mid-stream and the concatenation would no longer decode.
|
|
116
129
|
*/
|
|
117
|
-
|
|
130
|
+
function base64Of(bytes) {
|
|
131
|
+
let out = "";
|
|
132
|
+
for (let offset = 0; offset < bytes.length; offset += 12_288) {
|
|
133
|
+
out += btoa(String.fromCharCode(...bytes.subarray(offset, offset + 12_288)));
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
/** Stash a completed result, or return a notice without a paging route. */
|
|
138
|
+
async function stashResult(bytes, results) {
|
|
139
|
+
const totalBytes = bytes.length;
|
|
118
140
|
const id = crypto.randomUUID();
|
|
119
|
-
|
|
141
|
+
try {
|
|
142
|
+
// Base64 permits byte-range decoding, and splitting the envelope across
|
|
143
|
+
// keys keeps a page's storage read proportional to the page instead of to
|
|
144
|
+
// the whole result (issue #540). Chunk 0 carries the header; the get_result
|
|
145
|
+
// reader below maps a byte offset back to chunk index and base64 quad.
|
|
146
|
+
const chunkBytes = resultChunkBytes(totalBytes);
|
|
147
|
+
const chunks = [`${RESULT_ENVELOPE_V2}${totalBytes}:${chunkBytes}:`];
|
|
148
|
+
for (let offset = 0; offset < bytes.length; offset += chunkBytes) {
|
|
149
|
+
const chunk = base64Of(bytes.subarray(offset, offset + chunkBytes));
|
|
150
|
+
if (offset === 0)
|
|
151
|
+
chunks[0] += chunk;
|
|
152
|
+
else
|
|
153
|
+
chunks.push(chunk);
|
|
154
|
+
}
|
|
155
|
+
if (!await results.set(`result:${id}`, chunks, RESULT_TTL_SECONDS)) {
|
|
156
|
+
throw new Error("Result stash capacity exhausted");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Paging is advisory after a completed call, including an approved write.
|
|
161
|
+
// Neither backend prose nor a retry hint belongs in this successful result.
|
|
162
|
+
try {
|
|
163
|
+
results.warn();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Logging cannot change the call either.
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
truncated: true,
|
|
170
|
+
totalBytes,
|
|
171
|
+
hint: "Paging is unavailable. Use execute_code to reduce read-only results before returning them. Do not repeat a completed write to recover its result.",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
120
174
|
return {
|
|
121
175
|
truncated: true,
|
|
122
176
|
resultId: id,
|
|
@@ -141,7 +195,7 @@ async function guardEncoded(text, bytes, results, cap) {
|
|
|
141
195
|
truncated: false,
|
|
142
196
|
};
|
|
143
197
|
}
|
|
144
|
-
const notice = await stashResult(
|
|
198
|
+
const notice = await stashResult(bytes, results);
|
|
145
199
|
const head = dec.decode(bytes.slice(0, alignEndToCharBoundary(bytes, 0, cap, bytes.length)));
|
|
146
200
|
return {
|
|
147
201
|
result: {
|
|
@@ -166,8 +220,12 @@ async function guardValue(value, results, cap) {
|
|
|
166
220
|
const bytes = enc.encode(text);
|
|
167
221
|
if (bytes.length <= cap)
|
|
168
222
|
return { result: value, truncated: false };
|
|
223
|
+
const notice = await stashResult(bytes, results);
|
|
169
224
|
return {
|
|
170
|
-
result:
|
|
225
|
+
result: notice.resultId ? notice : {
|
|
226
|
+
...notice,
|
|
227
|
+
preview: dec.decode(bytes.slice(0, alignEndToCharBoundary(bytes, 0, cap, bytes.length))),
|
|
228
|
+
},
|
|
171
229
|
truncated: true,
|
|
172
230
|
};
|
|
173
231
|
}
|
|
@@ -210,7 +268,7 @@ async function guardContent(content, results, cap) {
|
|
|
210
268
|
if (content.every((b) => b.type === "text")) {
|
|
211
269
|
return guardEncoded(text, bytes, results, cap);
|
|
212
270
|
}
|
|
213
|
-
const notice = await stashResult(
|
|
271
|
+
const notice = await stashResult(bytes, results);
|
|
214
272
|
return {
|
|
215
273
|
result: { content: [{ type: "text", text: JSON.stringify(notice) }] },
|
|
216
274
|
truncated: true,
|
|
@@ -259,7 +317,6 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
259
317
|
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
260
318
|
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
261
319
|
async function runCall(call, source, options = {}) {
|
|
262
|
-
const results = registry.resultsStorage();
|
|
263
320
|
const timeoutMs = normalizeTimeoutMs(call.timeoutMs) ?? defaultToolTimeoutMs;
|
|
264
321
|
const outcome = await invocation.invoke(call.address, call.args ?? {}, {
|
|
265
322
|
source,
|
|
@@ -272,6 +329,13 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
272
329
|
: {}),
|
|
273
330
|
unwrapResult: call.resultMode === "value",
|
|
274
331
|
processResult: async (result, resolved) => {
|
|
332
|
+
const results = {
|
|
333
|
+
set: (key, value, ttlSeconds) => registry.stashResult(key, value, ttlSeconds),
|
|
334
|
+
warn: () => registry.contextFor(resolved.connector.id, baseUrl, requestScope).logger.warn("[connecta] result paging unavailable", {
|
|
335
|
+
connector: resolved.connector.id,
|
|
336
|
+
tool: resolved.toolName,
|
|
337
|
+
}),
|
|
338
|
+
};
|
|
275
339
|
// Result-size cap for THIS call: the connector's own override wins,
|
|
276
340
|
// then the deployment-wide value, then the built-in default (already
|
|
277
341
|
// folded into `globalCap`). Resolved per call so one request can
|
|
@@ -292,7 +356,14 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
292
356
|
}
|
|
293
357
|
if (resolved.connector.kind === "mcp") {
|
|
294
358
|
const mcpResult = result;
|
|
295
|
-
|
|
359
|
+
let content = mcpResult?.content ?? [];
|
|
360
|
+
if (!content.some((block) => block.type === "text") &&
|
|
361
|
+
mcpResult?.structuredContent !== undefined) {
|
|
362
|
+
content = [...content, {
|
|
363
|
+
type: "text",
|
|
364
|
+
text: JSON.stringify(mcpResult.structuredContent),
|
|
365
|
+
}];
|
|
366
|
+
}
|
|
296
367
|
const guarded = await guardContent(content, results, cap);
|
|
297
368
|
return processed(guarded.result, guarded.truncated);
|
|
298
369
|
}
|
|
@@ -305,12 +376,15 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
305
376
|
if (!outcome.ok) {
|
|
306
377
|
const structuredRecovery = outcome.error.nextAction !== undefined;
|
|
307
378
|
const recoveryRequired = structuredRecovery ||
|
|
379
|
+
// Sanitized `unavailable` diagnostics ride the structured shape too;
|
|
380
|
+
// the plain-text path would drop them (#539).
|
|
381
|
+
outcome.error.details !== undefined ||
|
|
308
382
|
[
|
|
309
383
|
"auth_required",
|
|
310
384
|
"invalid_args",
|
|
311
385
|
"input_required_unsupported",
|
|
312
386
|
].includes(outcome.error.code);
|
|
313
|
-
const
|
|
387
|
+
const makeFailedResult = () => recoveryRequired ||
|
|
314
388
|
call.resultMode === "value"
|
|
315
389
|
? jsonResult({
|
|
316
390
|
ok: false,
|
|
@@ -320,6 +394,18 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
320
394
|
...(call.diagnostics ? { timing: outcome.timing } : {}),
|
|
321
395
|
})
|
|
322
396
|
: errorResult(outcome.error.message);
|
|
397
|
+
let failedResult = makeFailedResult();
|
|
398
|
+
// Value mode repeats the error in text and structuredContent. Account for
|
|
399
|
+
// both copies and JSON escaping when the bounded provider reason is large.
|
|
400
|
+
if (!recoveryRequired) {
|
|
401
|
+
const cap = resolveMaxResultBytes(outcome.resolved?.connector.maxResultBytes, globalCap);
|
|
402
|
+
let budget = 512;
|
|
403
|
+
while (enc.encode(JSON.stringify(failedResult)).length > cap && budget > 0) {
|
|
404
|
+
budget = Math.floor(budget / 2);
|
|
405
|
+
outcome.error.message = boundedEchoText(outcome.error.message, budget);
|
|
406
|
+
failedResult = makeFailedResult();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
323
409
|
if (recoveryRequired) {
|
|
324
410
|
failedResult.isError = true;
|
|
325
411
|
}
|
|
@@ -372,6 +458,9 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
372
458
|
return { content: [{ type: "text", text: skill.content }] };
|
|
373
459
|
},
|
|
374
460
|
async searchTools(args) {
|
|
461
|
+
if (args.connector !== undefined && enc.encode(args.connector).length > 512) {
|
|
462
|
+
return discoveryErrorResult(new DiscoveryPolicyError("invalid_args", "connector must be at most 512 UTF-8 bytes."));
|
|
463
|
+
}
|
|
375
464
|
return discoveryResult(async () => groupedSearchResult(await catalog.search({
|
|
376
465
|
...args,
|
|
377
466
|
includeSchemaKeys: args.includeSchemas !== undefined,
|
|
@@ -401,29 +490,95 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
401
490
|
`>= ${MIN_RESULT_OFFSET}. Omit it to start at the beginning.`);
|
|
402
491
|
}
|
|
403
492
|
const results = registry.resultsStorage();
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
493
|
+
const unavailableResult = () => ({
|
|
494
|
+
...jsonResult({
|
|
495
|
+
error: {
|
|
496
|
+
code: "unavailable",
|
|
497
|
+
message: "Result paging storage is unavailable.",
|
|
498
|
+
retryable: true,
|
|
499
|
+
},
|
|
500
|
+
}),
|
|
501
|
+
isError: true,
|
|
502
|
+
});
|
|
503
|
+
// `false` is a storage failure — retryable, and distinct from an id that
|
|
504
|
+
// is simply gone. Every key a page touches answers the same way, so a
|
|
505
|
+
// backend that dies halfway through a multi-chunk page says so.
|
|
506
|
+
const read = async (key) => {
|
|
507
|
+
try {
|
|
508
|
+
return await results.get(key) ?? null;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const stored = await read(`result:${args.id}`);
|
|
515
|
+
if (stored === false)
|
|
516
|
+
return unavailableResult();
|
|
517
|
+
if (stored === null) {
|
|
518
|
+
return errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
|
|
519
|
+
}
|
|
520
|
+
const requestedOffset = args.offset ?? 0;
|
|
521
|
+
const maxBytes = args.maxBytes ?? globalCap;
|
|
522
|
+
// Read and decode only the chunks this page covers, plus a few bytes of
|
|
523
|
+
// UTF-8 boundary lookaround. Pre-upgrade entries stay readable for their
|
|
524
|
+
// short TTL: v1 inlined the whole envelope under one key, which is this
|
|
525
|
+
// format with a single chunk as wide as the result, and raw text before
|
|
526
|
+
// that still pays one full encode per page.
|
|
527
|
+
const chunked = RESULT_ENVELOPE_V2_HEADER.exec(stored.slice(0, 80));
|
|
528
|
+
const inline = chunked ? null : /^connecta-result-v1:(\d+):/.exec(stored.slice(0, 64));
|
|
529
|
+
const header = chunked ?? inline;
|
|
530
|
+
let bytes;
|
|
531
|
+
let total;
|
|
532
|
+
let start = 0;
|
|
533
|
+
if (header) {
|
|
534
|
+
total = Number(header[1]);
|
|
535
|
+
const chunkBytes = chunked ? Number(chunked[2]) : Math.max(total, 1);
|
|
536
|
+
start = Math.floor(Math.max(0, Math.min(requestedOffset, total) - 3) / 3) * 3;
|
|
537
|
+
const end = Math.min(total, requestedOffset + maxBytes + 4);
|
|
538
|
+
bytes = new Uint8Array(Math.max(0, end - start));
|
|
539
|
+
const lastChunk = Math.floor(Math.max(end - 1, start) / chunkBytes);
|
|
540
|
+
for (let index = Math.floor(start / chunkBytes); index <= lastChunk; index++) {
|
|
541
|
+
const encoded = index === 0
|
|
542
|
+
? stored.slice(header[0].length)
|
|
543
|
+
: await read(`result:${args.id}#${index}`);
|
|
544
|
+
if (encoded === false)
|
|
545
|
+
return unavailableResult();
|
|
546
|
+
if (encoded === null) {
|
|
547
|
+
// A chunk expired or was evicted under its own header; the id can no
|
|
548
|
+
// longer serve this range, and inventing U+0000 filler would be worse.
|
|
549
|
+
return errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
|
|
550
|
+
}
|
|
551
|
+
const chunkStart = index * chunkBytes;
|
|
552
|
+
// Both bounds are chunk-local. `from` inherits `start`'s three-byte
|
|
553
|
+
// alignment because every chunk boundary is a multiple of three.
|
|
554
|
+
const from = Math.max(start, chunkStart) - chunkStart;
|
|
555
|
+
const to = Math.min(end, chunkStart + chunkBytes, total) - chunkStart;
|
|
556
|
+
const binary = atob(encoded.slice(from / 3 * 4, Math.ceil(to / 3) * 4));
|
|
557
|
+
for (let at = from; at < to; at++) {
|
|
558
|
+
bytes[chunkStart + at - start] = binary.charCodeAt(at - from);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
bytes = enc.encode(stored);
|
|
564
|
+
total = bytes.length;
|
|
407
565
|
}
|
|
408
|
-
const bytes = enc.encode(stored);
|
|
409
|
-
const total = bytes.length;
|
|
410
566
|
// Validated above, so no coercion is needed here — only alignment. A
|
|
411
567
|
// client that computes its own offsets can land inside a multi-byte
|
|
412
568
|
// character, which would decode as U+FFFD; the offset actually served is
|
|
413
569
|
// the boundary at or before it, and it is what the response reports back
|
|
414
570
|
// as `offset` (issue #38).
|
|
415
|
-
const offset = alignStartToCharBoundary(bytes,
|
|
571
|
+
const offset = start + alignStartToCharBoundary(bytes, requestedOffset - start);
|
|
416
572
|
// Page size only: a stashed result carries no connector identity, so
|
|
417
573
|
// get_result keeps the deployment-wide default when none is requested.
|
|
418
574
|
// Both sides are validated by now — the argument above, `globalCap` at
|
|
419
575
|
// intake — so `offset + maxBytes` always reaches past `offset`.
|
|
420
|
-
const maxBytes = args.maxBytes ?? globalCap;
|
|
421
576
|
// Align the slice end to a codepoint boundary so a multi-byte char is
|
|
422
577
|
// never split across pages (which would emit U+FFFD on both sides).
|
|
423
578
|
// `nextOffset` is this aligned end, so it is a valid boundary for the
|
|
424
579
|
// next call and paging reassembles the original byte-for-byte.
|
|
425
|
-
const end = alignEndToCharBoundary(bytes, offset, offset + maxBytes, total);
|
|
426
|
-
const slice = dec.decode(bytes.
|
|
580
|
+
const end = start + alignEndToCharBoundary(bytes, offset - start, offset - start + maxBytes, total - start);
|
|
581
|
+
const slice = dec.decode(bytes.subarray(offset - start, end - start));
|
|
427
582
|
const nextOffset = end < total ? end : undefined;
|
|
428
583
|
return jsonResult({
|
|
429
584
|
offset,
|
|
@@ -435,7 +590,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
435
590
|
async authorizeConnector(args) {
|
|
436
591
|
const connector = registry.getConnector(args.connector);
|
|
437
592
|
if (!connector) {
|
|
438
|
-
return errorResult(`Unknown connector "${args.connector}"`);
|
|
593
|
+
return errorResult(`Unknown connector "${boundedEchoText(args.connector)}"`);
|
|
439
594
|
}
|
|
440
595
|
if (!connector.startAuth) {
|
|
441
596
|
if (!connector.credential) {
|