@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.
- package/CHANGELOG.md +99 -0
- package/dist/activity.d.ts +17 -0
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js.map +1 -1
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +101 -0
- package/dist/auth/clerk.js.map +1 -1
- package/dist/auth/downstream-oauth.d.ts +57 -20
- package/dist/auth/downstream-oauth.d.ts.map +1 -1
- package/dist/auth/downstream-oauth.js +275 -67
- package/dist/auth/downstream-oauth.js.map +1 -1
- package/dist/catalog.d.ts +5 -3
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +13 -4
- package/dist/catalog.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +165 -103
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +27 -15
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +4 -4
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +85 -25
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +24 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +87 -17
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +238 -19
- package/dist/server.js.map +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +8 -0
- package/dist/storage/file.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +7 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +118 -4
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/activity.ts +20 -0
- package/src/auth/clerk.ts +124 -0
- package/src/auth/downstream-oauth.ts +359 -68
- package/src/catalog.ts +24 -4
- package/src/connectors/remote-mcp.ts +172 -104
- package/src/execute.ts +29 -13
- package/src/index.ts +12 -1
- package/src/meta-tools.ts +89 -25
- package/src/registry.ts +115 -20
- package/src/server.ts +306 -16
- package/src/storage/file.ts +7 -0
- package/src/types.ts +23 -0
- package/src/ui.ts +124 -3
- 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(
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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 (
|
|
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.
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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 (
|