@zackbart/connecta 0.40.0 → 0.42.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 +148 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +32 -5
- package/dist/activity.js +32 -6
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +33 -44
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +15 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +77 -39
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +352 -43
- package/dist/catalog-freshness.js +25 -0
- package/dist/catalog-service.js +408 -78
- package/dist/catalog.js +216 -62
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +53 -21
- package/dist/config.js +56 -17
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +113 -22
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +14 -5
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +4 -2
- package/dist/describe-config.js +1 -0
- package/dist/discovery-health.js +26 -11
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +60 -4
- package/dist/errors.js +88 -3
- package/dist/execute.js +491 -202
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +3 -3
- package/dist/executors/quickjs-child.js +14 -5
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +37 -20
- package/dist/executors/quickjs.js +24 -7
- package/dist/guarded-transport.js +2 -0
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +14 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +11 -3
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +15 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +167 -64
- package/dist/membership.d.ts +1 -1
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +286 -73
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +177 -55
- package/dist/node.d.ts +7 -0
- package/dist/node.js +85 -4
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +104 -0
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +9 -1
- package/dist/operator-record.js +16 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +1 -14
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +2 -1
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +8 -3
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +2 -1
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +3 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +12 -6
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +3 -6
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +2 -1
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +2 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +3 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +142 -30
- package/dist/resource-read.js +21 -2
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +53 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +83 -35
- package/dist/routes/shared.d.ts +6 -0
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +38 -2
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +213 -57
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +16 -7
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +36 -0
- package/dist/url-safety.js +38 -20
- package/dist/usage-guide.js +14 -13
- package/dist/utility-recovery.js +33 -0
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +60 -15
- package/documentation/architecture.md +152 -43
- package/documentation/auth.md +160 -53
- package/documentation/code-mode.md +396 -119
- package/documentation/deploying.md +35 -10
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +761 -157
- package/documentation/operating.md +91 -4
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +34 -20
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +10 -8
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
package/dist/catalog-cache.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { traceFor } from "./timing-trace.js";
|
|
2
|
+
import { markCatalogGeneration } from "./catalog-freshness.js";
|
|
1
3
|
import { detach, runEdge } from "./runtime/run.js";
|
|
2
4
|
import { Deferred, Effect } from "effect";
|
|
3
5
|
import { authFlow, authCall, authInvoke, authValue, authMethod, authProgram, registerAuth, authStorage, authStorageMany, authMap, authAwait, } from "./auth/native.js";
|
|
@@ -236,6 +238,21 @@ export function observeCatalogExpiry(ctx, method, ttlMs, expiresAt, staleFallbac
|
|
|
236
238
|
// failure for five minutes. Resources never use stale inventories. A fallback
|
|
237
239
|
// carries its original expired deadline and cannot authorize auth recovery.
|
|
238
240
|
const STALE_FALLBACK_MS = 300_000;
|
|
241
|
+
/**
|
|
242
|
+
* How long past its expiry a complete tool catalog stays listable when its
|
|
243
|
+
* connector does not answer discovery in time. The listing only: a call always
|
|
244
|
+
* resolves through STALE_FALLBACK_MS at most. One day matches the longest TTL a
|
|
245
|
+
* deployment can configure, so this retains nothing it could not already hold.
|
|
246
|
+
* A refusal retires it sooner, for that credential: see `fenceTools`.
|
|
247
|
+
*/
|
|
248
|
+
const STALE_DISCOVERY_MS = 86_400_000;
|
|
249
|
+
/** A fence outlives every entry it could be keeping from reuse. */
|
|
250
|
+
const CATALOG_FENCE_TTL_SECONDS = (MAX_CACHE_TTL_MS + STALE_DISCOVERY_MS) / 1000;
|
|
251
|
+
/** What a refusal's fence is keyed by, beside the catalog: one credential's slot within one generation. */
|
|
252
|
+
const catalogFenceFingerprint = (sharing, partition, authPartition, credentialIdentity) => authCall(fingerprintSerializedCatalog, [
|
|
253
|
+
// A shared credential gets one answer whoever asks, so one fence covers all its callers.
|
|
254
|
+
JSON.stringify(["fence", sharing === "shared" ? authPartition : partition, credentialIdentity ?? null]),
|
|
255
|
+
]);
|
|
239
256
|
/** Catalog chunks one storage read fetches together. */
|
|
240
257
|
const CHUNKS_PER_READ = 8;
|
|
241
258
|
export function attachCatalogCache(ctx, value) {
|
|
@@ -446,6 +463,51 @@ function rememberValidated(key, value, bytes) {
|
|
|
446
463
|
validatedCatalogs.set(key, { value, bytes });
|
|
447
464
|
validatedBytes += bytes;
|
|
448
465
|
}
|
|
466
|
+
const catalogLocation = authMethod((ctx, config, authPartition) => authFlow(function* () {
|
|
467
|
+
const policy = serviceFor(ctx, CatalogPolicy);
|
|
468
|
+
const partition = JSON.stringify([
|
|
469
|
+
policy?.partition ?? JSON.stringify(callerOf(ctx) ?? null),
|
|
470
|
+
ctx.baseUrl,
|
|
471
|
+
ctx.publicUrl,
|
|
472
|
+
authPartition,
|
|
473
|
+
]);
|
|
474
|
+
const sharedPartition = JSON.stringify([
|
|
475
|
+
"host-shared",
|
|
476
|
+
policy?.sharedPartition ?? JSON.stringify([ctx.baseUrl, ctx.publicUrl, callerOf(ctx)?.pool]),
|
|
477
|
+
authPartition,
|
|
478
|
+
]);
|
|
479
|
+
const configHash = (yield authCall(fingerprintSerializedCatalog, [
|
|
480
|
+
JSON.stringify([
|
|
481
|
+
config,
|
|
482
|
+
policy?.defaultTtlMs ?? 300000,
|
|
483
|
+
policy?.minTtlMs ?? 0,
|
|
484
|
+
policy?.maxTtlMs ?? MAX_CACHE_TTL_MS,
|
|
485
|
+
]),
|
|
486
|
+
])).fingerprint;
|
|
487
|
+
return { partition, sharedPartition, configHash };
|
|
488
|
+
}));
|
|
489
|
+
/** Prefetch candidates only; authority, identity and acceptance stay fresh. */
|
|
490
|
+
export const prefetchCatalog = authMethod((ctx, connectorId, config, authPartition, sharing, credentialIdentity, negotiationKey) => authFlow(function* () {
|
|
491
|
+
const { partition, sharedPartition, configHash } = yield authCall(catalogLocation, [ctx, config, authPartition]);
|
|
492
|
+
const slots = sharing === "shared" ? [partition, sharedPartition] : [partition];
|
|
493
|
+
const suffixes = [
|
|
494
|
+
(yield catalogFenceFingerprint(sharing, partition, authPartition, credentialIdentity)).fingerprint,
|
|
495
|
+
];
|
|
496
|
+
for (const slot of slots)
|
|
497
|
+
suffixes.push((yield authCall(fingerprintSerializedCatalog, [
|
|
498
|
+
JSON.stringify(["tools/list", slot, credentialIdentity ?? null]),
|
|
499
|
+
])).fingerprint);
|
|
500
|
+
const storage = serviceFor(ctx, CatalogPolicy)?.storage ?? ctx.storage;
|
|
501
|
+
yield ownerReads(Context.get(viewServices(ctx), RequestScope)).prefetch([
|
|
502
|
+
{ storage: ctx.storage, key: negotiationKey },
|
|
503
|
+
{ storage, key: responseCacheKeys.generation(connectorId) },
|
|
504
|
+
], (storage, keys) => authStorageMany(storage, keys), {
|
|
505
|
+
storage,
|
|
506
|
+
generationKey: responseCacheKeys.generation(connectorId),
|
|
507
|
+
prefix: responseCacheKeys.locatorPrefix(connectorId, configHash),
|
|
508
|
+
suffixes,
|
|
509
|
+
});
|
|
510
|
+
}));
|
|
449
511
|
/** Keys ignore self-reported serverInfo and bind the configured connector instead. */
|
|
450
512
|
export function catalogClientOptions(ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity, connectionless = false) {
|
|
451
513
|
return runEdge(authProgram(catalogClientOptionsNative, [
|
|
@@ -475,29 +537,11 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
475
537
|
// canonical request owner, never the initiating call's live services.
|
|
476
538
|
const cacheMethod = (program) => authMethod((...args) => program(...args).pipe(Effect.provideService(RequestScope, request)));
|
|
477
539
|
const policy = serviceFor(ctx, CatalogPolicy);
|
|
478
|
-
const partition =
|
|
479
|
-
policy?.partition ?? JSON.stringify(callerOf(ctx) ?? null),
|
|
480
|
-
ctx.baseUrl,
|
|
481
|
-
ctx.publicUrl,
|
|
482
|
-
authPartition,
|
|
483
|
-
]);
|
|
484
|
-
const sharedPartition = JSON.stringify([
|
|
485
|
-
"host-shared",
|
|
486
|
-
policy?.sharedPartition ?? JSON.stringify([ctx.baseUrl, ctx.publicUrl, callerOf(ctx)?.pool]),
|
|
487
|
-
authPartition,
|
|
488
|
-
]);
|
|
540
|
+
const { partition, sharedPartition, configHash } = yield authCall(catalogLocation, [ctx, config, authPartition]);
|
|
489
541
|
const storage = policy?.storage ?? ctx.storage;
|
|
490
542
|
let storeIdentity = catalogStores.get(storage);
|
|
491
543
|
if (!storeIdentity)
|
|
492
544
|
catalogStores.set(storage, (storeIdentity = crypto.randomUUID()));
|
|
493
|
-
const configHash = (yield authCall(fingerprintSerializedCatalog, [
|
|
494
|
-
JSON.stringify([
|
|
495
|
-
config,
|
|
496
|
-
policy?.defaultTtlMs ?? 300000,
|
|
497
|
-
policy?.minTtlMs ?? 0,
|
|
498
|
-
policy?.maxTtlMs ?? MAX_CACHE_TTL_MS,
|
|
499
|
-
]),
|
|
500
|
-
])).fingerprint;
|
|
501
545
|
const scope = cacheScope(ctx, connectorId, owner);
|
|
502
546
|
let unavailable = false;
|
|
503
547
|
const connectionGeneration = {};
|
|
@@ -597,6 +641,58 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
597
641
|
])).fingerprint;
|
|
598
642
|
return stopped() ? undefined : responseCacheKeys.entry(prefix, digest);
|
|
599
643
|
}));
|
|
644
|
+
const fenceKey = cacheMethod((operation, generation) => authFlow(function* () {
|
|
645
|
+
const { fingerprint } = (yield catalogFenceFingerprint(sharing, partition, authPartition, operation.credentialIdentity));
|
|
646
|
+
return responseCacheKeys.entry(responseCacheKeys.namespace(connectorId, configHash, generation), fingerprint);
|
|
647
|
+
}));
|
|
648
|
+
// Whether an expired catalog may stand in now. Read past this request's
|
|
649
|
+
// memo: a refusal or invalidation in another isolate retires it at once.
|
|
650
|
+
const staleAllowed = cacheMethod((operation) => authFlow(function* () {
|
|
651
|
+
if (operation.stopped() || !(yield authCall(namespace, [operation])))
|
|
652
|
+
return false;
|
|
653
|
+
const key = (yield authCall(fenceKey, [operation, operation.generation.value]));
|
|
654
|
+
const fence = (yield authCall(operation.io, [
|
|
655
|
+
cacheMethod(() => reads.read(storage, key, authStorage(storage, "get", [key]), true)),
|
|
656
|
+
]));
|
|
657
|
+
return fence === null && !operation.stopped();
|
|
658
|
+
}));
|
|
659
|
+
// Read before the listing dispatches: a refusal after this point writes a
|
|
660
|
+
// value this listing never saw. An older memoized read only lifts less.
|
|
661
|
+
const readListingFence = cacheMethod((operation) => authFlow(function* () {
|
|
662
|
+
const { listing, stopped, io } = operation;
|
|
663
|
+
if (!listing || listing.fence !== undefined || stopped())
|
|
664
|
+
return;
|
|
665
|
+
// A generation claimed later holds no fence this listing could know of.
|
|
666
|
+
if (!listing.generationStored) {
|
|
667
|
+
listing.fence = null;
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const key = (yield authCall(fenceKey, [operation, listing.generation.value]));
|
|
671
|
+
listing.fence = (yield authCall(io, [
|
|
672
|
+
cacheMethod(() => reads.read(storage, key, authStorage(storage, "get", [key]))),
|
|
673
|
+
]));
|
|
674
|
+
}));
|
|
675
|
+
// A complete listing's answer lifts the fence captured before it was
|
|
676
|
+
// asked, and only that one: a refusal that landed while it ran wrote
|
|
677
|
+
// another, which stays. With nothing captured it lifts nothing, so the
|
|
678
|
+
// next listing that reads the fence first does it instead.
|
|
679
|
+
const liftFence = cacheMethod((operation) => authFlow(function* () {
|
|
680
|
+
const { listing, stopped, io } = operation;
|
|
681
|
+
if (!listing?.fence || stopped())
|
|
682
|
+
return;
|
|
683
|
+
try {
|
|
684
|
+
const key = (yield authCall(fenceKey, [operation, operation.generation.value]));
|
|
685
|
+
const lifted = (yield authCall(io, [
|
|
686
|
+
cacheMethod(() => authStorage(storage, "compareAndSet", [key, listing.fence, null])),
|
|
687
|
+
]));
|
|
688
|
+
if (lifted)
|
|
689
|
+
listing.fence = null;
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
if (!stopped())
|
|
693
|
+
logFailure(ctx.logger, "catalog fence failed", failureRecord({ connector: connectorId }, error));
|
|
694
|
+
}
|
|
695
|
+
}));
|
|
600
696
|
const manifest = (raw) => {
|
|
601
697
|
if (!raw)
|
|
602
698
|
return undefined;
|
|
@@ -676,6 +772,8 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
676
772
|
get: cacheMethod((key) => authFlow(function* () {
|
|
677
773
|
const operation = capture();
|
|
678
774
|
const { ctx, stopped, io } = operation;
|
|
775
|
+
const trace = traceFor(ctx.requestScope);
|
|
776
|
+
trace?.record("catalog.cache.lookup");
|
|
679
777
|
const root = (yield authCall(address, [key, operation]));
|
|
680
778
|
if (!root || stopped())
|
|
681
779
|
return undefined;
|
|
@@ -692,10 +790,18 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
692
790
|
]));
|
|
693
791
|
roots.push(responseCacheKeys.entry(responseCacheKeys.namespace(connectorId, configHash, operation.generation.value), fingerprint));
|
|
694
792
|
}
|
|
695
|
-
const
|
|
793
|
+
const fence = (yield authCall(fenceKey, [operation, operation.generation.value]));
|
|
794
|
+
yield reads.prefetch([...roots, fence].map((key) => ({ storage, key })), (storage, keys) => authStorageMany(storage, keys));
|
|
795
|
+
(yield authCall(readListingFence, [operation]));
|
|
796
|
+
const values = [];
|
|
797
|
+
for (const root of roots)
|
|
798
|
+
values.push(yield reads.read(storage, root, authStorage(storage, "get", [root])));
|
|
696
799
|
listing.manifests ??= new Map();
|
|
697
800
|
roots.forEach((root, i) => listing.manifests.set(root, values[i] ?? null));
|
|
698
801
|
}
|
|
802
|
+
// A refused credential is listed anew, whatever its catalog's age.
|
|
803
|
+
if (listing?.fence && key.method === "tools/list")
|
|
804
|
+
return undefined;
|
|
699
805
|
const m = manifest(listing?.manifests?.has(root)
|
|
700
806
|
? listing.manifests.get(root)
|
|
701
807
|
: (yield authCall(io, [
|
|
@@ -704,7 +810,7 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
704
810
|
if (stopped() ||
|
|
705
811
|
!m ||
|
|
706
812
|
(m.expiresAt <= Date.now() &&
|
|
707
|
-
(key.method !== "tools/list" || m.expiresAt +
|
|
813
|
+
(key.method !== "tools/list" || m.expiresAt + STALE_DISCOVERY_MS <= Date.now())))
|
|
708
814
|
return undefined;
|
|
709
815
|
// Up to CHUNKS_PER_READ chunks per round trip, so one answer stays a
|
|
710
816
|
// few megabytes; the generation fence below still decides whether
|
|
@@ -735,8 +841,10 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
735
841
|
// Hash the chunks again above, never trust the manifest alone.
|
|
736
842
|
const reuseKey = JSON.stringify([storeIdentity, root, partition, m.fingerprint]);
|
|
737
843
|
let accepted = validatedCatalogs.get(reuseKey)?.value;
|
|
844
|
+
trace?.record(accepted ? "catalog.validated.hit" : "catalog.validated.miss");
|
|
738
845
|
if (!accepted) {
|
|
739
846
|
const result = JSON.parse(value);
|
|
847
|
+
trace?.record("catalog.decode", 0, fingerprint.byteLength);
|
|
740
848
|
const items = catalogItems(method, result);
|
|
741
849
|
if (!Array.isArray(items) ||
|
|
742
850
|
items.length > MAX_CATALOG_TOOLS ||
|
|
@@ -744,6 +852,7 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
744
852
|
(result.resultType !== undefined && result.resultType !== "complete"))
|
|
745
853
|
return undefined;
|
|
746
854
|
const validated = (yield authInvoke(catalogSchema(method)["~standard"], "validate", [result]));
|
|
855
|
+
trace?.record("catalog.validate");
|
|
747
856
|
if (stopped() || validated.issues)
|
|
748
857
|
return undefined;
|
|
749
858
|
accepted = validated.value;
|
|
@@ -751,12 +860,16 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
751
860
|
// Redaction depends on this request's sent credentials. It must
|
|
752
861
|
// still run, and returns detached objects from the frozen facts.
|
|
753
862
|
const clean = intake(ctx, method, accepted);
|
|
863
|
+
trace?.record("catalog.intake");
|
|
864
|
+
trace?.record("catalog.chunks", 0, fingerprint.byteLength, 0, 0, m.chunkCount);
|
|
754
865
|
rememberValidated(reuseKey, accepted, fingerprint.byteLength);
|
|
755
866
|
if (m.expiresAt <= Date.now()) {
|
|
867
|
+
trace?.record("catalog.cache.expired");
|
|
756
868
|
if (method === "tools/list" && (!staleTools || staleTools.manifest.fetchedAt < m.fetchedAt))
|
|
757
869
|
staleTools = { result: clean, manifest: m };
|
|
758
870
|
return undefined;
|
|
759
871
|
}
|
|
872
|
+
trace?.record("catalog.cache.hit");
|
|
760
873
|
observeCatalogFetch(ctx, m.fetchedAt, method);
|
|
761
874
|
observeCatalogExpiry(ctx, method, clean.ttlMs, m.expiresAt);
|
|
762
875
|
return { value: JSON.stringify(clean), stamp: m.stamp, expiresAt: m.expiresAt, scope: m.scope };
|
|
@@ -775,6 +888,8 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
775
888
|
(yield authCall(io, [
|
|
776
889
|
cacheMethod(() => authStorage(storage, "delete", [root])),
|
|
777
890
|
]));
|
|
891
|
+
if (method === "tools/list")
|
|
892
|
+
(yield authCall(liftFence, [operation]));
|
|
778
893
|
return 0;
|
|
779
894
|
}
|
|
780
895
|
if (!Array.isArray(catalogItems(method, result)) ||
|
|
@@ -794,7 +909,7 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
794
909
|
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
795
910
|
let offset = 0;
|
|
796
911
|
let chunkCount = 0;
|
|
797
|
-
const ttlSeconds = () => Math.max(0.001, (expiresAt + (method === "tools/list" ?
|
|
912
|
+
const ttlSeconds = () => Math.max(0.001, (expiresAt + (method === "tools/list" ? STALE_DISCOVERY_MS : 0) - Date.now()) / 1000);
|
|
798
913
|
while (offset < bytes.length) {
|
|
799
914
|
if (stopped() || expiresAt <= Date.now())
|
|
800
915
|
return 0;
|
|
@@ -846,6 +961,27 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
846
961
|
return 0;
|
|
847
962
|
if (published) {
|
|
848
963
|
lastStamp = stamp;
|
|
964
|
+
// Tool chunks outlive their manifest by the stale limit, so a
|
|
965
|
+
// superseded revision is removed now rather than retained for
|
|
966
|
+
// a day. A reader holding it misses and lists again; a failed
|
|
967
|
+
// removal leaves the chunks to their TTL.
|
|
968
|
+
const superseded = manifest(previous);
|
|
969
|
+
if (superseded && superseded.revision !== revision) {
|
|
970
|
+
try {
|
|
971
|
+
for (let index = 0; index < superseded.chunkCount && !stopped(); index++)
|
|
972
|
+
(yield authCall(io, [
|
|
973
|
+
cacheMethod(() => authStorage(storage, "delete", [
|
|
974
|
+
responseCacheKeys.chunk(root, superseded.revision, index),
|
|
975
|
+
])),
|
|
976
|
+
]));
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
if (!stopped())
|
|
980
|
+
logFailure(ctx.logger, "catalog chunk cleanup failed", failureRecord({ connector: connectorId }, error));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (method === "tools/list")
|
|
984
|
+
(yield authCall(liftFence, [operation]));
|
|
849
985
|
return stamp;
|
|
850
986
|
}
|
|
851
987
|
}
|
|
@@ -891,7 +1027,7 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
891
1027
|
// The queue retains only a Promise turn. Awaiting it in this caller's
|
|
892
1028
|
// request preserves workerd I/O ownership; the listing program stays native.
|
|
893
1029
|
let tail = Promise.resolve();
|
|
894
|
-
const withListing = cacheMethod((listingCtx, read) => authFlow(function* () {
|
|
1030
|
+
const withListing = cacheMethod((listingCtx, read, bound) => authFlow(function* () {
|
|
895
1031
|
const previous = tail;
|
|
896
1032
|
let release;
|
|
897
1033
|
tail = new Promise((resolve) => {
|
|
@@ -912,9 +1048,17 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
912
1048
|
staleTools = undefined;
|
|
913
1049
|
const operation = capture();
|
|
914
1050
|
try {
|
|
915
|
-
if (
|
|
1051
|
+
if (bound && "credentialIdentity" in bound)
|
|
1052
|
+
listing.credentialIdentity = bound.credentialIdentity;
|
|
1053
|
+
else if (resolveCredentialIdentity)
|
|
916
1054
|
listing.credentialIdentity = yield authInvoke(operation, "io", [resolveCredentialIdentity]);
|
|
917
|
-
if (
|
|
1055
|
+
if (bound?.fence !== undefined)
|
|
1056
|
+
listing.fence = bound.fence;
|
|
1057
|
+
if (bound?.generationStored !== undefined) {
|
|
1058
|
+
listing.generationStored = bound.generationStored;
|
|
1059
|
+
listing.generation.value = bound.generation;
|
|
1060
|
+
}
|
|
1061
|
+
else if (!operation.stopped()) {
|
|
918
1062
|
try {
|
|
919
1063
|
// Memoized: it locates the listing's entries, which the
|
|
920
1064
|
// fresh fence in get accepts or refuses.
|
|
@@ -933,7 +1077,9 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
933
1077
|
logFailure(listingCtx.logger, "catalog read failed", failureRecord({ connector: connectorId }, error));
|
|
934
1078
|
}
|
|
935
1079
|
}
|
|
936
|
-
|
|
1080
|
+
const result = yield authCall(read, []);
|
|
1081
|
+
markCatalogGeneration(listingCtx.requestScope, connectorId, listing.generation.value);
|
|
1082
|
+
return result;
|
|
937
1083
|
}
|
|
938
1084
|
finally {
|
|
939
1085
|
listing.ended = true;
|
|
@@ -948,6 +1094,24 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
948
1094
|
void previous.then(release);
|
|
949
1095
|
}
|
|
950
1096
|
}));
|
|
1097
|
+
// Capture what an attempt is about to run under. `pinned` names a
|
|
1098
|
+
// credential the caller has already read for it.
|
|
1099
|
+
const bindListing = cacheMethod((listingCtx, pinned, readFence = false) => authCall(withListing, [
|
|
1100
|
+
listingCtx,
|
|
1101
|
+
cacheMethod(() => authFlow(function* () {
|
|
1102
|
+
const operation = capture();
|
|
1103
|
+
if (readFence)
|
|
1104
|
+
(yield authCall(readListingFence, [operation]));
|
|
1105
|
+
const listing = operation.listing;
|
|
1106
|
+
return {
|
|
1107
|
+
credentialIdentity: listing.credentialIdentity,
|
|
1108
|
+
generation: listing.generation.value,
|
|
1109
|
+
generationStored: listing.generationStored ?? false,
|
|
1110
|
+
fence: listing.fence,
|
|
1111
|
+
};
|
|
1112
|
+
})),
|
|
1113
|
+
pinned,
|
|
1114
|
+
]));
|
|
951
1115
|
// A complete, unexpired tools/list entry, read before any connection
|
|
952
1116
|
// exists. This is the SDK's own two-probe read: the private slot
|
|
953
1117
|
// `[serverIdentity, cachePartition]`, then a shared connector's public
|
|
@@ -976,12 +1140,101 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
976
1140
|
return Array.isArray(result.tools) ? result : undefined;
|
|
977
1141
|
})),
|
|
978
1142
|
]));
|
|
1143
|
+
// The newest complete tools/list entry this caller could be served, fresh
|
|
1144
|
+
// or within STALE_DISCOVERY_MS of expiry, with its original times. Reads
|
|
1145
|
+
// only, through the same address, fingerprint, generation and credential
|
|
1146
|
+
// fences as a listing, so another owner's entry, an invalidated generation
|
|
1147
|
+
// or a rotated credential is a miss. Never a source for call resolution.
|
|
1148
|
+
const peekTools = cacheMethod((listingCtx, serverIdentity = "") => authCall(withListing, [
|
|
1149
|
+
listingCtx,
|
|
1150
|
+
cacheMethod(() => authFlow(function* () {
|
|
1151
|
+
if (!active?.generationStored)
|
|
1152
|
+
return undefined;
|
|
1153
|
+
const read = (slot) => authInvoke(store, "get", [
|
|
1154
|
+
{ method: "tools/list", params: "", partition: JSON.stringify([serverIdentity, slot]) },
|
|
1155
|
+
]);
|
|
1156
|
+
let entry = (yield read(partition));
|
|
1157
|
+
let stale = staleTools;
|
|
1158
|
+
if (entry === undefined && sharing === "shared") {
|
|
1159
|
+
staleTools = undefined;
|
|
1160
|
+
const shared = (yield read(""));
|
|
1161
|
+
entry = shared?.scope === "public" ? shared : undefined;
|
|
1162
|
+
// Set by the read above, which control flow analysis cannot see.
|
|
1163
|
+
const sharedStale = staleTools;
|
|
1164
|
+
if (sharedStale?.manifest.scope === "public" &&
|
|
1165
|
+
(!stale || stale.manifest.fetchedAt < sharedStale.manifest.fetchedAt))
|
|
1166
|
+
stale = sharedStale;
|
|
1167
|
+
}
|
|
1168
|
+
if (entry?.expiresAt !== undefined && entry.expiresAt > Date.now()) {
|
|
1169
|
+
const result = JSON.parse(entry.value);
|
|
1170
|
+
if (!Array.isArray(result.tools))
|
|
1171
|
+
return undefined;
|
|
1172
|
+
return {
|
|
1173
|
+
result,
|
|
1174
|
+
fetchedAt: catalogFetchedAt(listingCtx) ?? Date.now(),
|
|
1175
|
+
expiresAt: entry.expiresAt,
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
const operation = capture();
|
|
1179
|
+
if (!stale ||
|
|
1180
|
+
!(yield authCall(staleAllowed, [operation])) ||
|
|
1181
|
+
stale.manifest.expiresAt + STALE_DISCOVERY_MS <= Date.now())
|
|
1182
|
+
return undefined;
|
|
1183
|
+
return { result: stale.result, fetchedAt: stale.manifest.fetchedAt, expiresAt: stale.manifest.expiresAt };
|
|
1184
|
+
})),
|
|
1185
|
+
]));
|
|
1186
|
+
// The downstream refused this credential, which says more than a TTL can:
|
|
1187
|
+
// nothing cached for it may stand in again until a listing succeeds. One
|
|
1188
|
+
// key beside the catalog, durable across requests and isolates, in the
|
|
1189
|
+
// namespace of the generation the refused attempt ran under, so a change
|
|
1190
|
+
// of credential or a disconnect discards it with the entries it guards. An
|
|
1191
|
+
// attempt that found no generation fences the one claimed since, if any:
|
|
1192
|
+
// that is where a listing beside it published. Nothing is stored while no
|
|
1193
|
+
// generation is, since then nothing is cached to retire. A failed write is
|
|
1194
|
+
// logged and never masks the refusal.
|
|
1195
|
+
const writeFence = cacheMethod((operation) => authFlow(function* () {
|
|
1196
|
+
const { listing, stopped, io } = operation;
|
|
1197
|
+
if (!listing || stopped())
|
|
1198
|
+
return;
|
|
1199
|
+
try {
|
|
1200
|
+
const generation = listing.generationStored
|
|
1201
|
+
? listing.generation.value
|
|
1202
|
+
: (yield authCall(io, [cacheMethod(() => generationRead(false))]));
|
|
1203
|
+
if (!generation || stopped())
|
|
1204
|
+
return;
|
|
1205
|
+
const key = (yield authCall(fenceKey, [operation, generation]));
|
|
1206
|
+
(yield authCall(io, [
|
|
1207
|
+
cacheMethod(() => authStorage(storage, "set", [
|
|
1208
|
+
key,
|
|
1209
|
+
crypto.randomUUID(),
|
|
1210
|
+
{ ttlSeconds: CATALOG_FENCE_TTL_SECONDS },
|
|
1211
|
+
])),
|
|
1212
|
+
]));
|
|
1213
|
+
}
|
|
1214
|
+
catch (error) {
|
|
1215
|
+
if (!stopped())
|
|
1216
|
+
logFailure(ctx.logger, "catalog fence failed", failureRecord({ connector: connectorId }, error));
|
|
1217
|
+
}
|
|
1218
|
+
}));
|
|
1219
|
+
// The refusal of the listing now running, as that listing was bound.
|
|
1220
|
+
const fenceListing = cacheMethod(() => authCall(writeFence, [capture()]));
|
|
1221
|
+
// The refusal of an attempt bound earlier, outside any listing.
|
|
1222
|
+
const fenceTools = cacheMethod((listingCtx, bound) => authCall(withListing, [listingCtx, cacheMethod(() => authCall(writeFence, [capture()])), bound]));
|
|
1223
|
+
// For a listing that never reads the cache before it dispatches.
|
|
1224
|
+
const captureFence = cacheMethod(() => authFlow(function* () {
|
|
1225
|
+
const operation = capture();
|
|
1226
|
+
try {
|
|
1227
|
+
(yield authCall(readListingFence, [operation]));
|
|
1228
|
+
}
|
|
1229
|
+
catch (error) {
|
|
1230
|
+
if (!operation.stopped())
|
|
1231
|
+
logFailure(ctx.logger, "catalog fence failed", failureRecord({ connector: connectorId }, error));
|
|
1232
|
+
}
|
|
1233
|
+
}));
|
|
979
1234
|
const fallbackTools = cacheMethod(() => authFlow(function* () {
|
|
980
1235
|
const operation = capture();
|
|
981
1236
|
if (!staleTools ||
|
|
982
|
-
operation
|
|
983
|
-
!(yield authCall(namespace, [operation])) ||
|
|
984
|
-
operation.stopped() ||
|
|
1237
|
+
!(yield authCall(staleAllowed, [operation])) ||
|
|
985
1238
|
staleTools.manifest.expiresAt + STALE_FALLBACK_MS <= Date.now())
|
|
986
1239
|
return;
|
|
987
1240
|
const { result, manifest } = staleTools;
|
|
@@ -994,6 +1247,11 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
994
1247
|
completeCatalogRefresh,
|
|
995
1248
|
fallbackTools,
|
|
996
1249
|
cachedTools,
|
|
1250
|
+
peekTools,
|
|
1251
|
+
bindListing,
|
|
1252
|
+
captureFence,
|
|
1253
|
+
fenceListing,
|
|
1254
|
+
fenceTools,
|
|
997
1255
|
responseCacheStore: store,
|
|
998
1256
|
cachePartition: partition,
|
|
999
1257
|
defaultCacheTtlMs: policy?.defaultTtlMs ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds * 1000,
|
|
@@ -1005,36 +1263,54 @@ function isCatalogMethod(method) {
|
|
|
1005
1263
|
return method === "tools/list" || method === "resources/list" || method === "resources/templates/list";
|
|
1006
1264
|
}
|
|
1007
1265
|
function customCatalogCache(ctx, connectorId) {
|
|
1008
|
-
return runEdge(authProgram(customCatalogCacheNative, [
|
|
1009
|
-
ctx,
|
|
1010
|
-
connectorId,
|
|
1011
|
-
]));
|
|
1266
|
+
return runEdge(authProgram(customCatalogCacheNative, [ctx, connectorId]));
|
|
1012
1267
|
}
|
|
1013
|
-
const
|
|
1268
|
+
const customCatalogAuth = authMethod((ctx) => authFlow(function* () {
|
|
1014
1269
|
const credential = (yield authValue(() => ctx.credential?.getAll()));
|
|
1015
1270
|
// Credential values enter the stored partition only through a keyed digest.
|
|
1016
|
-
|
|
1271
|
+
return credential == null
|
|
1017
1272
|
? (yield authCall(fingerprintSerializedCatalog, ["null"])).fingerprint
|
|
1018
1273
|
: (yield authCall(secretDigest, [ctx, JSON.stringify(credential)]));
|
|
1274
|
+
}));
|
|
1275
|
+
const customCatalogCacheNative = /* @__PURE__ */ registerAuth(customCatalogCache, (ctx, connectorId) => authFlow(function* () {
|
|
1276
|
+
const auth = (yield authCall(customCatalogAuth, [ctx]));
|
|
1019
1277
|
const cache = (yield authCall(catalogClientOptions, [ctx, connectorId, "custom-connector", auth]));
|
|
1020
1278
|
const key = {
|
|
1021
1279
|
method: "tools/list",
|
|
1022
1280
|
partition: JSON.stringify(["custom-connector", cache.cachePartition]),
|
|
1023
1281
|
};
|
|
1024
|
-
return { cache, key };
|
|
1282
|
+
return { cache, key, auth };
|
|
1025
1283
|
}));
|
|
1026
|
-
/**
|
|
1027
|
-
*
|
|
1028
|
-
|
|
1029
|
-
|
|
1284
|
+
/**
|
|
1285
|
+
* Before a custom listing dispatches: only an authenticated owner may retain a
|
|
1286
|
+
* complete private fallback, and unscoped/operator reads start no cache I/O.
|
|
1287
|
+
* Its answer is recorded against this ticket, never a later read of the store.
|
|
1288
|
+
*/
|
|
1289
|
+
export function bindCustomCatalogListing(ctx, connectorId) {
|
|
1290
|
+
return runEdge(authProgram(bindCustomCatalogListingNative, [ctx, connectorId]));
|
|
1291
|
+
}
|
|
1292
|
+
const bindCustomCatalogListingNative = /* @__PURE__ */ registerAuth(bindCustomCatalogListing, (ctx, connectorId) => authFlow(function* () {
|
|
1293
|
+
if (!catalogTtlMs(ctx, undefined) || !callerOf(ctx)?.authenticated)
|
|
1294
|
+
return;
|
|
1295
|
+
const custom = (yield authCall(customCatalogCache, [ctx, connectorId]));
|
|
1296
|
+
const bound = (yield authInvoke(custom.cache, "bindListing", [ctx, undefined, true]));
|
|
1297
|
+
return { ...custom, bound };
|
|
1298
|
+
}));
|
|
1299
|
+
/** Custom listings always fetch; the ticket says whether and where one is retained. */
|
|
1300
|
+
export function storeCustomCatalogFallback(ctx, tools, listing) {
|
|
1301
|
+
return runEdge(authProgram(storeCustomCatalogFallbackNative, [ctx, tools, listing]));
|
|
1030
1302
|
}
|
|
1031
|
-
const storeCustomCatalogFallbackNative = /* @__PURE__ */ registerAuth(storeCustomCatalogFallback, (ctx,
|
|
1303
|
+
const storeCustomCatalogFallbackNative = /* @__PURE__ */ registerAuth(storeCustomCatalogFallback, (ctx, tools, listing) => authFlow(function* () {
|
|
1032
1304
|
const ttlMs = catalogTtlMs(ctx, undefined);
|
|
1033
1305
|
observeCatalogFetch(ctx, Date.now());
|
|
1034
1306
|
observeCatalogExpiry(ctx, "tools/list", ttlMs);
|
|
1035
|
-
if (!ttlMs || !
|
|
1307
|
+
if (!ttlMs || !listing)
|
|
1036
1308
|
return;
|
|
1037
|
-
|
|
1309
|
+
// The connector read its own credential, so a change while it ran leaves
|
|
1310
|
+
// this answer belonging to neither: it is retained for neither.
|
|
1311
|
+
if ((yield authCall(customCatalogAuth, [ctx])) !== listing.auth)
|
|
1312
|
+
return;
|
|
1313
|
+
const { cache, key, bound } = listing;
|
|
1038
1314
|
(yield authInvoke(cache, "withListing", [
|
|
1039
1315
|
ctx,
|
|
1040
1316
|
authMethod(() => authFlow(function* () {
|
|
@@ -1055,8 +1331,41 @@ const storeCustomCatalogFallbackNative = /* @__PURE__ */ registerAuth(storeCusto
|
|
|
1055
1331
|
},
|
|
1056
1332
|
]));
|
|
1057
1333
|
})),
|
|
1334
|
+
bound,
|
|
1058
1335
|
]));
|
|
1059
1336
|
}));
|
|
1337
|
+
/** A custom connector's retained catalog, for discovery only. Same owner rule as its fallback. */
|
|
1338
|
+
export function customCatalogPeek(ctx, connectorId) {
|
|
1339
|
+
return runEdge(authProgram(customCatalogPeekNative, [ctx, connectorId]));
|
|
1340
|
+
}
|
|
1341
|
+
const customCatalogPeekNative = /* @__PURE__ */ registerAuth(customCatalogPeek, (ctx, connectorId) => authFlow(function* () {
|
|
1342
|
+
if (!callerOf(ctx)?.authenticated)
|
|
1343
|
+
return;
|
|
1344
|
+
const { cache } = (yield authCall(customCatalogCache, [ctx, connectorId]));
|
|
1345
|
+
const entry = (yield authInvoke(cache, "peekTools", [ctx, "custom-connector"]));
|
|
1346
|
+
return entry
|
|
1347
|
+
? { tools: entry.result.tools, fetchedAt: entry.fetchedAt, expiresAt: entry.expiresAt }
|
|
1348
|
+
: undefined;
|
|
1349
|
+
}));
|
|
1350
|
+
/**
|
|
1351
|
+
* A custom connector's refusal. It reads its own credential, by any route, so
|
|
1352
|
+
* only one unchanged since the ticket identifies what was refused: that
|
|
1353
|
+
* catalog stands in for nothing until a listing succeeds. A change, or a failed
|
|
1354
|
+
* read, leaves it unknown, so every catalog retained for this connector goes.
|
|
1355
|
+
*/
|
|
1356
|
+
export function fenceCustomCatalog(ctx, connectorId, listing) {
|
|
1357
|
+
return runEdge(authProgram(fenceCustomCatalogNative, [ctx, connectorId, listing]));
|
|
1358
|
+
}
|
|
1359
|
+
const fenceCustomCatalogNative = /* @__PURE__ */ registerAuth(fenceCustomCatalog, (ctx, connectorId, listing) => authFlow(function* () {
|
|
1360
|
+
const current = (yield authCall(customCatalogAuth, [ctx]).pipe(Effect.catch(() => Effect.succeed(undefined))));
|
|
1361
|
+
if (current === listing.auth)
|
|
1362
|
+
(yield authInvoke(listing.cache, "fenceTools", [ctx, listing.bound]));
|
|
1363
|
+
else
|
|
1364
|
+
(yield authCall(invalidateCatalogCache, [
|
|
1365
|
+
serviceFor(ctx, CatalogPolicy)?.storage ?? ctx.storage,
|
|
1366
|
+
connectorId,
|
|
1367
|
+
]));
|
|
1368
|
+
}));
|
|
1060
1369
|
export function customCatalogFallback(ctx, connectorId) {
|
|
1061
1370
|
return runEdge(authProgram(customCatalogFallbackNative, [ctx, connectorId]));
|
|
1062
1371
|
}
|
|
@@ -6,8 +6,33 @@ export function markCatalogFreshness(tools, freshUntil) {
|
|
|
6
6
|
return tools;
|
|
7
7
|
}
|
|
8
8
|
export function carryCatalogFreshness(source, copy) {
|
|
9
|
+
const stale = staleCatalogs.get(source);
|
|
10
|
+
if (stale)
|
|
11
|
+
staleCatalogs.set(copy, stale);
|
|
9
12
|
return markCatalogFreshness(copy, deadlines.get(source) ?? 0);
|
|
10
13
|
}
|
|
14
|
+
const staleCatalogs = new WeakMap();
|
|
15
|
+
/** Host-only, beside freshness: a stale listing is never fresh and never grants anything. */
|
|
16
|
+
export function markCatalogStale(tools, staleness) {
|
|
17
|
+
staleCatalogs.set(tools, staleness);
|
|
18
|
+
return tools;
|
|
19
|
+
}
|
|
20
|
+
export function catalogStaleness(tools) {
|
|
21
|
+
return staleCatalogs.get(tools);
|
|
22
|
+
}
|
|
11
23
|
export function catalogIsFresh(tools) {
|
|
12
24
|
return (deadlines.get(tools) ?? 0) > Date.now();
|
|
13
25
|
}
|
|
26
|
+
// Primitive accepted-generation evidence lives only beside the current request.
|
|
27
|
+
const generations = new WeakMap();
|
|
28
|
+
export function markCatalogGeneration(scope, connector, generation) {
|
|
29
|
+
if (!scope || generation === undefined)
|
|
30
|
+
return;
|
|
31
|
+
let values = generations.get(scope);
|
|
32
|
+
if (!values)
|
|
33
|
+
generations.set(scope, (values = new Map()));
|
|
34
|
+
values.set(connector, generation);
|
|
35
|
+
}
|
|
36
|
+
export function catalogGenerationFor(scope, connector) {
|
|
37
|
+
return generations.get(scope)?.get(connector);
|
|
38
|
+
}
|