@fungi.computer/veil 0.4.0 → 0.5.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/dist/connections/cleanup.d.ts +21 -0
- package/dist/connections/cleanup.js +66 -0
- package/dist/connections/contract.d.ts +153 -7
- package/dist/connections/contract.js +42 -2
- package/dist/connections/core.d.ts +55 -5
- package/dist/connections/core.js +365 -106
- package/dist/connections/discover.d.ts +3 -0
- package/dist/connections/discover.js +23 -0
- package/dist/connections/egress.js +3 -2
- package/dist/connections/index.d.ts +1 -0
- package/dist/connections/index.js +1 -0
- package/dist/connections/internal.d.ts +78 -0
- package/dist/connections/internal.js +66 -0
- package/dist/connections/mcp-agent.d.ts +37 -0
- package/dist/connections/mcp-agent.js +75 -0
- package/dist/connections/nango.d.ts +30 -0
- package/dist/connections/nango.js +136 -0
- package/dist/connections/oauth-flow.d.ts +81 -0
- package/dist/connections/oauth-flow.js +322 -0
- package/dist/connections/oauth-provider.d.ts +31 -0
- package/dist/connections/oauth-provider.js +96 -0
- package/dist/connections/oauth.d.ts +93 -0
- package/dist/connections/oauth.js +239 -0
- package/dist/connections/openapi.d.ts +36 -0
- package/dist/connections/openapi.js +120 -0
- package/dist/connections/sealed.js +23 -4
- package/dist/connections/source.d.ts +17 -0
- package/dist/connections/source.js +111 -0
- package/dist/connections/update.d.ts +41 -0
- package/dist/connections/update.js +81 -0
- package/dist/gatekeeper.d.ts +21 -1
- package/dist/gatekeeper.js +74 -12
- package/dist/nango.d.ts +41 -0
- package/dist/nango.js +6 -6
- package/package.json +11 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type * as Schema from "effect/Schema";
|
|
3
|
+
import type { ConnectionsOptions } from "./contract.js";
|
|
4
|
+
import { type AttemptRow, type ConnectionRow } from "./internal.js";
|
|
5
|
+
/** The cleanup outbox, attempt retirement and reconnect marking over the owner's SQLite. */
|
|
6
|
+
export declare function createCleanup(deps: {
|
|
7
|
+
ownerId: string;
|
|
8
|
+
custody: ConnectionsOptions["custody"];
|
|
9
|
+
clock: () => number;
|
|
10
|
+
sql: ConnectionsOptions["store"]["sql"];
|
|
11
|
+
transactionSync: <A>(body: () => A) => A;
|
|
12
|
+
rows: <S extends Schema.Decoder<unknown>>(schema: S, statement: string, ...bindings: (string | number | null)[]) => S["Type"][];
|
|
13
|
+
bump: () => void;
|
|
14
|
+
}): {
|
|
15
|
+
cleanupPending: () => number;
|
|
16
|
+
sweep: () => Effect.Effect<void, import("./contract.js").ConnectionsError, never>;
|
|
17
|
+
enqueueLocator: (connectionId: string, locator: string, credential: ConnectionRow["credential"]) => void;
|
|
18
|
+
enqueueCleanup: (row: ConnectionRow) => void;
|
|
19
|
+
markReconnect: (id: string) => void;
|
|
20
|
+
retireAttempt: (attempt: AttemptRow, status: string) => void;
|
|
21
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { attemptRow, cleanupRow, connectionRow, count, sync, } from "./internal.js";
|
|
3
|
+
const cleanupLimit = 5;
|
|
4
|
+
/** The cleanup outbox, attempt retirement and reconnect marking over the owner's SQLite. */
|
|
5
|
+
export function createCleanup(deps) {
|
|
6
|
+
const { sql, rows, bump, transactionSync } = deps;
|
|
7
|
+
/** Sealed credentials still waiting to be deleted; hosts wake their alarm while this is positive. */
|
|
8
|
+
const cleanupPending = () => rows(count, "SELECT COUNT(*) AS n FROM veil_connection_cleanup WHERE attempts < ?", cleanupLimit)[0]?.n ?? 0;
|
|
9
|
+
const sweep = () => Effect.gen(function* () {
|
|
10
|
+
yield* sync(expireAttempts);
|
|
11
|
+
const due = yield* sync(() => rows(cleanupRow, "SELECT * FROM veil_connection_cleanup WHERE attempts < ?", cleanupLimit));
|
|
12
|
+
for (const job of due) {
|
|
13
|
+
const outcome = yield* deps.custody
|
|
14
|
+
.remove({
|
|
15
|
+
ownerId: deps.ownerId,
|
|
16
|
+
connectionId: job.connectionId,
|
|
17
|
+
credential: job.credential,
|
|
18
|
+
}, job.locator)
|
|
19
|
+
.pipe(Effect.result);
|
|
20
|
+
yield* sync(() => {
|
|
21
|
+
if (outcome._tag === "Success")
|
|
22
|
+
sql.exec("DELETE FROM veil_connection_cleanup WHERE id = ?", job.id);
|
|
23
|
+
else
|
|
24
|
+
sql.exec("UPDATE veil_connection_cleanup SET attempts = attempts + 1 WHERE id = ?", job.id);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const enqueueLocator = (connectionId, locator, credential) => {
|
|
29
|
+
sql.exec("INSERT INTO veil_connection_cleanup (connectionId, locator, credential, attempts) VALUES (?, ?, ?, 0)", connectionId, locator, JSON.stringify(credential));
|
|
30
|
+
};
|
|
31
|
+
const enqueueCleanup = (row) => {
|
|
32
|
+
if (row.locator !== null)
|
|
33
|
+
enqueueLocator(row.id, row.locator, row.credential);
|
|
34
|
+
};
|
|
35
|
+
const markReconnect = (id) => {
|
|
36
|
+
sql.exec("UPDATE veil_connections SET state = 'reconnect_required', authorityVersion = authorityVersion + 1 WHERE id = ? AND state = 'active'", id);
|
|
37
|
+
bump();
|
|
38
|
+
};
|
|
39
|
+
/** An attempt ends: its sealed verifier goes to cleanup and a still-pending connection is withdrawn. */
|
|
40
|
+
const retireAttempt = (attempt, status) => {
|
|
41
|
+
sql.exec("UPDATE veil_connection_attempts SET status = ?, pkceVerifierSealed = NULL WHERE state = ?", status, attempt.state);
|
|
42
|
+
const row = rows(connectionRow, "SELECT * FROM veil_connections WHERE id = ?", attempt.connectionId)[0];
|
|
43
|
+
if (row === undefined)
|
|
44
|
+
return;
|
|
45
|
+
if (attempt.pkceVerifierSealed !== null)
|
|
46
|
+
enqueueLocator(row.id, attempt.pkceVerifierSealed, row.credential);
|
|
47
|
+
if (row.state === "pending") {
|
|
48
|
+
sql.exec("UPDATE veil_connections SET state = 'revoked', authorityVersion = authorityVersion + 1 WHERE id = ?", row.id);
|
|
49
|
+
bump();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const expireAttempts = () => {
|
|
53
|
+
transactionSync(() => {
|
|
54
|
+
for (const attempt of rows(attemptRow, "SELECT * FROM veil_connection_attempts WHERE status IN ('pending', 'consumed') AND expiresAt <= ?", deps.clock()))
|
|
55
|
+
retireAttempt(attempt, "expired");
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
cleanupPending,
|
|
60
|
+
sweep,
|
|
61
|
+
enqueueLocator,
|
|
62
|
+
enqueueCleanup,
|
|
63
|
+
markReconnect,
|
|
64
|
+
retireAttempt,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
3
|
import type { PublicEgressPolicy } from "../public-egress-policy.js";
|
|
4
|
-
export type ConnectionsRefusal = "invalid_input" | "denied" | "revoked" | "conflict" | "unsupported" | "too_large" | "reconnect_required" | "invalid_response" | "unavailable";
|
|
4
|
+
export type ConnectionsRefusal = "invalid_input" | "denied" | "revoked" | "conflict" | "unsupported" | "too_large" | "reconnect_required" | "pending" | "invalid_response" | "unavailable";
|
|
5
5
|
/** Fixed codes only: upstream bodies, URLs and credential bytes never enter errors. */
|
|
6
6
|
export declare class ConnectionsError extends Error {
|
|
7
7
|
readonly code: ConnectionsRefusal;
|
|
@@ -15,7 +15,11 @@ declare const placementSchema: Schema.Union<readonly [Schema.Literal<"bearer">,
|
|
|
15
15
|
readonly query: Schema.String;
|
|
16
16
|
}>]>;
|
|
17
17
|
export type Placement = typeof placementSchema.Type;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* How the final request is authorized. For `oauth` the owner may bring its own
|
|
20
|
+
* client (`clientId`, with the client secret as the draft's `secret`); without
|
|
21
|
+
* one the server's dynamic registration is used.
|
|
22
|
+
*/
|
|
19
23
|
export declare const credentialSchema: Schema.Union<readonly [Schema.Struct<{
|
|
20
24
|
readonly kind: Schema.Literal<"none">;
|
|
21
25
|
}>, Schema.Struct<{
|
|
@@ -25,6 +29,12 @@ export declare const credentialSchema: Schema.Union<readonly [Schema.Struct<{
|
|
|
25
29
|
}>, Schema.Struct<{
|
|
26
30
|
readonly query: Schema.String;
|
|
27
31
|
}>]>;
|
|
32
|
+
}>, Schema.Struct<{
|
|
33
|
+
readonly kind: Schema.Literal<"oauth">;
|
|
34
|
+
readonly scope: Schema.optional<Schema.String>;
|
|
35
|
+
readonly clientId: Schema.optional<Schema.String>;
|
|
36
|
+
/** An operator-registered REST provider; absent for an MCP server's own sign-in. */
|
|
37
|
+
readonly provider: Schema.optional<Schema.String>;
|
|
28
38
|
}>]>;
|
|
29
39
|
export type Credential = typeof credentialSchema.Type;
|
|
30
40
|
export type SurfaceView = Readonly<{
|
|
@@ -33,21 +43,27 @@ export type SurfaceView = Readonly<{
|
|
|
33
43
|
}> | Readonly<{
|
|
34
44
|
kind: "openapi";
|
|
35
45
|
baseUrl: string;
|
|
36
|
-
table: unknown;
|
|
46
|
+
table: readonly unknown[];
|
|
47
|
+
/** Digest of the decoded operation table. */
|
|
48
|
+
digest: string;
|
|
37
49
|
}>;
|
|
38
|
-
export declare const toolsSchema: Schema.Union<readonly [Schema.Literal<"all">, Schema
|
|
50
|
+
export declare const toolsSchema: Schema.Union<readonly [Schema.Literal<"all">, Schema.Struct<{
|
|
51
|
+
readonly effect: Schema.Literal<"read">;
|
|
52
|
+
}>, Schema.$Array<Schema.String>]>;
|
|
39
53
|
export type Tools = typeof toolsSchema.Type;
|
|
40
54
|
export declare const draftSchema: Schema.Struct<{
|
|
41
55
|
/** Idempotency key: a replay with the same request returns the same connection. */
|
|
42
56
|
readonly requestId: Schema.String;
|
|
43
57
|
readonly name: Schema.String;
|
|
58
|
+
/** Display text for owners; defaults to the name. */
|
|
59
|
+
readonly label: Schema.optional<Schema.String>;
|
|
44
60
|
readonly surface: Schema.Union<readonly [Schema.Struct<{
|
|
45
61
|
readonly kind: Schema.Literal<"mcp">;
|
|
46
62
|
readonly url: Schema.String;
|
|
47
63
|
}>, Schema.Struct<{
|
|
48
64
|
readonly kind: Schema.Literal<"openapi">;
|
|
49
65
|
readonly baseUrl: Schema.String;
|
|
50
|
-
readonly table: Schema.Unknown
|
|
66
|
+
readonly table: Schema.$Array<Schema.Unknown>;
|
|
51
67
|
}>]>;
|
|
52
68
|
readonly credential: Schema.Union<readonly [Schema.Struct<{
|
|
53
69
|
readonly kind: Schema.Literal<"none">;
|
|
@@ -58,6 +74,12 @@ export declare const draftSchema: Schema.Struct<{
|
|
|
58
74
|
}>, Schema.Struct<{
|
|
59
75
|
readonly query: Schema.String;
|
|
60
76
|
}>]>;
|
|
77
|
+
}>, Schema.Struct<{
|
|
78
|
+
readonly kind: Schema.Literal<"oauth">;
|
|
79
|
+
readonly scope: Schema.optional<Schema.String>;
|
|
80
|
+
readonly clientId: Schema.optional<Schema.String>;
|
|
81
|
+
/** An operator-registered REST provider; absent for an MCP server's own sign-in. */
|
|
82
|
+
readonly provider: Schema.optional<Schema.String>;
|
|
61
83
|
}>]>;
|
|
62
84
|
readonly secret: Schema.optional<Schema.String>;
|
|
63
85
|
}>;
|
|
@@ -65,9 +87,54 @@ export type ConnectionDraft = typeof draftSchema.Type;
|
|
|
65
87
|
export declare const grantInputSchema: Schema.Struct<{
|
|
66
88
|
readonly connectionId: Schema.String;
|
|
67
89
|
readonly agentId: Schema.String;
|
|
68
|
-
readonly tools: Schema.Union<readonly [Schema.Literal<"all">, Schema
|
|
90
|
+
readonly tools: Schema.Union<readonly [Schema.Literal<"all">, Schema.Struct<{
|
|
91
|
+
readonly effect: Schema.Literal<"read">;
|
|
92
|
+
}>, Schema.$Array<Schema.String>]>;
|
|
69
93
|
}>;
|
|
70
94
|
export type GrantInput = typeof grantInputSchema.Type;
|
|
95
|
+
/** The authorization server's redirect back to the owner. */
|
|
96
|
+
export declare const completeInputSchema: Schema.Struct<{
|
|
97
|
+
readonly state: Schema.String;
|
|
98
|
+
readonly code: Schema.String;
|
|
99
|
+
readonly iss: Schema.optional<Schema.String>;
|
|
100
|
+
}>;
|
|
101
|
+
export type CompleteInput = typeof completeInputSchema.Type;
|
|
102
|
+
/** Owner edits to a live connection, guarded by the version the owner last saw. */
|
|
103
|
+
export declare const updateInputSchema: Schema.Struct<{
|
|
104
|
+
readonly requestId: Schema.String;
|
|
105
|
+
readonly connectionId: Schema.String;
|
|
106
|
+
readonly expectedVersion: Schema.Number;
|
|
107
|
+
readonly label: Schema.optional<Schema.String>;
|
|
108
|
+
readonly name: Schema.optional<Schema.String>;
|
|
109
|
+
/** Replacement OpenAPI operation table, decoded by Forage. */
|
|
110
|
+
readonly table: Schema.optional<Schema.$Array<Schema.Unknown>>;
|
|
111
|
+
readonly baseUrl: Schema.optional<Schema.String>;
|
|
112
|
+
/** Credential swap; `none` and `static` only. */
|
|
113
|
+
readonly credential: Schema.optional<Schema.Union<readonly [Schema.Struct<{
|
|
114
|
+
readonly kind: Schema.Literal<"none">;
|
|
115
|
+
}>, Schema.Struct<{
|
|
116
|
+
readonly kind: Schema.Literal<"static">;
|
|
117
|
+
readonly placement: Schema.Union<readonly [Schema.Literal<"bearer">, Schema.Struct<{
|
|
118
|
+
readonly header: Schema.String;
|
|
119
|
+
}>, Schema.Struct<{
|
|
120
|
+
readonly query: Schema.String;
|
|
121
|
+
}>]>;
|
|
122
|
+
}>, Schema.Struct<{
|
|
123
|
+
readonly kind: Schema.Literal<"oauth">;
|
|
124
|
+
readonly scope: Schema.optional<Schema.String>;
|
|
125
|
+
readonly clientId: Schema.optional<Schema.String>;
|
|
126
|
+
/** An operator-registered REST provider; absent for an MCP server's own sign-in. */
|
|
127
|
+
readonly provider: Schema.optional<Schema.String>;
|
|
128
|
+
}>]>>;
|
|
129
|
+
readonly secret: Schema.optional<Schema.String>;
|
|
130
|
+
}>;
|
|
131
|
+
export type UpdateInput = typeof updateInputSchema.Type;
|
|
132
|
+
/** One typed OpenAPI operation call. */
|
|
133
|
+
export declare const callInputSchema: Schema.Struct<{
|
|
134
|
+
readonly operation: Schema.String;
|
|
135
|
+
readonly input: Schema.Unknown;
|
|
136
|
+
}>;
|
|
137
|
+
export type CallInput = typeof callInputSchema.Type;
|
|
71
138
|
export declare const egressRefSchema: Schema.Struct<{
|
|
72
139
|
readonly connectionId: Schema.String;
|
|
73
140
|
readonly grantVersion: Schema.Number;
|
|
@@ -87,6 +154,35 @@ export type CustodyAdapter = Readonly<{
|
|
|
87
154
|
put: (address: CustodyAddress, secret: string) => Effect.Effect<string, ConnectionsError>;
|
|
88
155
|
inject: (address: CustodyAddress, locator: string, request: Request) => Effect.Effect<Response, ConnectionsError>;
|
|
89
156
|
remove: (address: CustodyAddress, locator: string) => Effect.Effect<"deleted" | "absent", ConnectionsError>;
|
|
157
|
+
/**
|
|
158
|
+
* Release a sealed OAuth bundle to the owner's connections core, which runs
|
|
159
|
+
* the authorization flow and refreshes tokens. Never reaches an Agent.
|
|
160
|
+
*/
|
|
161
|
+
open?: (address: CustodyAddress, locator: string) => Effect.Effect<string, ConnectionsError>;
|
|
162
|
+
/**
|
|
163
|
+
* A custodian that runs the authorization itself (Nango's Connect) returns
|
|
164
|
+
* the page to send the owner to; it settles through a signed completion
|
|
165
|
+
* (`complete`) carrying the `state` given here, not through our callback.
|
|
166
|
+
*/
|
|
167
|
+
connect?: (address: CustodyAddress, attempt: Readonly<{
|
|
168
|
+
state: string;
|
|
169
|
+
actor: string;
|
|
170
|
+
integrationId: string;
|
|
171
|
+
}>) => Effect.Effect<Readonly<{
|
|
172
|
+
connectUrl: string;
|
|
173
|
+
expiresAt: number;
|
|
174
|
+
}>, ConnectionsError>;
|
|
175
|
+
}>;
|
|
176
|
+
/**
|
|
177
|
+
* What a custodian proves after it ran an authorization: the attempt's state
|
|
178
|
+
* and where it keeps the credential. The host verifies the custodian's
|
|
179
|
+
* signature before it builds one.
|
|
180
|
+
*/
|
|
181
|
+
export type CustodyCompletion = Readonly<{
|
|
182
|
+
state: string;
|
|
183
|
+
locator: string;
|
|
184
|
+
/** The integration the custodian ran it through; it must be the one the attempt began with. */
|
|
185
|
+
integrationId: string;
|
|
90
186
|
}>;
|
|
91
187
|
export type SqlValue = string | number | null;
|
|
92
188
|
/** The owner's SQLite in Cloudflare `ctx.storage` shape. */
|
|
@@ -98,13 +194,54 @@ export type SqlStore = Readonly<{
|
|
|
98
194
|
}>;
|
|
99
195
|
transactionSync: <A>(operation: () => A) => A;
|
|
100
196
|
}>;
|
|
197
|
+
/**
|
|
198
|
+
* A REST provider the operator registered an app with. Such providers publish
|
|
199
|
+
* no metadata the SDK can discover, and many require a pre-registered client,
|
|
200
|
+
* so the host names the endpoints and holds the client. Only the operator's
|
|
201
|
+
* configuration reaches this type; neither an Agent nor an owner supplies one.
|
|
202
|
+
*/
|
|
203
|
+
export type StaticOAuthProvider = Readonly<{
|
|
204
|
+
issuer: string;
|
|
205
|
+
authorizationEndpoint: string;
|
|
206
|
+
tokenEndpoint: string;
|
|
207
|
+
/** Scopes requested when the owner names none. */
|
|
208
|
+
scope?: string;
|
|
209
|
+
client: {
|
|
210
|
+
client_id: string;
|
|
211
|
+
client_secret?: string | undefined;
|
|
212
|
+
};
|
|
213
|
+
}>;
|
|
101
214
|
export type ConnectionsOptions = Readonly<{
|
|
102
215
|
ownerId: string;
|
|
103
216
|
store: SqlStore;
|
|
104
217
|
custody: CustodyAdapter;
|
|
105
218
|
/** Host authority read: may this actor manage connections, or still stand as a grantor? */
|
|
106
219
|
authorize: (actorId: string, purpose: "manage" | "grantor") => boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Asked right before every send, with the grantor who stands behind the
|
|
222
|
+
* grant. The host keeps its own live recheck here (membership, write
|
|
223
|
+
* rights); the core never learns what the host's policy is.
|
|
224
|
+
*/
|
|
225
|
+
check?: (grantor: string, grant: Readonly<{
|
|
226
|
+
agentId: string;
|
|
227
|
+
connectionId: string;
|
|
228
|
+
tools: Tools;
|
|
229
|
+
}>, operation: Readonly<{
|
|
230
|
+
name: string;
|
|
231
|
+
effect: "read" | "write" | null;
|
|
232
|
+
}> | null) => Promise<boolean>;
|
|
107
233
|
egressPolicy?: PublicEgressPolicy;
|
|
234
|
+
/**
|
|
235
|
+
* Needed only for `oauth` connections: where the authorization server sends
|
|
236
|
+
* the owner back, and the checked public transport for discovery, token and
|
|
237
|
+
* refresh requests. Custody must also implement `open`.
|
|
238
|
+
*/
|
|
239
|
+
oauth?: Readonly<{
|
|
240
|
+
callbackUrl: string;
|
|
241
|
+
fetch: (request: Request) => Promise<Response>;
|
|
242
|
+
/** REST providers the operator registered an app with, by id. */
|
|
243
|
+
providers?: Readonly<Record<string, StaticOAuthProvider>>;
|
|
244
|
+
}> | undefined;
|
|
108
245
|
clock: () => number;
|
|
109
246
|
ids: () => string;
|
|
110
247
|
}>;
|
|
@@ -116,6 +253,8 @@ export type ConnectionProjection = Readonly<{
|
|
|
116
253
|
name: string;
|
|
117
254
|
surface: SurfaceView;
|
|
118
255
|
tools: Tools;
|
|
256
|
+
/** Bumped by every `update`. */
|
|
257
|
+
version: number;
|
|
119
258
|
authorityVersion: number;
|
|
120
259
|
grantVersion: number;
|
|
121
260
|
}>[];
|
|
@@ -123,17 +262,24 @@ export type ConnectionProjection = Readonly<{
|
|
|
123
262
|
export type ConnectionView = Readonly<{
|
|
124
263
|
connectionId: string;
|
|
125
264
|
name: string;
|
|
265
|
+
label: string;
|
|
126
266
|
surface: Readonly<{
|
|
127
267
|
kind: "mcp" | "openapi";
|
|
128
268
|
origin: string;
|
|
269
|
+
url: string;
|
|
270
|
+
digest: string | null;
|
|
129
271
|
}>;
|
|
130
272
|
credentialKind: Credential["kind"];
|
|
131
|
-
|
|
273
|
+
/** How the secret is placed; the secret itself never appears. */
|
|
274
|
+
credential: Credential;
|
|
275
|
+
state: "pending" | "active" | "reconnect_required" | "revoked";
|
|
276
|
+
version: number;
|
|
132
277
|
authorityVersion: number;
|
|
133
278
|
grants: readonly Readonly<{
|
|
134
279
|
agentId: string;
|
|
135
280
|
tools: Tools;
|
|
136
281
|
version: number;
|
|
282
|
+
grantor: string;
|
|
137
283
|
}>[];
|
|
138
284
|
}>;
|
|
139
285
|
/** The Agent's narrowed capability: only the owner opens the credential. */
|
|
@@ -18,10 +18,21 @@ const placementSchema = Schema.Union([
|
|
|
18
18
|
Schema.Struct({ header: headerName }),
|
|
19
19
|
Schema.Struct({ query: queryName }),
|
|
20
20
|
]);
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* How the final request is authorized. For `oauth` the owner may bring its own
|
|
23
|
+
* client (`clientId`, with the client secret as the draft's `secret`); without
|
|
24
|
+
* one the server's dynamic registration is used.
|
|
25
|
+
*/
|
|
22
26
|
export const credentialSchema = Schema.Union([
|
|
23
27
|
Schema.Struct({ kind: Schema.Literal("none") }),
|
|
24
28
|
Schema.Struct({ kind: Schema.Literal("static"), placement: placementSchema }),
|
|
29
|
+
Schema.Struct({
|
|
30
|
+
kind: Schema.Literal("oauth"),
|
|
31
|
+
scope: Schema.optional(Schema.String),
|
|
32
|
+
clientId: Schema.optional(Schema.String),
|
|
33
|
+
/** An operator-registered REST provider; absent for an MCP server's own sign-in. */
|
|
34
|
+
provider: Schema.optional(Schema.String),
|
|
35
|
+
}),
|
|
25
36
|
]);
|
|
26
37
|
/** What the Agent can call. The OpenAPI table is decoded by Forage before it is stored. */
|
|
27
38
|
const surfaceDraftSchema = Schema.Union([
|
|
@@ -29,17 +40,21 @@ const surfaceDraftSchema = Schema.Union([
|
|
|
29
40
|
Schema.Struct({
|
|
30
41
|
kind: Schema.Literal("openapi"),
|
|
31
42
|
baseUrl: Schema.String,
|
|
32
|
-
table: Schema.Unknown,
|
|
43
|
+
table: Schema.Array(Schema.Unknown),
|
|
33
44
|
}),
|
|
34
45
|
]);
|
|
35
46
|
export const toolsSchema = Schema.Union([
|
|
36
47
|
Schema.Literal("all"),
|
|
48
|
+
/** OpenAPI only: every operation whose stored effect is `read`. */
|
|
49
|
+
Schema.Struct({ effect: Schema.Literal("read") }),
|
|
37
50
|
Schema.Array(Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(256)))).pipe(Schema.check(Schema.isMaxLength(256))),
|
|
38
51
|
]);
|
|
39
52
|
export const draftSchema = Schema.Struct({
|
|
40
53
|
/** Idempotency key: a replay with the same request returns the same connection. */
|
|
41
54
|
requestId: identifier,
|
|
42
55
|
name: Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(128))),
|
|
56
|
+
/** Display text for owners; defaults to the name. */
|
|
57
|
+
label: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(128)))),
|
|
43
58
|
surface: surfaceDraftSchema,
|
|
44
59
|
credential: credentialSchema,
|
|
45
60
|
secret: Schema.optional(Schema.String.pipe(Schema.check(Schema.isPattern(/^[\x21-\x7e]{1,16384}$/u)))),
|
|
@@ -49,6 +64,31 @@ export const grantInputSchema = Schema.Struct({
|
|
|
49
64
|
agentId: identifier,
|
|
50
65
|
tools: toolsSchema,
|
|
51
66
|
});
|
|
67
|
+
/** The authorization server's redirect back to the owner. */
|
|
68
|
+
export const completeInputSchema = Schema.Struct({
|
|
69
|
+
state: Schema.String.pipe(Schema.check(Schema.isMaxLength(512))),
|
|
70
|
+
code: Schema.String.pipe(Schema.check(Schema.isMaxLength(4096))),
|
|
71
|
+
iss: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMaxLength(2048)))),
|
|
72
|
+
});
|
|
73
|
+
/** Owner edits to a live connection, guarded by the version the owner last saw. */
|
|
74
|
+
export const updateInputSchema = Schema.Struct({
|
|
75
|
+
requestId: identifier,
|
|
76
|
+
connectionId: identifier,
|
|
77
|
+
expectedVersion: version,
|
|
78
|
+
label: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(128)))),
|
|
79
|
+
name: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(128)))),
|
|
80
|
+
/** Replacement OpenAPI operation table, decoded by Forage. */
|
|
81
|
+
table: Schema.optional(Schema.Array(Schema.Unknown)),
|
|
82
|
+
baseUrl: Schema.optional(Schema.String),
|
|
83
|
+
/** Credential swap; `none` and `static` only. */
|
|
84
|
+
credential: Schema.optional(credentialSchema),
|
|
85
|
+
secret: Schema.optional(Schema.String.pipe(Schema.check(Schema.isPattern(/^[\x21-\x7e]{1,16384}$/u)))),
|
|
86
|
+
});
|
|
87
|
+
/** One typed OpenAPI operation call. */
|
|
88
|
+
export const callInputSchema = Schema.Struct({
|
|
89
|
+
operation: Schema.String.pipe(Schema.check(Schema.isMaxLength(256))),
|
|
90
|
+
input: Schema.Unknown,
|
|
91
|
+
});
|
|
52
92
|
export const egressRefSchema = Schema.Struct({
|
|
53
93
|
connectionId: identifier,
|
|
54
94
|
grantVersion: version,
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
-
import { ConnectionsError, type ConnectionDraft, type GrantInput, type ConnectionsOptions, type EgressRef, type NarrowFetch, type SurfaceView } from "./contract.js";
|
|
2
|
+
import { ConnectionsError, type CallInput, type ConnectionDraft, type Tools, type GrantInput, type ConnectionsOptions, type EgressRef, type NarrowFetch, type SurfaceView } from "./contract.js";
|
|
3
3
|
/** One owner's connections: attempts, connections, grants, receipts and cleanup in its SQLite. */
|
|
4
4
|
export declare function createConnections(options: ConnectionsOptions): {
|
|
5
5
|
begin: (actor: string, input: ConnectionDraft) => Effect.Effect<{
|
|
6
6
|
connectionId: string;
|
|
7
|
-
authorizeUrl: null;
|
|
7
|
+
authorizeUrl: string | null;
|
|
8
|
+
}, ConnectionsError, never>;
|
|
9
|
+
complete: (actor: string, input: import("./contract.js").CompleteInput) => Effect.Effect<{
|
|
10
|
+
connectionId: string;
|
|
11
|
+
}, ConnectionsError, never>;
|
|
12
|
+
completeCustody: (input: import("./contract.js").CustodyCompletion) => Effect.Effect<{
|
|
13
|
+
connectionId: string;
|
|
8
14
|
}, ConnectionsError, never>;
|
|
9
15
|
grant: (actor: string, input: GrantInput) => Effect.Effect<{
|
|
10
16
|
grantVersion: number;
|
|
@@ -22,7 +28,10 @@ export declare function createConnections(options: ConnectionsOptions): {
|
|
|
22
28
|
connectionId: string;
|
|
23
29
|
name: string;
|
|
24
30
|
surface: SurfaceView;
|
|
25
|
-
tools: readonly string[] | "all"
|
|
31
|
+
tools: readonly string[] | "all" | {
|
|
32
|
+
readonly effect: "read";
|
|
33
|
+
};
|
|
34
|
+
version: number;
|
|
26
35
|
authorityVersion: number;
|
|
27
36
|
grantVersion: number;
|
|
28
37
|
}[];
|
|
@@ -30,19 +39,60 @@ export declare function createConnections(options: ConnectionsOptions): {
|
|
|
30
39
|
list: (actor: string) => Effect.Effect<Readonly<{
|
|
31
40
|
connectionId: string;
|
|
32
41
|
name: string;
|
|
42
|
+
label: string;
|
|
33
43
|
surface: Readonly<{
|
|
34
44
|
kind: "mcp" | "openapi";
|
|
35
45
|
origin: string;
|
|
46
|
+
url: string;
|
|
47
|
+
digest: string | null;
|
|
36
48
|
}>;
|
|
37
49
|
credentialKind: import("./contract.js").Credential["kind"];
|
|
38
|
-
|
|
50
|
+
credential: import("./contract.js").Credential;
|
|
51
|
+
state: "pending" | "active" | "reconnect_required" | "revoked";
|
|
52
|
+
version: number;
|
|
39
53
|
authorityVersion: number;
|
|
40
54
|
grants: readonly Readonly<{
|
|
41
55
|
agentId: string;
|
|
42
|
-
tools:
|
|
56
|
+
tools: Tools;
|
|
43
57
|
version: number;
|
|
58
|
+
grantor: string;
|
|
44
59
|
}>[];
|
|
45
60
|
}>[], ConnectionsError, never>;
|
|
46
61
|
egress: (agentId: string, input: EgressRef) => NarrowFetch;
|
|
62
|
+
call: (agentId: string, input: EgressRef, raw: CallInput) => Effect.Effect<import("zod").JSONType, ConnectionsError, never>;
|
|
63
|
+
ownerCall: (actor: string, at: Readonly<{
|
|
64
|
+
connectionId: string;
|
|
65
|
+
expectedVersion: number;
|
|
66
|
+
}>, raw: CallInput) => Effect.Effect<import("zod").JSONType, ConnectionsError, never>;
|
|
67
|
+
tools: (actor: string, connectionId: string) => Effect.Effect<readonly string[], ConnectionsError, never>;
|
|
68
|
+
operations: (actor: string, id: string) => Effect.Effect<{
|
|
69
|
+
operations: {
|
|
70
|
+
status: "supported";
|
|
71
|
+
name: string;
|
|
72
|
+
method: "GET" | "HEAD" | "OPTIONS" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
73
|
+
path: string;
|
|
74
|
+
effect: "write" | "read";
|
|
75
|
+
description: string & import("zod").$brand<"ThirdPartyText">;
|
|
76
|
+
textSource: "third_party";
|
|
77
|
+
tags: (string & import("zod").$brand<"ThirdPartyText">)[];
|
|
78
|
+
parameters: {
|
|
79
|
+
name: string;
|
|
80
|
+
in: "header" | "path" | "query";
|
|
81
|
+
required: boolean;
|
|
82
|
+
schema: boolean | Record<string, import("zod").JSONType>;
|
|
83
|
+
style?: string | undefined;
|
|
84
|
+
explode?: boolean | undefined;
|
|
85
|
+
allowReserved?: boolean | undefined;
|
|
86
|
+
}[];
|
|
87
|
+
input: boolean | Record<string, import("zod").JSONType>;
|
|
88
|
+
output: boolean | Record<string, import("zod").JSONType>;
|
|
89
|
+
}[];
|
|
90
|
+
digest: string;
|
|
91
|
+
}, ConnectionsError, never>;
|
|
92
|
+
update: (actor: string, raw: import("./contract.js").UpdateInput) => Effect.Effect<{
|
|
93
|
+
connectionId: string;
|
|
94
|
+
version: number;
|
|
95
|
+
}, ConnectionsError, never>;
|
|
47
96
|
sweep: () => Effect.Effect<void, ConnectionsError, never>;
|
|
97
|
+
cleanupPending: () => number;
|
|
48
98
|
};
|