@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/agent-routes.js
CHANGED
|
@@ -26,45 +26,8 @@
|
|
|
26
26
|
* catalogs with the description built for its own surface.
|
|
27
27
|
*/
|
|
28
28
|
import { Context } from "effect";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
search: "search_tools",
|
|
32
|
-
read: "call_tool",
|
|
33
|
-
write: "call_destructive_tool",
|
|
34
|
-
program: "execute_code",
|
|
35
|
-
authorize: "authorize_connector",
|
|
36
|
-
skills: "skills",
|
|
37
|
-
}),
|
|
38
|
-
code: Object.freeze({
|
|
39
|
-
search: "connecta.search inside execute_code",
|
|
40
|
-
read: "connecta.call inside execute_code",
|
|
41
|
-
write: "connecta.call inside execute_code",
|
|
42
|
-
program: "execute_code",
|
|
43
|
-
authorize: "authorize_connector",
|
|
44
|
-
skills: "skills",
|
|
45
|
-
}),
|
|
46
|
-
native: Object.freeze({
|
|
47
|
-
search: "your host's tool search",
|
|
48
|
-
read: "the original native tool",
|
|
49
|
-
write: "the original native tool",
|
|
50
|
-
program: "your host's tool search",
|
|
51
|
-
authorize: "the returned authorization handoff",
|
|
52
|
-
skills: "skills",
|
|
53
|
-
}),
|
|
54
|
-
"native-code": Object.freeze({
|
|
55
|
-
search: "your host's tool search",
|
|
56
|
-
read: "the original native tool",
|
|
57
|
-
write: "the original native tool",
|
|
58
|
-
program: "execute_code",
|
|
59
|
-
authorize: "the returned authorization handoff",
|
|
60
|
-
skills: "skills",
|
|
61
|
-
}),
|
|
62
|
-
};
|
|
63
|
-
/** The six-tool surface's names: the default wherever no other surface applies. */
|
|
64
|
-
export const META_ROUTES = NAMES.meta;
|
|
65
|
-
export function routeNames(surface) {
|
|
66
|
-
return NAMES[surface];
|
|
67
|
-
}
|
|
29
|
+
import { META_ROUTES, routeNames } from "./route-names.js";
|
|
30
|
+
export { META_ROUTES, routeNames } from "./route-names.js";
|
|
68
31
|
/** The names for the surface a connector operation serves; meta for contexts Connecta did not build. */
|
|
69
32
|
export function routesOf(ctx) {
|
|
70
33
|
return ctx?.routes ?? META_ROUTES;
|
|
@@ -86,11 +49,25 @@ const nativeAuthorize = {
|
|
|
86
49
|
authorizeCall: () => "Give this error's nextAction to the user or operator.",
|
|
87
50
|
authorizeAgain: "retry the original native call",
|
|
88
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* An auth error's message, once that error carries what authorize_connector
|
|
54
|
+
* would return: the fixed route text stops sending the agent to fetch it again.
|
|
55
|
+
* Only Connecta's own phrases change; provider wording around them stays.
|
|
56
|
+
*/
|
|
57
|
+
export function handoffInHand(message, connector) {
|
|
58
|
+
return message
|
|
59
|
+
.replaceAll(metaAuthorize.authorizeAndOpen(connector), nativeAuthorize.authorizeAndOpen())
|
|
60
|
+
.replaceAll(metaAuthorize.authorizeAndFollow(connector), nativeAuthorize.authorizeAndFollow())
|
|
61
|
+
.replaceAll(metaAuthorize.authorizeCall(connector), nativeAuthorize.authorizeCall())
|
|
62
|
+
.replaceAll(metaAuthorize.authorizeCall(), nativeAuthorize.authorizeCall())
|
|
63
|
+
.replaceAll(metaAuthorize.authorizeFor("recovery options"), nativeAuthorize.authorizeFor())
|
|
64
|
+
.replaceAll(metaAuthorize.authorizeFor("this connector"), nativeAuthorize.authorizeFor());
|
|
65
|
+
}
|
|
89
66
|
const nativeDirect = {
|
|
90
67
|
directOnly: "when calling this tool directly",
|
|
91
68
|
forDirect: "when calling this tool directly",
|
|
92
69
|
onlyDirectRead: "only when calling this tool directly",
|
|
93
|
-
writeNeedsApproval: "Invoke it through its own
|
|
70
|
+
writeNeedsApproval: "This name is bound to a read, so nothing was sent. Invoke it through its own write tool so the MCP host can request approval.",
|
|
94
71
|
};
|
|
95
72
|
const PHRASES = {
|
|
96
73
|
meta: Object.freeze({
|
|
@@ -102,6 +79,15 @@ const PHRASES = {
|
|
|
102
79
|
writeNeedsApproval: "Invoke it through call_destructive_tool so the MCP host can request approval.",
|
|
103
80
|
...metaAuthorize,
|
|
104
81
|
}),
|
|
82
|
+
"read-route": Object.freeze({
|
|
83
|
+
program: "execute_read_code",
|
|
84
|
+
directOnly: "for a direct call_tool only",
|
|
85
|
+
forDirect: "for a direct call_tool",
|
|
86
|
+
onlyDirectRead: "only for a direct call_tool read",
|
|
87
|
+
callDirectlyAndPage: "call directly (call_tool, connecta.result to page)",
|
|
88
|
+
writeNeedsApproval: "Invoke it through call_destructive_tool so the MCP host can request approval.",
|
|
89
|
+
...metaAuthorize,
|
|
90
|
+
}),
|
|
105
91
|
// The eval code arm keeps the text its comparator protocol recorded.
|
|
106
92
|
code: Object.freeze({
|
|
107
93
|
program: "execute_code",
|
|
@@ -125,7 +111,10 @@ const PHRASES = {
|
|
|
125
111
|
...nativeAuthorize,
|
|
126
112
|
}),
|
|
127
113
|
};
|
|
128
|
-
const SURFACES = new Map(
|
|
114
|
+
const SURFACES = new Map(["meta", "read-route", "code", "native", "native-code"].map((surface) => [
|
|
115
|
+
routeNames(surface),
|
|
116
|
+
surface,
|
|
117
|
+
]));
|
|
129
118
|
/** The surface a names table belongs to; meta for a table Connecta did not build. */
|
|
130
119
|
function routeSurface(routes) {
|
|
131
120
|
return (routes && SURFACES.get(routes)) ?? "meta";
|
|
@@ -222,10 +211,10 @@ const programRouteConfigs = new WeakMap();
|
|
|
222
211
|
* No package entry exports it and no configuration key selects it; the eval
|
|
223
212
|
* code arm marks the configuration object it passes to createConnecta.
|
|
224
213
|
*/
|
|
225
|
-
export function renderRoutesAsPrograms(config) {
|
|
226
|
-
programRouteConfigs.set(config,
|
|
214
|
+
export function renderRoutesAsPrograms(config, searchOnly = false) {
|
|
215
|
+
programRouteConfigs.set(config, searchOnly ? { ...META_ROUTES, search: routeNames("code").search } : routeNames("code"));
|
|
227
216
|
}
|
|
228
217
|
/** The names every reply of a deployment built from `config` uses outside native pools. */
|
|
229
218
|
export function deploymentRoutes(config) {
|
|
230
|
-
return programRouteConfigs.
|
|
219
|
+
return programRouteConfigs.get(config) ?? META_ROUTES;
|
|
231
220
|
}
|
package/dist/auth/clerk.d.ts
CHANGED
|
@@ -4,7 +4,12 @@ export type { ClerkGateClient, ClerkUser } from "./clerk-sdk/client.js";
|
|
|
4
4
|
export interface ClerkAuthOptions {
|
|
5
5
|
publishableKey: string;
|
|
6
6
|
secretKey: string;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Public base URL of this deployment: what tokens' resource and sessions'
|
|
9
|
+
* `azp` are checked against. Defaults to `ConnectaConfig.publicUrl`. One of
|
|
10
|
+
* the two is required; a request's Host never substitutes for it. A local
|
|
11
|
+
* deployment names its loopback origin, such as `http://localhost:8787`.
|
|
12
|
+
*/
|
|
8
13
|
publicUrl?: string;
|
|
9
14
|
/**
|
|
10
15
|
* OAuth client IDs dedicated to this deployment, used only when a verified
|
package/dist/auth/clerk.js
CHANGED
|
@@ -436,7 +436,22 @@ function mcpPoolSuffix(pathname) {
|
|
|
436
436
|
const match = /^\/mcp(\/[a-z0-9_-]+)$/.exec(pathname);
|
|
437
437
|
return match ? match[1] : null;
|
|
438
438
|
}
|
|
439
|
+
function assertPublicUrl(value) {
|
|
440
|
+
if (value === undefined)
|
|
441
|
+
return;
|
|
442
|
+
let url;
|
|
443
|
+
try {
|
|
444
|
+
url = typeof value === "string" ? new URL(value) : undefined;
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
url = undefined;
|
|
448
|
+
}
|
|
449
|
+
if (!url || (url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password) {
|
|
450
|
+
throw new Error("clerkAuth: `publicUrl` must be an absolute http(s) URL without credentials.");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
439
453
|
export function clerkAuth(opts) {
|
|
454
|
+
assertPublicUrl(opts.publicUrl);
|
|
440
455
|
assertNoRetiredToolkitOptions("clerkAuth", opts);
|
|
441
456
|
// Before the Clerk client, so a malformed key fails as a connecta
|
|
442
457
|
// configuration error rather than however the SDK happens to treat it.
|
|
@@ -771,6 +786,7 @@ export function clerkAuth(opts) {
|
|
|
771
786
|
return {
|
|
772
787
|
kind: "clerk",
|
|
773
788
|
interactiveOperator: true,
|
|
789
|
+
...(opts.publicUrl === undefined ? { requiresPublicUrl: true } : {}),
|
|
774
790
|
recognizesCredential: (request) => {
|
|
775
791
|
if (authorizationCredential(request).kind !== "absent")
|
|
776
792
|
return !isMachineCredential(request);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import { withAuthenticationDeadline } from "../auth-admission.js";
|
|
2
3
|
import { validIdentityReference } from "../identity.js";
|
|
3
4
|
import { authorizationCredential } from "../inbound-credential.js";
|
|
4
5
|
import { attachFailureFacts } from "../operator-record.js";
|
|
5
|
-
import { authCall, authFlow,
|
|
6
|
+
import { authCall, authFlow, authMethod } from "./native.js";
|
|
6
7
|
function identityString(identity, field) {
|
|
7
8
|
const value = identity[field];
|
|
8
9
|
return typeof value === "string" && validIdentityReference({ namespace: "cloudflare-access", id: value })
|
|
@@ -15,12 +16,30 @@ function unauthorized(reason) {
|
|
|
15
16
|
response: attachFailureFacts(Response.json({ error: "Cloudflare Access authentication required" }, { status: 401, headers: { "WWW-Authenticate": 'Bearer scope="openid email"', "Cache-Control": "no-store" } }), { reason }),
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
|
-
function unavailable(problem) {
|
|
19
|
+
function unavailable(problem, identityLookup) {
|
|
19
20
|
return {
|
|
20
21
|
ok: false,
|
|
21
|
-
response: attachFailureFacts(Response.json({ error: "Cloudflare Access identity unavailable", problem }, { status: 503, headers: { "Cache-Control": "no-store", "Retry-After": "1" } }), {
|
|
22
|
+
response: attachFailureFacts(Response.json({ error: "Cloudflare Access identity unavailable", problem }, { status: 503, headers: { "Cache-Control": "no-store", "Retry-After": "1" } }), {
|
|
23
|
+
reason: problem === "identity_unavailable" ? "access_identity_unavailable" : problem,
|
|
24
|
+
...(identityLookup ? { identityLookup } : {}),
|
|
25
|
+
}),
|
|
22
26
|
};
|
|
23
27
|
}
|
|
28
|
+
let lookups = 0;
|
|
29
|
+
let isolateCold = true;
|
|
30
|
+
const lookupSettled = () => {
|
|
31
|
+
lookups--;
|
|
32
|
+
};
|
|
33
|
+
function lookupIdentity(access) {
|
|
34
|
+
lookups++;
|
|
35
|
+
try {
|
|
36
|
+
return Promise.resolve(access.getIdentity()).finally(lookupSettled);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
lookups--;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
24
43
|
/**
|
|
25
44
|
* Trust the identity Cloudflare Access attached to this direct Worker
|
|
26
45
|
* invocation. Access has already validated the browser session, Managed OAuth
|
|
@@ -50,16 +69,39 @@ export function cloudflareAccessAuth() {
|
|
|
50
69
|
if (typeof access.aud !== "string" || !/^[\x21-\x7e]{1,256}$/.test(access.aud))
|
|
51
70
|
return unavailable("access_audience_invalid");
|
|
52
71
|
let identity;
|
|
72
|
+
const started = performance.now();
|
|
73
|
+
const cold = isolateCold;
|
|
74
|
+
isolateCold = false;
|
|
75
|
+
let rejected = false;
|
|
53
76
|
try {
|
|
54
77
|
identity = (yield authCall(withAuthenticationDeadline, [
|
|
55
78
|
request,
|
|
56
|
-
authMethod(() => {
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
authMethod((signal) => authFlow(function* () {
|
|
80
|
+
for (let attempt = 0;; attempt++) {
|
|
81
|
+
signal.throwIfAborted();
|
|
82
|
+
try {
|
|
83
|
+
return (yield authCall(lookupIdentity, [access]));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (attempt === 1) {
|
|
87
|
+
rejected = true;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Only a rejected identity lookup retries. A hung lookup never
|
|
92
|
+
// overlaps a second one; this sleep shares the original deadline.
|
|
93
|
+
yield Effect.sleep(100 + Math.floor(Math.random() * 151));
|
|
94
|
+
}
|
|
95
|
+
})),
|
|
59
96
|
]));
|
|
60
97
|
}
|
|
61
98
|
catch {
|
|
62
|
-
return unavailable("identity_unavailable"
|
|
99
|
+
return unavailable("identity_unavailable", {
|
|
100
|
+
outcome: rejected ? "rejected" : "timeout",
|
|
101
|
+
durationMs: Math.max(0, Math.round(performance.now() - started)),
|
|
102
|
+
cold,
|
|
103
|
+
inFlight: lookups,
|
|
104
|
+
});
|
|
63
105
|
}
|
|
64
106
|
// Access binds ctx.access.aud to its Worker application at the edge.
|
|
65
107
|
// Service credentials can pass that edge, but connecta machines must
|
|
@@ -99,10 +99,38 @@ const FLOW_VERSION = 1;
|
|
|
99
99
|
* flow record's fifteen minutes.
|
|
100
100
|
*/
|
|
101
101
|
const PENDING_AUTHORIZATION_MAX_AGE_MS = 10 * 60 * 1000;
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
/**
|
|
103
|
+
* What a flow record without a sealer may keep of a URL: everything but its
|
|
104
|
+
* userinfo, query and fragment. Non-URLs and clean URLs stay exact, so an
|
|
105
|
+
* ordinary issuer or client id still compares byte for byte.
|
|
106
|
+
*/
|
|
107
|
+
export function withoutUrlSecrets(value) {
|
|
108
|
+
if (Array.isArray(value))
|
|
109
|
+
return value.map(withoutUrlSecrets);
|
|
110
|
+
if (plainObject(value)) {
|
|
111
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, withoutUrlSecrets(item)]));
|
|
112
|
+
}
|
|
113
|
+
if (typeof value !== "string")
|
|
114
|
+
return value;
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(value);
|
|
117
|
+
if (!url.username && !url.password && !url.search && !url.hash)
|
|
118
|
+
return value;
|
|
119
|
+
url.username = url.password = url.search = url.hash = "";
|
|
120
|
+
return url.href;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `minimized` binds the discovery metadata without its URL secrets, for a
|
|
128
|
+
* record no key protects: that digest is unkeyed, so it may not cover them.
|
|
129
|
+
*/
|
|
130
|
+
export function discoveryBinding(state, minimized = false) {
|
|
131
|
+
return runEdge(authProgram(discoveryBindingNative, [state, minimized]));
|
|
104
132
|
}
|
|
105
|
-
const discoveryBindingNative = /* @__PURE__ */ registerAuth(discoveryBinding, function (state) {
|
|
133
|
+
const discoveryBindingNative = /* @__PURE__ */ registerAuth(discoveryBinding, function (state, minimized = false) {
|
|
106
134
|
return authFlow(function* () {
|
|
107
135
|
return state
|
|
108
136
|
? {
|
|
@@ -111,7 +139,9 @@ const discoveryBindingNative = /* @__PURE__ */ registerAuth(discoveryBinding, fu
|
|
|
111
139
|
...(state.authorizationServerMetadata?.authorization_response_iss_parameter_supported === true
|
|
112
140
|
? { issRequired: true }
|
|
113
141
|
: {}),
|
|
114
|
-
discovery: (yield authCall(oauthStateDigest, [
|
|
142
|
+
discovery: (yield authCall(oauthStateDigest, [
|
|
143
|
+
JSON.stringify(minimized ? withoutUrlSecrets(state) : state),
|
|
144
|
+
])),
|
|
115
145
|
}
|
|
116
146
|
: {};
|
|
117
147
|
});
|
|
@@ -120,20 +150,26 @@ const discoveryBindingNative = /* @__PURE__ */ registerAuth(discoveryBinding, fu
|
|
|
120
150
|
export const consentMethods = {
|
|
121
151
|
encodeFlow: function (key, flow) {
|
|
122
152
|
return authFlow(function* () {
|
|
123
|
-
const { verifier, ...rest } = flow;
|
|
153
|
+
const { binding, url, verifier, ...rest } = flow;
|
|
124
154
|
const stored = { connectaOAuthFlow: FLOW_VERSION, ...rest };
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
if (this.sealer) {
|
|
156
|
+
stored.record = (yield authInvoke(this, "wait", [
|
|
157
|
+
authMethod(() => {
|
|
158
|
+
return authInvoke(this.sealer, "seal", [
|
|
159
|
+
this.flowRecordLabel(key, flow.epoch),
|
|
160
|
+
JSON.stringify({ binding, url, verifier }),
|
|
161
|
+
]);
|
|
162
|
+
}),
|
|
163
|
+
]));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// No key to protect it with: keep only what the callback must read.
|
|
167
|
+
// The consent URL is the first thing to go, so a repeated start mints
|
|
168
|
+
// a fresh consent rather than handing this one back.
|
|
169
|
+
if (binding)
|
|
170
|
+
stored.binding = withoutUrlSecrets(binding);
|
|
171
|
+
if (verifier !== undefined)
|
|
135
172
|
stored.verifier = verifier;
|
|
136
|
-
}
|
|
137
173
|
}
|
|
138
174
|
return JSON.stringify(stored);
|
|
139
175
|
}.bind(this));
|
|
@@ -146,7 +182,8 @@ export const consentMethods = {
|
|
|
146
182
|
typeof value.epoch !== "string" ||
|
|
147
183
|
typeof value.at !== "number" ||
|
|
148
184
|
(value.url !== undefined && typeof value.url !== "string") ||
|
|
149
|
-
(value.verifier !== undefined && typeof value.verifier !== "string")
|
|
185
|
+
(value.verifier !== undefined && typeof value.verifier !== "string") ||
|
|
186
|
+
(value.record !== undefined && typeof value.record !== "string")) {
|
|
150
187
|
return undefined;
|
|
151
188
|
}
|
|
152
189
|
const flow = {
|
|
@@ -156,23 +193,32 @@ export const consentMethods = {
|
|
|
156
193
|
...(typeof value.url === "string" ? { url: value.url } : {}),
|
|
157
194
|
...(value.consumed === true ? { consumed: true } : {}),
|
|
158
195
|
};
|
|
159
|
-
|
|
196
|
+
const sealedRecord = typeof value.record === "string";
|
|
197
|
+
if (!sealedRecord && (!withVerifier || typeof value.verifier !== "string"))
|
|
160
198
|
return flow;
|
|
161
|
-
if (value.sealed !== true)
|
|
199
|
+
if (!sealedRecord && value.sealed !== true)
|
|
162
200
|
return { ...flow, verifier: value.verifier };
|
|
163
201
|
try {
|
|
164
202
|
if (!this.sealer)
|
|
165
203
|
return undefined;
|
|
204
|
+
const opened = (yield authInvoke(this, "wait", [
|
|
205
|
+
authMethod(() => {
|
|
206
|
+
return authInvoke(this.sealer, "open", [
|
|
207
|
+
sealedRecord ? this.flowRecordLabel(key, flow.epoch) : this.flowLabel(key, flow.epoch),
|
|
208
|
+
(sealedRecord ? value.record : value.verifier),
|
|
209
|
+
]);
|
|
210
|
+
}),
|
|
211
|
+
]));
|
|
212
|
+
if (!sealedRecord)
|
|
213
|
+
return { ...flow, verifier: opened };
|
|
214
|
+
const record = parsed(opened);
|
|
215
|
+
if (!plainObject(record))
|
|
216
|
+
return undefined;
|
|
166
217
|
return {
|
|
167
218
|
...flow,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.flowLabel(key, flow.epoch),
|
|
172
|
-
value.verifier,
|
|
173
|
-
]);
|
|
174
|
-
}),
|
|
175
|
-
])),
|
|
219
|
+
...(plainObject(record.binding) ? { binding: record.binding } : {}),
|
|
220
|
+
...(typeof record.url === "string" ? { url: record.url } : {}),
|
|
221
|
+
...(withVerifier && typeof record.verifier === "string" ? { verifier: record.verifier } : {}),
|
|
176
222
|
};
|
|
177
223
|
}
|
|
178
224
|
catch {
|
|
@@ -258,7 +304,10 @@ export const consentMethods = {
|
|
|
258
304
|
const binding = this.configuredAuthorizationBinding() ?? {
|
|
259
305
|
...(yield authInvoke(this, "wait", [
|
|
260
306
|
authMethod(() => {
|
|
261
|
-
return authCall(discoveryBinding, [
|
|
307
|
+
return authCall(discoveryBinding, [
|
|
308
|
+
this.consentDiscovery ?? grant.body.discovery,
|
|
309
|
+
!this.sealer,
|
|
310
|
+
]);
|
|
262
311
|
}),
|
|
263
312
|
])),
|
|
264
313
|
issuer: discoveryIssuer(this.consentDiscovery) ?? grant.body.issuer,
|
|
@@ -442,7 +491,8 @@ export const consentMethods = {
|
|
|
442
491
|
if (grant.epoch !== epoch)
|
|
443
492
|
throw this.flowSuperseded();
|
|
444
493
|
(yield authInvoke(this, "checkFlowBinding", [grant]));
|
|
445
|
-
if (endpoint !== undefined &&
|
|
494
|
+
if (endpoint !== undefined &&
|
|
495
|
+
this.flowValue(this.callback?.flow.binding?.tokenEndpoint) !== this.flowValue(String(endpoint))) {
|
|
446
496
|
throw this.flowSuperseded();
|
|
447
497
|
}
|
|
448
498
|
// Opening ciphertext may yield. Re-read the exact record after opening,
|
|
@@ -16,6 +16,15 @@ const DISCONNECTED_EPOCH_PREFIX = "disconnected:";
|
|
|
16
16
|
const GRANT_VERSION = 3;
|
|
17
17
|
/** Attempts at one grant write while other writers keep winning. */
|
|
18
18
|
export const MAX_GRANT_WRITES = 32;
|
|
19
|
+
/** Token commits and their status fence are the same CAS, never separate writes. */
|
|
20
|
+
export function nextTokenGrant(grant, body) {
|
|
21
|
+
const revision = (grant.tokenRevision ?? 0) + 1;
|
|
22
|
+
if (!Number.isSafeInteger(revision))
|
|
23
|
+
throw new Error("OAuth grant revision exhausted");
|
|
24
|
+
const next = { ...grant, body, tokenRevision: revision };
|
|
25
|
+
delete next.observation;
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
19
28
|
export function plainObject(value) {
|
|
20
29
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21
30
|
}
|
|
@@ -32,6 +41,8 @@ function storedGrant(raw) {
|
|
|
32
41
|
return plainObject(value) &&
|
|
33
42
|
value.connectaOAuth === GRANT_VERSION &&
|
|
34
43
|
typeof value.epoch === "string" &&
|
|
44
|
+
(value.tokenRevision === undefined ||
|
|
45
|
+
(Number.isSafeInteger(value.tokenRevision) && value.tokenRevision >= 0)) &&
|
|
35
46
|
(value.flow === undefined || typeof value.flow === "string") &&
|
|
36
47
|
(value.sealed === undefined || typeof value.sealed === "string")
|
|
37
48
|
? value
|
|
@@ -105,6 +116,8 @@ export const grantStoreMethods = {
|
|
|
105
116
|
const stored = {
|
|
106
117
|
connectaOAuth: GRANT_VERSION,
|
|
107
118
|
epoch: grant.epoch,
|
|
119
|
+
...(grant.tokenRevision !== undefined ? { tokenRevision: grant.tokenRevision } : {}),
|
|
120
|
+
...(grant.observation ? { observation: grant.observation } : {}),
|
|
108
121
|
...(grant.flow !== undefined ? { flow: grant.flow } : {}),
|
|
109
122
|
};
|
|
110
123
|
if (Object.keys(grant.body).length > 0) {
|
|
@@ -160,6 +173,11 @@ export const grantStoreMethods = {
|
|
|
160
173
|
}
|
|
161
174
|
const grant = {
|
|
162
175
|
epoch: stored.epoch,
|
|
176
|
+
...(stored.tokenRevision !== undefined ? { tokenRevision: stored.tokenRevision } : {}),
|
|
177
|
+
...(stored.observation?.revision === (stored.tokenRevision ?? 0) &&
|
|
178
|
+
(stored.observation.outcome === "accepted" || stored.observation.outcome === "refused")
|
|
179
|
+
? { observation: { revision: stored.observation.revision, outcome: stored.observation.outcome } }
|
|
180
|
+
: {}),
|
|
163
181
|
...(stored.flow !== undefined ? { flow: stored.flow } : {}),
|
|
164
182
|
body: grantBody(body),
|
|
165
183
|
};
|
|
@@ -189,7 +207,12 @@ export const grantStoreMethods = {
|
|
|
189
207
|
authMethod(() => authStorage(this.storage, "get", [GRANT])),
|
|
190
208
|
]));
|
|
191
209
|
const grant = (yield authInvoke(this, "openGrant", [raw]));
|
|
192
|
-
return {
|
|
210
|
+
return {
|
|
211
|
+
epoch: grant.epoch,
|
|
212
|
+
...(grant.body.tokens ? { tokens: grant.body.tokens } : {}),
|
|
213
|
+
...(grant.body.issuer !== undefined ? { issuer: grant.body.issuer } : {}),
|
|
214
|
+
...(grant.observation ? { observation: grant.observation } : {}),
|
|
215
|
+
};
|
|
193
216
|
}.bind(this));
|
|
194
217
|
},
|
|
195
218
|
readGrant: function (signal) {
|
|
@@ -309,7 +332,8 @@ export const grantStoreMethods = {
|
|
|
309
332
|
if (refreshConsent)
|
|
310
333
|
body.refreshConsent = refreshConsent;
|
|
311
334
|
}
|
|
312
|
-
|
|
335
|
+
const next = this.withIssuer(grant, body);
|
|
336
|
+
return field === "tokens" ? nextTokenGrant(next, body) : next;
|
|
313
337
|
},
|
|
314
338
|
]));
|
|
315
339
|
if (field === "tokens")
|
|
@@ -318,6 +342,20 @@ export const grantStoreMethods = {
|
|
|
318
342
|
this.seen[field] = fingerprint(field === "client" ? value.value : value);
|
|
319
343
|
}.bind(this));
|
|
320
344
|
},
|
|
345
|
+
recordTokenOutcome: function (snapshot, outcome) {
|
|
346
|
+
if (outcome !== "accepted" && outcome !== "refused")
|
|
347
|
+
return Effect.void;
|
|
348
|
+
return authInvoke(this, "updateGrant", [
|
|
349
|
+
(grant) => {
|
|
350
|
+
if (grant.epoch !== snapshot.epoch || (grant.tokenRevision ?? 0) !== snapshot.revision || !grant.body.tokens)
|
|
351
|
+
return undefined;
|
|
352
|
+
if (grant.observation?.revision === snapshot.revision && grant.observation.outcome === outcome)
|
|
353
|
+
return undefined;
|
|
354
|
+
return { ...grant, observation: { revision: snapshot.revision, outcome } };
|
|
355
|
+
},
|
|
356
|
+
{ epoch: snapshot.epoch, quiet: true },
|
|
357
|
+
]);
|
|
358
|
+
},
|
|
321
359
|
bindingForGrant: function (grant) {
|
|
322
360
|
return authFlow(function* () {
|
|
323
361
|
const configured = this.configuredAuthorizationBinding();
|
|
@@ -326,7 +364,7 @@ export const grantStoreMethods = {
|
|
|
326
364
|
: {
|
|
327
365
|
...(yield authInvoke(this, "wait", [
|
|
328
366
|
authMethod(() => {
|
|
329
|
-
return authCall(discoveryBinding, [grant.body.discovery]);
|
|
367
|
+
return authCall(discoveryBinding, [grant.body.discovery, !this.sealer]);
|
|
330
368
|
}),
|
|
331
369
|
])),
|
|
332
370
|
issuer: grant.body.issuer,
|
|
@@ -338,7 +376,8 @@ export const grantStoreMethods = {
|
|
|
338
376
|
return authFlow(function* () {
|
|
339
377
|
const binding = this.callback?.flow.binding;
|
|
340
378
|
if (!binding ||
|
|
341
|
-
fingerprint(binding) !==
|
|
379
|
+
fingerprint(this.flowValue(binding)) !==
|
|
380
|
+
fingerprint(this.flowValue((yield authInvoke(this, "bindingForGrant", [grant]))))) {
|
|
342
381
|
throw this.flowSuperseded();
|
|
343
382
|
}
|
|
344
383
|
}.bind(this));
|
|
@@ -384,9 +423,14 @@ export const grantStoreMethods = {
|
|
|
384
423
|
};
|
|
385
424
|
}.bind(this));
|
|
386
425
|
},
|
|
387
|
-
resetAuthorization: function (operatorDisconnected = false, preserveClient = false) {
|
|
426
|
+
resetAuthorization: function (operatorDisconnected = false, preserveClient = false, refused) {
|
|
388
427
|
return authFlow(function* () {
|
|
389
|
-
const operation = authInvoke(this, "performReset", [
|
|
428
|
+
const operation = authInvoke(this, "performReset", [
|
|
429
|
+
operatorDisconnected,
|
|
430
|
+
preserveClient,
|
|
431
|
+
undefined,
|
|
432
|
+
refused,
|
|
433
|
+
]);
|
|
390
434
|
if (!this.onReset)
|
|
391
435
|
return (yield operation);
|
|
392
436
|
const reset = runEdge(operation);
|
|
@@ -404,11 +448,18 @@ export const grantStoreMethods = {
|
|
|
404
448
|
return (yield authValue(() => reset));
|
|
405
449
|
}.bind(this));
|
|
406
450
|
},
|
|
407
|
-
performReset: function (operatorDisconnected, preserveClient, revoke) {
|
|
451
|
+
performReset: function (operatorDisconnected, preserveClient, revoke, refused) {
|
|
408
452
|
return authFlow(function* () {
|
|
409
453
|
const epoch = `${operatorDisconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX}${crypto.randomUUID()}`;
|
|
410
454
|
for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
|
|
411
455
|
const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
|
|
456
|
+
// Compare again on every CAS attempt: recovery may have changed the
|
|
457
|
+
// epoch, token revision, or observation since Connect read it.
|
|
458
|
+
if (refused &&
|
|
459
|
+
(grant.epoch !== refused.epoch ||
|
|
460
|
+
(grant.tokenRevision ?? 0) !== refused.revision ||
|
|
461
|
+
grant.observation?.outcome !== "refused"))
|
|
462
|
+
return;
|
|
412
463
|
const carried = preserveClient && !operatorDisconnected ? this.carriableClient(grant) : undefined;
|
|
413
464
|
const encoded = (yield authInvoke(this, "encodeGrant", [{ epoch, body: carried ?? {} }, null]));
|
|
414
465
|
let written;
|
|
@@ -188,8 +188,11 @@ export class OAuthRefreshCoordinator {
|
|
|
188
188
|
// Production adapters resolve the required host policy before a code or
|
|
189
189
|
// refresh claim can consume its single-use credential. Custom SDK leaves
|
|
190
190
|
// may supply their own policy; this hook performs no I/O and owns no data.
|
|
191
|
+
// Each also registers the request's credentials before this point, as `api()`
|
|
192
|
+
// does in this hook: a refresh outlives its caller once claimed, and a sealing
|
|
193
|
+
// run must not publish ahead of that. `remoteMcp()` does so around its SDK flows.
|
|
191
194
|
if (isOAuthCredentialRequest(init))
|
|
192
|
-
prepareCredentialRequest?.();
|
|
195
|
+
prepareCredentialRequest?.(input, init);
|
|
193
196
|
if (isRefreshTokenRequest(init)) {
|
|
194
197
|
const signal = requestSignal && init?.signal
|
|
195
198
|
? AbortSignal.any([requestSignal, init.signal])
|
|
@@ -4,7 +4,7 @@ import { fromSignal } from "../../runtime/run.js";
|
|
|
4
4
|
import { OAUTH_REFRESH_LEASE_SECONDS, oauthRefreshActiveKeys, oauthRefreshKeys, oauthRefreshSpentKeys, } from "../../storage/keys.js";
|
|
5
5
|
import { authCall, authCatch, authFlow, authInvoke, authMethod, authStorage, authStorageMany } from "../native.js";
|
|
6
6
|
import { tokenExpiry } from "../oauth-grant.js";
|
|
7
|
-
import { GRANT, MAX_GRANT_WRITES, epochOf, parsed, plainObject } from "./grant-store.js";
|
|
7
|
+
import { nextTokenGrant, GRANT, MAX_GRANT_WRITES, epochOf, parsed, plainObject } from "./grant-store.js";
|
|
8
8
|
import { aborted, refreshTokensDigest, refreshWait } from "./refresh-coordinator.js";
|
|
9
9
|
import { oauthStateDigest } from "./consent.js";
|
|
10
10
|
export function storedRefreshSpent(raw) {
|
|
@@ -378,10 +378,11 @@ export const refreshRecordMethods = {
|
|
|
378
378
|
const committed = issuer !== undefined ? { ...tokens, issuer } : tokens;
|
|
379
379
|
const { expiresAt: _previous, ...body } = grant.body;
|
|
380
380
|
const expiresAt = tokenExpiry(committed, issuedAt);
|
|
381
|
-
const next = {
|
|
382
|
-
...
|
|
383
|
-
|
|
384
|
-
|
|
381
|
+
const next = nextTokenGrant(grant, {
|
|
382
|
+
...body,
|
|
383
|
+
tokens: committed,
|
|
384
|
+
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
385
|
+
});
|
|
385
386
|
const encoded = (yield authInvoke(this, "encodeGrant", [next, null]));
|
|
386
387
|
if ((yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded]))) {
|
|
387
388
|
this.observe(next);
|