@zackbart/connecta 0.7.6 → 0.7.8

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 (62) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/activity.d.ts +17 -0
  3. package/dist/activity.d.ts.map +1 -1
  4. package/dist/activity.js.map +1 -1
  5. package/dist/auth/clerk.d.ts.map +1 -1
  6. package/dist/auth/clerk.js +101 -0
  7. package/dist/auth/clerk.js.map +1 -1
  8. package/dist/auth/downstream-oauth.d.ts +57 -20
  9. package/dist/auth/downstream-oauth.d.ts.map +1 -1
  10. package/dist/auth/downstream-oauth.js +275 -67
  11. package/dist/auth/downstream-oauth.js.map +1 -1
  12. package/dist/catalog.d.ts +5 -3
  13. package/dist/catalog.d.ts.map +1 -1
  14. package/dist/catalog.js +13 -4
  15. package/dist/catalog.js.map +1 -1
  16. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  17. package/dist/connectors/remote-mcp.js +165 -103
  18. package/dist/connectors/remote-mcp.js.map +1 -1
  19. package/dist/execute.d.ts.map +1 -1
  20. package/dist/execute.js +27 -15
  21. package/dist/execute.js.map +1 -1
  22. package/dist/index.d.ts +9 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/meta-tools.d.ts +4 -4
  27. package/dist/meta-tools.d.ts.map +1 -1
  28. package/dist/meta-tools.js +85 -25
  29. package/dist/meta-tools.js.map +1 -1
  30. package/dist/registry.d.ts +24 -0
  31. package/dist/registry.d.ts.map +1 -1
  32. package/dist/registry.js +87 -17
  33. package/dist/registry.js.map +1 -1
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +238 -19
  36. package/dist/server.js.map +1 -1
  37. package/dist/storage/file.d.ts.map +1 -1
  38. package/dist/storage/file.js +8 -0
  39. package/dist/storage/file.js.map +1 -1
  40. package/dist/types.d.ts +21 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/ui.d.ts +7 -1
  43. package/dist/ui.d.ts.map +1 -1
  44. package/dist/ui.js +118 -4
  45. package/dist/ui.js.map +1 -1
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/package.json +1 -1
  49. package/src/activity.ts +20 -0
  50. package/src/auth/clerk.ts +124 -0
  51. package/src/auth/downstream-oauth.ts +359 -68
  52. package/src/catalog.ts +24 -4
  53. package/src/connectors/remote-mcp.ts +172 -104
  54. package/src/execute.ts +29 -13
  55. package/src/index.ts +12 -1
  56. package/src/meta-tools.ts +89 -25
  57. package/src/registry.ts +115 -20
  58. package/src/server.ts +306 -16
  59. package/src/storage/file.ts +7 -0
  60. package/src/types.ts +23 -0
  61. package/src/ui.ts +124 -3
  62. package/src/version.ts +1 -1
package/src/registry.ts CHANGED
@@ -25,6 +25,8 @@ const ID_RE = /^[a-z0-9_-]+$/;
25
25
  const DEFAULT_TTL_SECONDS = 300;
26
26
  const DEFAULT_STALE_SECONDS = 3600;
27
27
  export const DEFAULT_MAX_RESULT_BYTES = 50_000;
28
+ /** Independent final-envelope boundary for `batch_call`. */
29
+ export const DEFAULT_MAX_BATCH_RESULT_BYTES = 100_000;
28
30
 
29
31
  /**
30
32
  * Smallest accepted inline-result cap. One byte is pathological but harmless:
@@ -143,6 +145,11 @@ export interface RegistryOptions {
143
145
  * to the default 50_000.
144
146
  */
145
147
  maxResultBytes?: number;
148
+ /**
149
+ * Cap on the complete serialized batch_call envelope. Must be a whole number
150
+ * of bytes >= 1; anything else warns and falls back to 100_000.
151
+ */
152
+ maxBatchResultBytes?: number;
146
153
  /** Tuning for the credential liveness checks (issue #24). */
147
154
  credentialHealth?: CredentialHealthConfig;
148
155
  }
@@ -179,6 +186,8 @@ type ConnectorOperationOptions = Pick<
179
186
  export interface RegistryView {
180
187
  /** Deployment-wide result-size cap threaded to the meta-tools. */
181
188
  readonly maxResultBytes: number;
189
+ /** Independent cap for the complete serialized batch_call envelope. */
190
+ readonly maxBatchResultBytes: number;
182
191
  listConnectors(): Connector[];
183
192
  getConnector(id: string): Connector | undefined;
184
193
  resolveAddress(
@@ -235,6 +244,10 @@ export class Registry implements RegistryView {
235
244
  private readonly connectors = new Map<string, Connector>();
236
245
  private readonly cache = new Map<string, CacheEntry>();
237
246
  private readonly invalidated = new Set<string>();
247
+ /** Per-connector epoch preventing a pre-invalidation refresh from publishing. */
248
+ private readonly catalogGenerations = new Map<string, number>();
249
+ /** Serialize persisted catalog set/delete operations within this isolate. */
250
+ private readonly catalogMutations = new Map<string, Promise<void>>();
238
251
  /** Deployment-wide observations — every call, whatever view made it. */
239
252
  private readonly health = new HealthLog();
240
253
  private readonly ttlMs: number;
@@ -242,6 +255,8 @@ export class Registry implements RegistryView {
242
255
  private readonly persistToolCatalog: boolean;
243
256
  /** Result-size guard cap threaded to the meta-tools. */
244
257
  readonly maxResultBytes: number;
258
+ /** Final batch envelope cap threaded to the meta-tools. */
259
+ readonly maxBatchResultBytes: number;
245
260
  /** Proactive liveness checks over stored downstream credentials (issue #24). */
246
261
  private readonly credentialHealth: CredentialHealthChecker;
247
262
 
@@ -258,6 +273,10 @@ export class Registry implements RegistryView {
258
273
  opts.maxResultBytes,
259
274
  DEFAULT_MAX_RESULT_BYTES,
260
275
  );
276
+ this.maxBatchResultBytes = resolveMaxResultBytes(
277
+ opts.maxBatchResultBytes,
278
+ DEFAULT_MAX_BATCH_RESULT_BYTES,
279
+ );
261
280
  for (const c of connectors) {
262
281
  if (!ID_RE.test(c.id)) {
263
282
  throw new Error(
@@ -270,7 +289,11 @@ export class Registry implements RegistryView {
270
289
  this.connectors.set(c.id, c);
271
290
  }
272
291
  this.checkConventions(opts.logger);
273
- this.checkResultCaps(opts.logger, opts.maxResultBytes);
292
+ this.checkResultCaps(
293
+ opts.logger,
294
+ opts.maxResultBytes,
295
+ opts.maxBatchResultBytes,
296
+ );
274
297
  this.credentialHealth = new CredentialHealthChecker(
275
298
  {
276
299
  listConnectors: () => this.listConnectors(),
@@ -296,6 +319,7 @@ export class Registry implements RegistryView {
296
319
  private checkResultCaps(
297
320
  logger: Logger,
298
321
  configured: number | undefined,
322
+ configuredBatch: number | undefined,
299
323
  ): void {
300
324
  if (configured !== undefined && !isValidMaxResultBytes(configured)) {
301
325
  logger.warn(
@@ -305,6 +329,17 @@ export class Registry implements RegistryView {
305
329
  `default ${DEFAULT_MAX_RESULT_BYTES} instead.`,
306
330
  );
307
331
  }
332
+ if (
333
+ configuredBatch !== undefined &&
334
+ !isValidMaxResultBytes(configuredBatch)
335
+ ) {
336
+ logger.warn(
337
+ `[connecta] calls.maxBatchResultBytes ${configuredBatch} is not a whole ` +
338
+ `number of bytes >= ${MIN_MAX_RESULT_BYTES}: it would leave the final ` +
339
+ "batch envelope unbounded or serve an unusable page. Using the " +
340
+ `default ${DEFAULT_MAX_BATCH_RESULT_BYTES} instead.`,
341
+ );
342
+ }
308
343
  for (const c of this.connectors.values()) {
309
344
  if (
310
345
  c.maxResultBytes !== undefined &&
@@ -459,6 +494,36 @@ export class Registry implements RegistryView {
459
494
  });
460
495
  }
461
496
 
497
+ private catalogGeneration(id: string): number {
498
+ return this.catalogGenerations.get(id) ?? 0;
499
+ }
500
+
501
+ private advanceCatalogGeneration(id: string): void {
502
+ this.catalogGenerations.set(id, this.catalogGeneration(id) + 1);
503
+ }
504
+
505
+ /**
506
+ * Keep this isolate's writes and invalidations ordered. Without the queue, an
507
+ * old refresh can finish its storage.set after a credential change deletes
508
+ * the catalog and resurrect the pre-change listing.
509
+ */
510
+ private enqueueCatalogMutation(
511
+ id: string,
512
+ operation: () => Promise<void>,
513
+ ): Promise<void> {
514
+ const previous = this.catalogMutations.get(id) ?? Promise.resolve();
515
+ const next = previous.catch(() => {}).then(operation);
516
+ this.catalogMutations.set(id, next);
517
+ void next
518
+ .finally(() => {
519
+ if (this.catalogMutations.get(id) === next) {
520
+ this.catalogMutations.delete(id);
521
+ }
522
+ })
523
+ .catch(() => {});
524
+ return next;
525
+ }
526
+
462
527
  /** Force a live listTools refresh and replace both catalog cache layers. */
463
528
  async refreshTools(
464
529
  id: string,
@@ -468,11 +533,16 @@ export class Registry implements RegistryView {
468
533
  ): Promise<ToolDef[]> {
469
534
  const connector = this.connectors.get(id);
470
535
  if (!connector) throw new Error(`Unknown connector "${id}"`);
536
+ const generation = this.catalogGeneration(id);
471
537
  const tools = connector.staticTools
472
538
  ? connector.staticTools
473
539
  : await connector.listTools(
474
540
  this.contextFor(id, baseUrl, requestScope, callOptions),
475
541
  );
542
+ // The caller that began this refresh may still use its result, but a
543
+ // credential/OAuth change that landed while listTools was in flight means
544
+ // the listing must not enter either shared cache layer.
545
+ if (generation !== this.catalogGeneration(id)) return tools;
476
546
  const now = Date.now();
477
547
  const previous = this.cache.get(id);
478
548
  const catalogChanged =
@@ -487,13 +557,16 @@ export class Registry implements RegistryView {
487
557
  });
488
558
  this.invalidated.delete(id);
489
559
  if (shouldPersist) {
490
- try {
491
- await this.storeCatalog(id, tools);
492
- } catch (err) {
493
- this.opts.logger.warn(
494
- `[connecta] connector "${id}" catalog persistence failed: ${msg(err)}`,
495
- );
496
- }
560
+ await this.enqueueCatalogMutation(id, async () => {
561
+ if (generation !== this.catalogGeneration(id)) return;
562
+ try {
563
+ await this.storeCatalog(id, tools);
564
+ } catch (err) {
565
+ this.opts.logger.warn(
566
+ `[connecta] connector "${id}" catalog persistence failed: ${msg(err)}`,
567
+ );
568
+ }
569
+ });
497
570
  }
498
571
  return tools;
499
572
  }
@@ -510,11 +583,13 @@ export class Registry implements RegistryView {
510
583
  if (connector.staticTools) return connector.staticTools;
511
584
 
512
585
  const now = Date.now();
586
+ const requestGeneration = this.catalogGeneration(id);
513
587
  const hit = this.cache.get(id);
514
588
  if (hit && hit.exp > now) return hit.tools;
515
589
 
516
590
  let stale = hit && hit.staleUntil > now ? hit.tools : undefined;
517
591
  if (this.persistToolCatalog && !this.invalidated.has(id)) {
592
+ const generation = this.catalogGeneration(id);
518
593
  let persisted: PersistedCatalog | null = null;
519
594
  try {
520
595
  persisted = this.validCatalog(
@@ -525,6 +600,10 @@ export class Registry implements RegistryView {
525
600
  `[connecta] connector "${id}" catalog read failed: ${msg(err)}`,
526
601
  );
527
602
  }
603
+ if (generation !== this.catalogGeneration(id)) {
604
+ persisted = null;
605
+ stale = undefined;
606
+ }
528
607
  if (persisted && persisted.staleUntil > now) {
529
608
  this.cache.set(id, {
530
609
  tools: persisted.tools,
@@ -539,7 +618,11 @@ export class Registry implements RegistryView {
539
618
  try {
540
619
  return await this.refreshTools(id, baseUrl, requestScope, callOptions);
541
620
  } catch (err) {
542
- if (stale) {
621
+ if (
622
+ stale &&
623
+ requestGeneration === this.catalogGeneration(id) &&
624
+ !this.invalidated.has(id)
625
+ ) {
543
626
  this.opts.logger.warn(
544
627
  `[connecta] connector "${id}" catalog refresh failed; serving stale catalog: ${msg(err)}`,
545
628
  );
@@ -675,29 +758,37 @@ export class Registry implements RegistryView {
675
758
 
676
759
  /** Drop a connector's cached tool list (e.g. after auth completes). */
677
760
  invalidate(id: string): void {
761
+ this.advanceCatalogGeneration(id);
678
762
  this.cache.delete(id);
679
763
  this.invalidated.add(id);
680
764
  if (this.persistToolCatalog) {
681
- void this.opts.storage.delete(this.catalogKey(id)).catch((err) => {
682
- this.opts.logger.warn(
683
- `[connecta] connector "${id}" catalog invalidation failed: ${msg(err)}`,
684
- );
765
+ void this.enqueueCatalogMutation(id, async () => {
766
+ try {
767
+ await this.opts.storage.delete(this.catalogKey(id));
768
+ } catch (err) {
769
+ this.opts.logger.warn(
770
+ `[connecta] connector "${id}" catalog invalidation failed: ${msg(err)}`,
771
+ );
772
+ }
685
773
  });
686
774
  }
687
775
  }
688
776
 
689
777
  /** Drop both in-memory and persisted tool catalogs. */
690
778
  async invalidateStored(id: string): Promise<void> {
779
+ this.advanceCatalogGeneration(id);
691
780
  this.cache.delete(id);
692
781
  this.invalidated.add(id);
693
782
  if (this.persistToolCatalog) {
694
- try {
695
- await this.opts.storage.delete(this.catalogKey(id));
696
- } catch (err) {
697
- this.opts.logger.warn(
698
- `[connecta] connector "${id}" catalog invalidation failed: ${msg(err)}`,
699
- );
700
- }
783
+ await this.enqueueCatalogMutation(id, async () => {
784
+ try {
785
+ await this.opts.storage.delete(this.catalogKey(id));
786
+ } catch (err) {
787
+ this.opts.logger.warn(
788
+ `[connecta] connector "${id}" catalog invalidation failed: ${msg(err)}`,
789
+ );
790
+ }
791
+ });
701
792
  }
702
793
  }
703
794
  }
@@ -771,6 +862,10 @@ export class ScopedRegistry implements RegistryView {
771
862
  return this.base.maxResultBytes;
772
863
  }
773
864
 
865
+ get maxBatchResultBytes(): number {
866
+ return this.base.maxBatchResultBytes;
867
+ }
868
+
774
869
  /** In scope AND actually registered. */
775
870
  private visible(id: string): boolean {
776
871
  return (