@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/routes/mcp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { startTimingTrace } from "../timing-trace.js";
|
|
2
|
+
import { META_ROUTES, routeNames } from "../agent-routes.js";
|
|
2
3
|
import { upwardStep } from "../upward.js";
|
|
3
4
|
import { bindDiscoveryEndpoint } from "../discovery-health.js";
|
|
4
5
|
import { authCall, authInvoke, authFlow, authMethod } from "../auth/native.js";
|
|
@@ -8,15 +9,15 @@ import { observeAgentAdmission } from "../agent-observation.js";
|
|
|
8
9
|
import { bindActivityRequest } from "../activity-request.js";
|
|
9
10
|
import { closeScope } from "../runtime/connector-scope.js";
|
|
10
11
|
import { executeLimits } from "../config.js";
|
|
11
|
-
import { oauthConnectUrl, oauthConnectLink,
|
|
12
|
+
import { oauthConnectUrl, oauthConnectLink, oauthConnectLinkStarted, claimAuthRetry, oauthConnectUnavailable, } from "../oauth-handoff.js";
|
|
12
13
|
import { AuthElicitation } from "../auth-elicitation.js";
|
|
13
14
|
import { classifyInboundRequest, createMcpHandler, isJSONRPCErrorResponse, isLegacyRequest, McpServer, WebStandardStreamableHTTPServerTransport, } from "@modelcontextprotocol/server";
|
|
14
15
|
import { Duration, Effect, Exit, Option, Result, Scope } from "effect";
|
|
15
16
|
import { registerExecuteTool } from "../execute.js";
|
|
16
17
|
import { ExecutorAdmissionError } from "../executor-admission.js";
|
|
17
18
|
import { registerMetaTools } from "../meta-tools.js";
|
|
18
|
-
import { endpointInstructions } from "../skills.js";
|
|
19
|
-
import { NATIVE_TOOLS_LIST_TTL_MS, NativeSurface, nativeRequestHint } from "../native-tools.js";
|
|
19
|
+
import { ENDPOINT_EXTENSION, endpointExtension, endpointInstructions } from "../skills.js";
|
|
20
|
+
import { NATIVE_DEGRADED_TOOLS_LIST_TTL_MS, NATIVE_TOOLS_LIST_TTL_MS, NativeSurface, nativeRequestHint, } from "../native-tools.js";
|
|
20
21
|
import { registerSkills } from "./skills.js";
|
|
21
22
|
import { failureRecord, logFailure } from "../operator-record.js";
|
|
22
23
|
import { redactAgentOutput, sentSecretsForRequest } from "../sent-secrets.js";
|
|
@@ -128,7 +129,7 @@ function withMcpCors(response, request, allowedOrigin) {
|
|
|
128
129
|
}
|
|
129
130
|
headers.append("Vary", "Access-Control-Request-Headers");
|
|
130
131
|
}
|
|
131
|
-
headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version, Connecta-Error-Code, Connecta-Recovery");
|
|
132
|
+
headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version, Connecta-Error-Code, Connecta-Recovery, Connecta-Next-Action");
|
|
132
133
|
const wrapped = new Response(response.body, {
|
|
133
134
|
status: response.status,
|
|
134
135
|
statusText: response.statusText,
|
|
@@ -321,7 +322,7 @@ function toolkitRetired(logger, sentSecrets) {
|
|
|
321
322
|
},
|
|
322
323
|
});
|
|
323
324
|
}
|
|
324
|
-
function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, registry, canManageAuth, principalKey, runtimeContext, readOnly = false, pool, principalActor) {
|
|
325
|
+
function serveMcp(request, requestSignal, requestStartedAt, requestScope, opts, baseUrl, actor, registry, canManageAuth, principalKey, runtimeContext, readOnly = false, pool, principalActor, agentEndpoint = false) {
|
|
325
326
|
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
326
327
|
// Every McpServer the request builds is fresh and closes with its scope.
|
|
327
328
|
// The modern handler tears its own down after the exchange; the legacy
|
|
@@ -329,7 +330,14 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
329
330
|
const servers = [];
|
|
330
331
|
// A native pool lists its downstream tools directly (pools.<name>.surface).
|
|
331
332
|
const native = pool !== undefined ? opts.pools.get(pool)?.native : undefined;
|
|
333
|
+
// Agent endpoints keep six tools until Eve is tested against the seventh. The
|
|
334
|
+
// eval arms that render other route names measure their own fixed surfaces.
|
|
335
|
+
const readRoute = opts.config.execute.readRoute && !native && !readOnly && !agentEndpoint && opts.routes === META_ROUTES;
|
|
332
336
|
const createServer = authMethod((hint) => authFlow(function* () {
|
|
337
|
+
// A native pool's text uses the native contract's wording.
|
|
338
|
+
const routes = native ? routeNames(native.executeCode ? "native-code" : "native") : opts.routes;
|
|
339
|
+
// Direct-call and handoff text names the program route read follow-ups should take.
|
|
340
|
+
const directRoutes = readRoute ? routeNames("read-route") : routes;
|
|
333
341
|
const authElicitation = new AuthElicitation({
|
|
334
342
|
vault: opts.config.vault,
|
|
335
343
|
publicUrl: opts.config.publicUrl,
|
|
@@ -337,40 +345,49 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
337
345
|
principal: principalKey,
|
|
338
346
|
registry,
|
|
339
347
|
canManage: canManageAuth,
|
|
340
|
-
connectLink: authMethod((id, force,
|
|
348
|
+
connectLink: authMethod((id, force, flow) => authCall(oauthConnectLink, [
|
|
341
349
|
opts,
|
|
342
350
|
opts.config.publicUrl ?? baseUrl,
|
|
343
351
|
id,
|
|
344
352
|
principalKey,
|
|
345
353
|
force,
|
|
346
354
|
Boolean(opts.config.vault?.seal && opts.config.vault.open),
|
|
347
|
-
|
|
355
|
+
flow,
|
|
348
356
|
])),
|
|
349
|
-
|
|
350
|
-
closeLinks: authMethod((id, nonces) => authCall(closeOAuthConnectLinks, [opts, baseUrl, id, nonces])),
|
|
357
|
+
linkStarted: authMethod((id, nonce) => authCall(oauthConnectLinkStarted, [opts, baseUrl, id, nonce])),
|
|
351
358
|
claimRetry: authMethod((id, nonce) => authCall(claimAuthRetry, [opts, baseUrl, id, nonce])),
|
|
352
359
|
unavailable: oauthConnectUnavailable(opts),
|
|
353
360
|
credentialUi: Boolean(opts.config.ui && opts.config.vault),
|
|
361
|
+
credentialHandoffUrl: opts.config.ui?.credentialHandoffUrl(baseUrl),
|
|
362
|
+
routes: directRoutes,
|
|
354
363
|
requestSignal,
|
|
355
364
|
requestScope,
|
|
356
365
|
defer: runtimeContext?.waitUntil?.bind(runtimeContext),
|
|
357
366
|
});
|
|
367
|
+
// Read as this request's response is built, so a degraded native listing can shorten it.
|
|
368
|
+
const toolsListHint = { ttlMs: native ? NATIVE_TOOLS_LIST_TTL_MS : 3_600_000, cacheScope: "private" };
|
|
369
|
+
const connectors = registry.listConnectors();
|
|
370
|
+
const effectiveReadOnly = readOnly || (connectors.length > 0 && connectors.every((connector) => connector.readOnly === true));
|
|
358
371
|
const server = new McpServer(opts.config.serverInfo, {
|
|
359
372
|
// A request-local server cannot publish catalog changes. Set this before
|
|
360
373
|
// tool registration, whose SDK default otherwise advertises listChanged.
|
|
361
|
-
capabilities: {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
374
|
+
capabilities: {
|
|
375
|
+
tools: { listChanged: false },
|
|
376
|
+
extensions: {
|
|
377
|
+
"io.modelcontextprotocol/skills": {},
|
|
378
|
+
[ENDPOINT_EXTENSION]: endpointExtension(connectors, native !== undefined, native?.executeCode ?? true, effectiveReadOnly),
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
// Instructions reflect the admitted pool, its connector roster and the effective write
|
|
382
|
+
// policy. `registry` is this caller's admitted view, the one execute_code's inventory
|
|
383
|
+
// reads, so a person at plain /mcp never sees an id outside their pool. Every cache hint
|
|
384
|
+
// below is private, so a client keeps what it learned for this caller and endpoint URL.
|
|
385
|
+
instructions: endpointInstructions(pool !== undefined ? opts.pools.get(pool)?.description : undefined, native, effectiveReadOnly, connectors.map((connector) => connector.id), readRoute),
|
|
366
386
|
inputRequired: { legacyShim: false },
|
|
367
387
|
requestState: { verify: (state, context) => authElicitation.verify(state, context) },
|
|
368
388
|
cacheHints: {
|
|
369
389
|
"server/discover": { ttlMs: 3_600_000, cacheScope: "private" },
|
|
370
|
-
"tools/list":
|
|
371
|
-
ttlMs: native ? NATIVE_TOOLS_LIST_TTL_MS : 3_600_000,
|
|
372
|
-
cacheScope: "private",
|
|
373
|
-
},
|
|
390
|
+
"tools/list": toolsListHint,
|
|
374
391
|
},
|
|
375
392
|
});
|
|
376
393
|
servers.push(server);
|
|
@@ -378,6 +395,7 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
378
395
|
? {
|
|
379
396
|
sink: opts.config.activity?.store,
|
|
380
397
|
recordTool: opts.config.activity?.recordTool,
|
|
398
|
+
recordOperation: opts.config.activity?.recordOperation,
|
|
381
399
|
actor,
|
|
382
400
|
...(principalActor ? { principalActor } : {}),
|
|
383
401
|
requestId: crypto.randomUUID(),
|
|
@@ -397,12 +415,15 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
397
415
|
probeTimeoutMs: opts.config.discovery.probeTimeoutMs,
|
|
398
416
|
...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
|
|
399
417
|
});
|
|
400
|
-
const diagnostics = native
|
|
401
|
-
|
|
402
|
-
|
|
418
|
+
const diagnostics = native
|
|
419
|
+
? { ...native, catalogErrors: [], staleCatalogs: [] }
|
|
420
|
+
: undefined;
|
|
421
|
+
// The program tool this request lists and its reads may offer: the read route, where listed.
|
|
422
|
+
const programTool = !native || native.executeCode ? directRoutes.program : undefined;
|
|
403
423
|
const nativeRoute = registerMetaTools(server, registry, {
|
|
404
424
|
native: diagnostics,
|
|
405
|
-
|
|
425
|
+
programTool,
|
|
426
|
+
routes: directRoutes,
|
|
406
427
|
authElicitation,
|
|
407
428
|
client,
|
|
408
429
|
baseUrl,
|
|
@@ -414,19 +435,23 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
414
435
|
credentialHandoffUrl: opts.config.ui?.credentialHandoffUrl(baseUrl),
|
|
415
436
|
...(activity ? { activity } : {}),
|
|
416
437
|
defaultToolTimeoutMs: opts.config.calls.defaultTimeoutMs ?? opts.config.execute.hostCallTimeoutMs,
|
|
438
|
+
responseCeilingMs: opts.config.execute.watchdogMs,
|
|
439
|
+
requestStartedAt,
|
|
417
440
|
maxProgramResultChars: opts.config.execute.maxResultChars,
|
|
441
|
+
maxProgramHostCalls: opts.config.execute.maxHostCalls,
|
|
418
442
|
probeTimeoutMs: opts.config.discovery.probeTimeoutMs,
|
|
419
443
|
discoveryConcurrency: opts.config.discovery.concurrency,
|
|
420
444
|
requestSignal,
|
|
421
445
|
...(runtimeContext?.waitUntil ? { defer: runtimeContext.waitUntil.bind(runtimeContext) } : {}),
|
|
422
446
|
});
|
|
423
|
-
if (
|
|
447
|
+
if (programTool) {
|
|
424
448
|
registerExecuteTool(server, registry, {
|
|
425
449
|
authElicitation,
|
|
426
450
|
client,
|
|
427
451
|
baseUrl,
|
|
428
452
|
requestScope,
|
|
429
453
|
executor: opts.executor,
|
|
454
|
+
requestStartedAt,
|
|
430
455
|
defaultToolTimeoutMs: opts.config.calls.defaultTimeoutMs ?? opts.config.execute.hostCallTimeoutMs,
|
|
431
456
|
logger: opts.config.logger,
|
|
432
457
|
...(activity ? { activity } : {}),
|
|
@@ -436,6 +461,7 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
436
461
|
readOnly,
|
|
437
462
|
native: native !== undefined,
|
|
438
463
|
routes,
|
|
464
|
+
readRoute,
|
|
439
465
|
});
|
|
440
466
|
}
|
|
441
467
|
if (native && nativeRoute && hint) {
|
|
@@ -445,6 +471,10 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
445
471
|
route: nativeRoute,
|
|
446
472
|
bind: (context) => authElicitation.bind(context),
|
|
447
473
|
catalogErrors: diagnostics.catalogErrors,
|
|
474
|
+
staleCatalogs: diagnostics.staleCatalogs,
|
|
475
|
+
onDegradedListing: () => {
|
|
476
|
+
toolsListHint.ttlMs = NATIVE_DEGRADED_TOOLS_LIST_TTL_MS;
|
|
477
|
+
},
|
|
448
478
|
logger: opts.config.logger,
|
|
449
479
|
requestScope,
|
|
450
480
|
requestSignal,
|
|
@@ -635,16 +665,22 @@ export function createMcpRoute(opts) {
|
|
|
635
665
|
request.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
636
666
|
if (request.signal.aborted)
|
|
637
667
|
onCallerAbort();
|
|
668
|
+
const finishTiming = startTimingTrace(requestScope, opts.config.timingTraceSampleRate, opts.config.logger);
|
|
638
669
|
let deadlineTimer;
|
|
639
670
|
return scopedToBody(Effect.gen(function* () {
|
|
640
671
|
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
672
|
+
try {
|
|
673
|
+
requestOwner(requestScope).close();
|
|
674
|
+
}
|
|
675
|
+
finally {
|
|
676
|
+
finishTiming?.();
|
|
677
|
+
if (!localAbort.signal.aborted) {
|
|
678
|
+
localAbort.abort(request.signal.reason ?? new Error("MCP request ended."));
|
|
679
|
+
}
|
|
680
|
+
request.signal.removeEventListener("abort", onCallerAbort);
|
|
681
|
+
if (deadlineTimer !== undefined)
|
|
682
|
+
clearTimeout(deadlineTimer);
|
|
644
683
|
}
|
|
645
|
-
request.signal.removeEventListener("abort", onCallerAbort);
|
|
646
|
-
if (deadlineTimer !== undefined)
|
|
647
|
-
clearTimeout(deadlineTimer);
|
|
648
684
|
}));
|
|
649
685
|
// A request naming `subscriptions/listen` defers its permit: nothing of
|
|
650
686
|
// its body is read until auth and endpoint admission pass, and then at
|
|
@@ -652,8 +688,12 @@ export function createMcpRoute(opts) {
|
|
|
652
688
|
// modern listen goes without a permit; trusting the header alone would
|
|
653
689
|
// let a legacy tools/call bypass the pool, so anything else takes one
|
|
654
690
|
// after classification. Every other request takes its permit here,
|
|
655
|
-
// before auth, and no body
|
|
691
|
+
// before auth, and this route reads no body before auth on any path.
|
|
692
|
+
// The Node adapter has already buffered up to its own pre-auth cap
|
|
693
|
+
// (`listen`'s `maxBodyBytes`) before a request reaches core.
|
|
656
694
|
const startedAt = Date.now();
|
|
695
|
+
// The response ceiling counts from here; a future stamp could only extend it.
|
|
696
|
+
const arrivedAt = Math.min(context.runtimeContext?.requestArrivedAt ?? startedAt, startedAt);
|
|
657
697
|
const maxDurationMs = opts.requestAdmission.maxDurationMs;
|
|
658
698
|
const listenHeader = request.method === "POST" && request.headers.get("Mcp-Method") === "subscriptions/listen";
|
|
659
699
|
const refuseAdmission = (error) => {
|
|
@@ -766,8 +806,12 @@ export function createMcpRoute(opts) {
|
|
|
766
806
|
const headers = new Headers(response.headers);
|
|
767
807
|
headers.set("Connecta-Error-Code", "host_auth_required");
|
|
768
808
|
headers.set("Connecta-Recovery", "host_connection");
|
|
769
|
-
|
|
770
|
-
|
|
809
|
+
headers.set("Connecta-Next-Action", JSON.stringify({
|
|
810
|
+
action: "reconnect_host",
|
|
811
|
+
purpose: "Reconnect the host with valid inbound credentials and endpoint grants; downstream OAuth cannot repair this connection.",
|
|
812
|
+
}));
|
|
813
|
+
// Custom adapters own their response stream and request lifetime.
|
|
814
|
+
// Their typed recovery is carried in headers instead of replacing it.
|
|
771
815
|
if (!response.headers.get("Content-Type")?.includes("application/json")) {
|
|
772
816
|
return cors(new Response(response.body, { status: response.status, headers }));
|
|
773
817
|
}
|
|
@@ -780,6 +824,10 @@ export function createMcpRoute(opts) {
|
|
|
780
824
|
message: "The host is not authorized to connect to this endpoint. Sign in or update the host's connecta access token and endpoint grants.",
|
|
781
825
|
retryable: false,
|
|
782
826
|
recovery: "host_connection",
|
|
827
|
+
nextAction: {
|
|
828
|
+
action: "reconnect_host",
|
|
829
|
+
purpose: "Reconnect this host to Connecta with valid inbound credentials and endpoint grants. Downstream connector authorization cannot repair the host connection.",
|
|
830
|
+
},
|
|
783
831
|
},
|
|
784
832
|
})), { status: response.status, headers }));
|
|
785
833
|
}
|
|
@@ -923,7 +971,7 @@ export function createMcpRoute(opts) {
|
|
|
923
971
|
if (new URL(request.url).searchParams.has("toolkit")) {
|
|
924
972
|
return cors(toolkitRetired(opts.config.logger, sentSecrets));
|
|
925
973
|
}
|
|
926
|
-
return cors(yield* serveMcp(localRequest, localAbort.signal, requestScope, opts, baseUrl, authz.actor, scopedRegistry, (id) => {
|
|
974
|
+
return cors(yield* serveMcp(localRequest, localAbort.signal, arrivedAt, requestScope, opts, baseUrl, authz.actor, scopedRegistry, (id) => {
|
|
927
975
|
const connector = scopedRegistry.getConnector(id);
|
|
928
976
|
return Boolean(connector && mayManageConnector(opts, authz, connector));
|
|
929
977
|
}, authz.principalKey, runtimeContext, readOnly, scope.pool, authz.identity.principal
|
|
@@ -932,7 +980,7 @@ export function createMcpRoute(opts) {
|
|
|
932
980
|
id: authz.identity.principal.id,
|
|
933
981
|
namespace: authz.identity.principal.namespace,
|
|
934
982
|
}
|
|
935
|
-
: undefined));
|
|
983
|
+
: undefined, agentEndpoint));
|
|
936
984
|
});
|
|
937
985
|
if (remainingMs === undefined)
|
|
938
986
|
return yield* handled;
|
package/dist/routes/shared.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface ServerOptions {
|
|
|
34
34
|
}
|
|
35
35
|
export interface RuntimeExecutionContext extends InboundAuthRuntimeContext {
|
|
36
36
|
waitUntil(promise: Promise<unknown>): void;
|
|
37
|
+
/**
|
|
38
|
+
* When the Node adapter received this request's headers, before it buffered
|
|
39
|
+
* the body. It comes from the host, never from anything the request carries.
|
|
40
|
+
* Workers read the body lazily, so the route's own clock already counts upload.
|
|
41
|
+
*/
|
|
42
|
+
readonly requestArrivedAt?: number;
|
|
37
43
|
}
|
|
38
44
|
export interface RouteContext {
|
|
39
45
|
request: Request;
|
package/dist/routes/ui-config.js
CHANGED
|
@@ -171,7 +171,7 @@ export function scanOperatorActivity(context, authz, registry, rows, agents) {
|
|
|
171
171
|
continue;
|
|
172
172
|
// A last call is a call; any row recorded for an agent, a catalog
|
|
173
173
|
// refresh included, is a sign it was seen.
|
|
174
|
-
const row = event.kind
|
|
174
|
+
const row = event.kind === undefined && OUTCOMES.has(event.outcome) ? byId.get(event.connectorId) : undefined;
|
|
175
175
|
const agent = (event.actor.kind === "access_token" || event.actor.kind === "vercel_oidc") &&
|
|
176
176
|
event.actor.namespace === AGENT_NAMESPACE &&
|
|
177
177
|
event.actor.id
|
package/dist/routes/ui-data.js
CHANGED
|
@@ -249,6 +249,7 @@ export function uiData(registry, baseUrl, options) {
|
|
|
249
249
|
toolCount,
|
|
250
250
|
observedAt: Date.now(),
|
|
251
251
|
}));
|
|
252
|
+
const observedAgeMs = observation ? Math.max(0, Date.now() - observation.observedAt) : undefined;
|
|
252
253
|
return {
|
|
253
254
|
id: c.id,
|
|
254
255
|
authScope: c.authScope ?? "shared",
|
|
@@ -271,9 +272,9 @@ export function uiData(registry, baseUrl, options) {
|
|
|
271
272
|
...(status.registrationPath ? { auth: { registrationPath: status.registrationPath } } : {}),
|
|
272
273
|
catalogAgeMs: registry.catalogAgeMs(c.id),
|
|
273
274
|
lastCall: null,
|
|
274
|
-
...(
|
|
275
|
+
...(observedAgeMs !== undefined ? { observedAgeMs } : {}),
|
|
275
276
|
},
|
|
276
|
-
...(
|
|
277
|
+
...(observedAgeMs !== undefined ? { observedAgeMs } : {}),
|
|
277
278
|
// Counts only, and only when a refresh in this runtime produced them.
|
|
278
279
|
// `Registry.withCatalogObservations` rebuilt the report through
|
|
279
280
|
...(status.catalogAccess ? { catalogAccess: status.catalogAccess } : {}),
|
package/dist/runtime/call.js
CHANGED
|
@@ -359,17 +359,22 @@ export function wrapViewCredentials(view) {
|
|
|
359
359
|
return;
|
|
360
360
|
entry.credentialWrapped = true;
|
|
361
361
|
const access = view.credential;
|
|
362
|
+
// A read outlives a caller that stopped waiting and registers when it lands, so the
|
|
363
|
+
// request counts it until then: a sealing run must not publish ahead of that.
|
|
364
|
+
const read = async (value, values) => {
|
|
365
|
+
const done = requestOf(entry.request).credentialReads.begin();
|
|
366
|
+
try {
|
|
367
|
+
const result = await value();
|
|
368
|
+
registerCredentialValues(viewSecrets(view), values(result));
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
done();
|
|
373
|
+
}
|
|
374
|
+
};
|
|
362
375
|
view.credential = {
|
|
363
|
-
get: (field) => settleOnClose(view,
|
|
364
|
-
|
|
365
|
-
registerCredentialValues(viewSecrets(view), value === null ? null : { [field ?? "value"]: value });
|
|
366
|
-
return value;
|
|
367
|
-
}, true),
|
|
368
|
-
getAll: () => settleOnClose(view, async () => {
|
|
369
|
-
const values = await access.getAll();
|
|
370
|
-
registerCredentialValues(viewSecrets(view), values);
|
|
371
|
-
return values;
|
|
372
|
-
}, true),
|
|
376
|
+
get: (field) => settleOnClose(view, () => read(() => access.get(field), (value) => (value === null ? null : { [field ?? "value"]: value })), true),
|
|
377
|
+
getAll: () => settleOnClose(view, () => read(() => access.getAll(), (values) => values), true),
|
|
373
378
|
};
|
|
374
379
|
}
|
|
375
380
|
/** Credentials for request-owned transports use the operation, never a closed call. */
|
|
@@ -25,6 +25,11 @@ export function registerNativeStorage(edge, native) {
|
|
|
25
25
|
storageMethods.set(edge, own);
|
|
26
26
|
const getMany = native.getMany;
|
|
27
27
|
storage.set(edge, {
|
|
28
|
+
catalogSnapshot: native.catalogSnapshot
|
|
29
|
+
? (keys, generationKey, prefix, suffixes) => typed(() => getNativeStorage(edge)
|
|
30
|
+
? native.catalogSnapshot(keys, generationKey, prefix, suffixes)
|
|
31
|
+
: Effect.succeed(new Map()))
|
|
32
|
+
: undefined,
|
|
28
33
|
accessToken: native.accessToken
|
|
29
34
|
? (hash) => typed(() => (getNativeStorage(edge) ? native.accessToken(hash) : Effect.succeed(undefined)))
|
|
30
35
|
: undefined,
|
|
@@ -37,6 +42,13 @@ export function registerNativeStorage(edge, native) {
|
|
|
37
42
|
: Effect.forEach(keys, (key) => current(edge, "get", own.get, [key], () => native.get(key)))),
|
|
38
43
|
}
|
|
39
44
|
: {}),
|
|
45
|
+
...(native.setMany
|
|
46
|
+
? {
|
|
47
|
+
setMany: (entries, options) => typed(() => getNativeStorage(edge)
|
|
48
|
+
? native.setMany(entries, options)
|
|
49
|
+
: Effect.asVoid(Effect.forEach(entries, ([key, value]) => current(edge, "set", own.set, [key, value, options], () => native.set(key, value, options))))),
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
40
52
|
set: (key, value, options) => current(edge, "set", own.set, [key, value, options], () => native.set(key, value, options)),
|
|
41
53
|
delete: (key) => current(edge, "delete", own.delete, [key], () => native.delete(key)),
|
|
42
54
|
list: (prefix) => current(edge, "list", own.list, [prefix], () => native.list(prefix)),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { utf8Length } from "../bytes.js";
|
|
1
2
|
// Request-scoped memo of storage reads (INV-7).
|
|
2
3
|
//
|
|
3
4
|
// One request often reads the same key many times: a catalog listing reads its
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
// `FinalizationRegistry` (workerd before compatibility date 2025-05-05 without
|
|
37
38
|
// `enable_weak_ref`) memoizes nothing: every read goes to storage.
|
|
38
39
|
import { Effect } from "effect";
|
|
40
|
+
import { getNativeStorage } from "./native-storage.js";
|
|
39
41
|
const roots = new WeakMap();
|
|
40
42
|
const observed = new WeakMap();
|
|
41
43
|
/**
|
|
@@ -88,7 +90,7 @@ export function observeStorageWrites(storage, layer) {
|
|
|
88
90
|
return storage;
|
|
89
91
|
}
|
|
90
92
|
/** The underlying store and key a read of `key` reaches, when every layer is observed. */
|
|
91
|
-
function
|
|
93
|
+
export function physicalStorageAddress(storage, key) {
|
|
92
94
|
const entry = observed.get(storage);
|
|
93
95
|
if (!entry)
|
|
94
96
|
return undefined;
|
|
@@ -99,10 +101,17 @@ function physical(storage, key) {
|
|
|
99
101
|
own.list !== storage.list ||
|
|
100
102
|
own.compareAndSet !== storage.compareAndSet)
|
|
101
103
|
return undefined;
|
|
102
|
-
return "root" in layer ? { store: layer.root, key } :
|
|
104
|
+
return "root" in layer ? { store: layer.root, key } : physicalStorageAddress(layer.storage, layer.prefix + key);
|
|
103
105
|
}
|
|
104
106
|
const weakReferences = () => typeof WeakRef === "function" && typeof FinalizationRegistry === "function";
|
|
105
107
|
export class RequestReads {
|
|
108
|
+
trace;
|
|
109
|
+
constructor(trace) {
|
|
110
|
+
this.trace = trace;
|
|
111
|
+
}
|
|
112
|
+
note(fresh, hit, value, started) {
|
|
113
|
+
this.trace?.record(fresh ? "memo.fresh" : hit ? "memo.hit" : "memo.miss", performance.now() - started, value === null ? 0 : utf8Length(value), 0, value === null ? 1 : 0);
|
|
114
|
+
}
|
|
106
115
|
closed = false;
|
|
107
116
|
stores = new Map();
|
|
108
117
|
ref = weakReferences() ? new WeakRef(this) : undefined;
|
|
@@ -129,7 +138,7 @@ export class RequestReads {
|
|
|
129
138
|
}
|
|
130
139
|
/** A memoized answer, or how to record the value a storage read returns. */
|
|
131
140
|
lookup(storage, key, fresh) {
|
|
132
|
-
const address = this.closed || !this.ref ? undefined :
|
|
141
|
+
const address = this.closed || !this.ref ? undefined : physicalStorageAddress(storage, key);
|
|
133
142
|
if (!address)
|
|
134
143
|
return undefined;
|
|
135
144
|
const entries = this.entriesFor(address.store);
|
|
@@ -155,31 +164,70 @@ export class RequestReads {
|
|
|
155
164
|
read(storage, key, load, fresh = false) {
|
|
156
165
|
return Effect.suspend(() => {
|
|
157
166
|
const found = this.lookup(storage, key, fresh);
|
|
158
|
-
if (!found)
|
|
167
|
+
if (!found && !this.trace)
|
|
159
168
|
return load;
|
|
160
|
-
|
|
169
|
+
const started = this.trace ? performance.now() : 0;
|
|
170
|
+
if (found?.hit) {
|
|
171
|
+
if (this.trace)
|
|
172
|
+
this.note(fresh, true, found.value, started);
|
|
161
173
|
return Effect.succeed(found.value);
|
|
162
|
-
|
|
174
|
+
}
|
|
175
|
+
return Effect.tap(load, (value) => Effect.sync(() => {
|
|
176
|
+
found?.record(value);
|
|
177
|
+
if (this.trace)
|
|
178
|
+
this.note(fresh, false, value, started);
|
|
179
|
+
}));
|
|
163
180
|
});
|
|
164
181
|
}
|
|
165
182
|
/** Prefetch only keys whose callers already permit request-local staleness. */
|
|
166
|
-
prefetch(requests, load) {
|
|
183
|
+
prefetch(requests, load, linked) {
|
|
167
184
|
return Effect.suspend(() => {
|
|
168
185
|
const pending = requests.map(({ storage, key }) => ({
|
|
169
|
-
address:
|
|
186
|
+
address: physicalStorageAddress(storage, key),
|
|
170
187
|
found: this.lookup(storage, key, false),
|
|
171
188
|
}));
|
|
189
|
+
if (this.trace)
|
|
190
|
+
for (const { found } of pending)
|
|
191
|
+
if (found?.hit)
|
|
192
|
+
this.note(false, true, found.value, performance.now());
|
|
193
|
+
const started = this.trace ? performance.now() : 0;
|
|
172
194
|
const root = pending[0]?.address?.store;
|
|
173
195
|
const storage = root && roots.get(root);
|
|
174
196
|
if (!storage || pending.some(({ address, found }) => !found || address?.store !== root))
|
|
175
197
|
return Effect.void;
|
|
198
|
+
if (pending.every(({ found }) => found?.hit))
|
|
199
|
+
return Effect.void;
|
|
200
|
+
const generation = linked && physicalStorageAddress(linked.storage, linked.generationKey);
|
|
201
|
+
const prefix = linked && physicalStorageAddress(linked.storage, linked.prefix);
|
|
202
|
+
const snapshot = getNativeStorage(storage)?.catalogSnapshot;
|
|
203
|
+
if (linked && snapshot && generation?.store === root && prefix?.store === root) {
|
|
204
|
+
// Candidates only locate entries. An intervening external write can make
|
|
205
|
+
// these stale, so consumers retain the separate post-chunk primary fence.
|
|
206
|
+
return Effect.map(snapshot(pending.map(({ address }) => address.key), generation.key, prefix.key, linked.suffixes), (values) => {
|
|
207
|
+
pending.forEach(({ address, found }) => {
|
|
208
|
+
if (found && !found.hit)
|
|
209
|
+
found.record(values.get(address.key) ?? null);
|
|
210
|
+
});
|
|
211
|
+
for (const [key, value] of values) {
|
|
212
|
+
if (pending.some(({ address }) => address.key === key))
|
|
213
|
+
continue;
|
|
214
|
+
const found = this.lookup(storage, key, false);
|
|
215
|
+
if (found && !found.hit)
|
|
216
|
+
found.record(value);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
176
220
|
const misses = pending.filter(({ found }) => found && !found.hit);
|
|
177
221
|
if (!misses.length)
|
|
178
222
|
return Effect.void;
|
|
179
223
|
return Effect.map(load(storage, misses.map(({ address }) => address.key)), (values) => {
|
|
180
224
|
misses.forEach(({ found }, i) => {
|
|
181
|
-
if (found && !found.hit)
|
|
182
|
-
|
|
225
|
+
if (found && !found.hit) {
|
|
226
|
+
const value = values[i] ?? null;
|
|
227
|
+
found.record(value);
|
|
228
|
+
if (this.trace)
|
|
229
|
+
this.note(false, false, value, started);
|
|
230
|
+
}
|
|
183
231
|
});
|
|
184
232
|
});
|
|
185
233
|
});
|
|
@@ -187,12 +235,16 @@ export class RequestReads {
|
|
|
187
235
|
/** `read` for a Promise-shaped caller, without another Effect edge. */
|
|
188
236
|
async readPromise(storage, key, load, fresh = false) {
|
|
189
237
|
const found = this.lookup(storage, key, fresh);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
238
|
+
const started = this.trace ? performance.now() : 0;
|
|
239
|
+
if (found?.hit) {
|
|
240
|
+
if (this.trace)
|
|
241
|
+
this.note(fresh, true, found.value, started);
|
|
193
242
|
return found.value;
|
|
243
|
+
}
|
|
194
244
|
const value = await load();
|
|
195
|
-
found
|
|
245
|
+
found?.record(value);
|
|
246
|
+
if (this.trace)
|
|
247
|
+
this.note(fresh, false, value, started);
|
|
196
248
|
return value;
|
|
197
249
|
}
|
|
198
250
|
close() {
|
|
@@ -1,8 +1,42 @@
|
|
|
1
|
+
import { traceFor } from "../timing-trace.js";
|
|
1
2
|
// One owner per existing upstream request identity. Facts survive revocation;
|
|
2
3
|
// retaining them never reconstitutes a call capability or a live operation.
|
|
3
|
-
import { Context, Exit, Scope } from "effect";
|
|
4
|
+
import { Context, Effect, Exit, Scope } from "effect";
|
|
4
5
|
import { RequestReads } from "./request-reads.js";
|
|
5
6
|
import { detach } from "./run.js";
|
|
7
|
+
/** Work that can still add to a secret set. A sealing run waits until none is left. */
|
|
8
|
+
export class PendingWork {
|
|
9
|
+
active = 0;
|
|
10
|
+
waiting = new Set();
|
|
11
|
+
get pending() {
|
|
12
|
+
return this.active > 0;
|
|
13
|
+
}
|
|
14
|
+
/** Returns the function that ends this piece of work; calling it twice counts once. */
|
|
15
|
+
begin() {
|
|
16
|
+
this.active++;
|
|
17
|
+
let done = false;
|
|
18
|
+
return () => {
|
|
19
|
+
if (done)
|
|
20
|
+
return;
|
|
21
|
+
done = true;
|
|
22
|
+
if (--this.active > 0)
|
|
23
|
+
return;
|
|
24
|
+
for (const wake of this.waiting)
|
|
25
|
+
wake();
|
|
26
|
+
this.waiting.clear();
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Succeeds when nothing is pending. Interrupting it drops the wait. */
|
|
30
|
+
idle = Effect.callback((resume) => {
|
|
31
|
+
if (this.active === 0)
|
|
32
|
+
return resume(Effect.void);
|
|
33
|
+
const wake = () => resume(Effect.void);
|
|
34
|
+
this.waiting.add(wake);
|
|
35
|
+
return Effect.sync(() => {
|
|
36
|
+
this.waiting.delete(wake);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
6
40
|
export class RequestOwner {
|
|
7
41
|
identity;
|
|
8
42
|
open = true;
|
|
@@ -13,6 +47,8 @@ export class RequestOwner {
|
|
|
13
47
|
calls = new Set();
|
|
14
48
|
signals = new WeakMap();
|
|
15
49
|
scope = Scope.makeUnsafe();
|
|
50
|
+
/** Credential reads whose values have yet to join this request's secret set. */
|
|
51
|
+
credentialReads = new PendingWork();
|
|
16
52
|
// Initialize secrets only when data is sent. Metadata-only UI imports must
|
|
17
53
|
// not retain the payload matcher merely to revoke a request.
|
|
18
54
|
retained = {
|
|
@@ -71,7 +107,7 @@ export function requestReads(identity) {
|
|
|
71
107
|
export function ownerReads(owner) {
|
|
72
108
|
const retained = owner.retained;
|
|
73
109
|
if (!retained.reads) {
|
|
74
|
-
retained.reads = new RequestReads();
|
|
110
|
+
retained.reads = new RequestReads(traceFor(owner.identity));
|
|
75
111
|
if (!owner.open)
|
|
76
112
|
retained.reads.close();
|
|
77
113
|
}
|
package/dist/runtime/run.js
CHANGED
|
@@ -128,7 +128,10 @@ export function withDeadlineEffect(operation, options) {
|
|
|
128
128
|
fromSignal(controller.signal),
|
|
129
129
|
];
|
|
130
130
|
if (options.timeoutMs !== undefined) {
|
|
131
|
-
contenders.push(Effect.sleep(Duration.millis(options.timeoutMs)).pipe(Effect.andThen(Effect.
|
|
131
|
+
contenders.push(Effect.sleep(Duration.millis(options.timeoutMs)).pipe(Effect.andThen(Effect.suspend(() => {
|
|
132
|
+
const more = options.extendMs?.() ?? 0;
|
|
133
|
+
return more > 0 ? Effect.sleep(Duration.millis(more)) : Effect.void;
|
|
134
|
+
})), Effect.andThen(Effect.sync(() => controller.abort(options.timeoutError))), Effect.andThen(Effect.never)));
|
|
132
135
|
}
|
|
133
136
|
return Effect.raceAllFirst(contenders).pipe(Effect.ensuring(Effect.sync(() => {
|
|
134
137
|
controller.abort();
|