@zackbart/connecta 0.24.3 → 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.
Files changed (67) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +64 -1
  3. package/README.md +5 -6
  4. package/dist/branding.d.ts +31 -2
  5. package/dist/branding.js +116 -8
  6. package/dist/connectors/api.d.ts +1 -1
  7. package/dist/connectors/api.js +10 -2
  8. package/dist/connectors/guarded-fetch.d.ts +5 -1
  9. package/dist/connectors/guarded-fetch.js +34 -4
  10. package/dist/connectors/remote-mcp.js +8 -4
  11. package/dist/errors.d.ts +11 -3
  12. package/dist/errors.js +2 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +12 -1
  15. package/dist/meta-tools.js +105 -29
  16. package/dist/operator-ui/generated.js +2 -2
  17. package/dist/operator-ui/view.d.ts +38 -1
  18. package/dist/operator-ui/view.js +71 -0
  19. package/dist/providers/cloudflare.d.ts +14 -2
  20. package/dist/providers/cloudflare.js +107 -16
  21. package/dist/providers/linear.d.ts +26 -4
  22. package/dist/providers/linear.js +19 -4
  23. package/dist/providers/mixpanel.d.ts +16 -3
  24. package/dist/providers/mixpanel.js +13 -2
  25. package/dist/providers/notion.d.ts +8 -1
  26. package/dist/providers/notion.js +83 -10
  27. package/dist/providers/revenuecat.d.ts +30 -4
  28. package/dist/providers/revenuecat.js +42 -4
  29. package/dist/providers/stripe.d.ts +7 -1
  30. package/dist/providers/stripe.js +30 -4
  31. package/dist/providers/vercel.js +11 -1
  32. package/dist/registry.d.ts +12 -4
  33. package/dist/registry.js +22 -8
  34. package/dist/types.d.ts +37 -0
  35. package/dist/ui.js +18 -10
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/documentation/architecture.md +193 -181
  39. package/documentation/auth.md +197 -176
  40. package/documentation/code-mode.md +426 -321
  41. package/documentation/meta-tools.md +356 -416
  42. package/examples/worker/AGENTS.md +2 -1
  43. package/examples/worker/README.md +12 -10
  44. package/examples/worker/src/index.ts +12 -15
  45. package/package.json +1 -2
  46. package/templates/node/.env.example +3 -3
  47. package/templates/node/AGENTS.md +5 -4
  48. package/templates/node/README.md +2 -1
  49. package/templates/node/package.json +1 -1
  50. package/templates/node/src/index.ts +23 -22
  51. package/documentation/call-admission.md +0 -158
  52. package/documentation/cloudflare.md +0 -471
  53. package/documentation/connector-guides.md +0 -176
  54. package/documentation/connectors.md +0 -431
  55. package/documentation/linear.md +0 -193
  56. package/documentation/mixpanel.md +0 -160
  57. package/documentation/notion.md +0 -308
  58. package/documentation/operations.md +0 -359
  59. package/documentation/operator-ui.md +0 -135
  60. package/documentation/optional-modules-upgrade.md +0 -243
  61. package/documentation/provider-conventions.md +0 -729
  62. package/documentation/request-admission.md +0 -204
  63. package/documentation/revenuecat.md +0 -305
  64. package/documentation/storage-and-credentials.md +0 -254
  65. package/documentation/stripe.md +0 -262
  66. package/documentation/upgrading.md +0 -768
  67. package/documentation/vercel.md +0 -241
@@ -99,19 +99,60 @@ export function alignEndToCharBoundary(bytes, offset, end, total) {
99
99
  }
100
100
  return e;
101
101
  }
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+):`);
106
+ /**
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.
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
+ }
125
+ /**
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.
129
+ */
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
+ }
102
137
  /** Stash a completed result, or return a notice without a paging route. */
103
138
  async function stashResult(bytes, results) {
104
139
  const totalBytes = bytes.length;
105
140
  const id = crypto.randomUUID();
106
141
  try {
107
- // Base64 permits byte-range decoding after a KV read, without scanning or
108
- // re-encoding all preceding text. Each chunk is a multiple of three bytes.
109
- const chunks = [];
110
- for (let offset = 0; offset < bytes.length; offset += 12_288) {
111
- chunks.push(btoa(String.fromCharCode(...bytes.subarray(offset, offset + 12_288))));
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);
112
154
  }
113
- const stored = `connecta-result-v1:${totalBytes}:${chunks.join("")}`;
114
- if (!await results.set(`result:${id}`, stored, RESULT_TTL_SECONDS)) {
155
+ if (!await results.set(`result:${id}`, chunks, RESULT_TTL_SECONDS)) {
115
156
  throw new Error("Result stash capacity exhausted");
116
157
  }
117
158
  }
@@ -449,39 +490,74 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
449
490
  `>= ${MIN_RESULT_OFFSET}. Omit it to start at the beginning.`);
450
491
  }
451
492
  const results = registry.resultsStorage();
452
- let stored;
453
- try {
454
- stored = await results.get(`result:${args.id}`);
455
- }
456
- catch {
457
- return {
458
- ...jsonResult({
459
- error: {
460
- code: "unavailable",
461
- message: "Result paging storage is unavailable.",
462
- retryable: true,
463
- },
464
- }),
465
- isError: true,
466
- };
467
- }
468
- if (stored === null || stored === undefined) {
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) {
469
518
  return errorResult(`Unknown or expired result id "${boundedEchoText(args.id)}"`);
470
519
  }
471
520
  const requestedOffset = args.offset ?? 0;
472
521
  const maxBytes = args.maxBytes ?? globalCap;
473
- // Decode only this page plus UTF-8 boundary lookaround. Legacy raw-text
474
- // entries remain readable for their short TTL after an upgrade.
475
- const header = /^connecta-result-v1:(\d+):/.exec(stored.slice(0, 64));
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;
476
530
  let bytes;
477
531
  let total;
478
532
  let start = 0;
479
533
  if (header) {
480
534
  total = Number(header[1]);
535
+ const chunkBytes = chunked ? Number(chunked[2]) : Math.max(total, 1);
481
536
  start = Math.floor(Math.max(0, Math.min(requestedOffset, total) - 3) / 3) * 3;
482
537
  const end = Math.min(total, requestedOffset + maxBytes + 4);
483
- const binary = atob(stored.slice(header[0].length + start / 3 * 4, header[0].length + Math.ceil(end / 3) * 4));
484
- bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
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
+ }
485
561
  }
486
562
  else {
487
563
  bytes = enc.encode(stored);