@fungi.computer/veil 0.3.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.
Files changed (43) hide show
  1. package/dist/connections/cleanup.d.ts +21 -0
  2. package/dist/connections/cleanup.js +66 -0
  3. package/dist/connections/contract.d.ts +287 -0
  4. package/dist/connections/contract.js +95 -0
  5. package/dist/connections/core.d.ts +98 -0
  6. package/dist/connections/core.js +631 -0
  7. package/dist/connections/discover.d.ts +3 -0
  8. package/dist/connections/discover.js +23 -0
  9. package/dist/connections/egress.d.ts +8 -0
  10. package/dist/connections/egress.js +78 -0
  11. package/dist/connections/index.d.ts +4 -0
  12. package/dist/connections/index.js +4 -0
  13. package/dist/connections/internal.d.ts +78 -0
  14. package/dist/connections/internal.js +66 -0
  15. package/dist/connections/mcp-agent.d.ts +37 -0
  16. package/dist/connections/mcp-agent.js +75 -0
  17. package/dist/connections/nango.d.ts +30 -0
  18. package/dist/connections/nango.js +136 -0
  19. package/dist/connections/oauth-flow.d.ts +81 -0
  20. package/dist/connections/oauth-flow.js +322 -0
  21. package/dist/connections/oauth-provider.d.ts +31 -0
  22. package/dist/connections/oauth-provider.js +96 -0
  23. package/dist/connections/oauth.d.ts +93 -0
  24. package/dist/connections/oauth.js +239 -0
  25. package/dist/connections/openapi.d.ts +36 -0
  26. package/dist/connections/openapi.js +120 -0
  27. package/dist/connections/sealed.d.ts +21 -0
  28. package/dist/connections/sealed.js +73 -0
  29. package/dist/connections/source.d.ts +17 -0
  30. package/dist/connections/source.js +111 -0
  31. package/dist/connections/update.d.ts +41 -0
  32. package/dist/connections/update.js +81 -0
  33. package/dist/credential-store.d.ts +64 -0
  34. package/dist/credential-store.js +151 -0
  35. package/dist/gatekeeper.d.ts +21 -1
  36. package/dist/gatekeeper.js +74 -12
  37. package/dist/index.d.ts +1 -1
  38. package/dist/model-keys.d.ts +24 -0
  39. package/dist/nango.d.ts +41 -0
  40. package/dist/nango.js +6 -6
  41. package/dist/openapi-secrets.d.ts +1 -1
  42. package/dist/openapi-secrets.js +19 -58
  43. package/package.json +22 -1
@@ -0,0 +1,631 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import { assertPublicHostName } from "../model-key-network.js";
4
+ import { createPublicEgressPolicy } from "../public-egress-policy.js";
5
+ import { ConnectionsError, callInputSchema, completeInputSchema, draftSchema, egressRefSchema, grantInputSchema, identifier, toolsSchema, } from "./contract.js";
6
+ import { admitRequest } from "./egress.js";
7
+ import { admits, boundedText, decodeTable, operationOutput, operationRequest, } from "./openapi.js";
8
+ import { createCleanup } from "./cleanup.js";
9
+ import { attemptRow, connectionRow, count, decode, io, json, registryRow, sync, } from "./internal.js";
10
+ import { createOAuthFlow } from "./oauth-flow.js";
11
+ import { listMcpTools } from "./discover.js";
12
+ import { createUpdate } from "./update.js";
13
+ const attemptTtlMs = 10 * 60 * 1000;
14
+ const grantRow = Schema.Struct({
15
+ connectionId: identifier,
16
+ agentId: identifier,
17
+ tools: json(toolsSchema),
18
+ grantor: identifier,
19
+ version: Schema.Number,
20
+ active: Schema.Literals([0, 1]),
21
+ });
22
+ const receiptRow = Schema.Struct({
23
+ fingerprint: Schema.String,
24
+ result: json(Schema.Struct({
25
+ connectionId: identifier,
26
+ authorizeUrl: Schema.optional(Schema.String),
27
+ version: Schema.optional(Schema.Number),
28
+ })),
29
+ });
30
+ /** One owner's connections: attempts, connections, grants, receipts and cleanup in its SQLite. */
31
+ export function createConnections(options) {
32
+ const ownerId = decode(identifier, options.ownerId);
33
+ const { sql } = options.store;
34
+ // A Durable Object's storage must be called as a method, never detached.
35
+ const transactionSync = (body) => options.store.transactionSync(body);
36
+ const policy = createPublicEgressPolicy(options.egressPolicy);
37
+ sql.exec(`
38
+ CREATE TABLE IF NOT EXISTS veil_connection_attempts (
39
+ state TEXT PRIMARY KEY, connectionId TEXT NOT NULL, actor TEXT NOT NULL,
40
+ status TEXT NOT NULL, expiresAt INTEGER NOT NULL,
41
+ pkceVerifierSealed TEXT, issuer TEXT) STRICT;
42
+ CREATE TABLE IF NOT EXISTS veil_connections (
43
+ id TEXT PRIMARY KEY, name TEXT NOT NULL, surface TEXT NOT NULL, credential TEXT NOT NULL,
44
+ locator TEXT, state TEXT NOT NULL, authorityVersion INTEGER NOT NULL,
45
+ version INTEGER NOT NULL) STRICT;
46
+ CREATE TABLE IF NOT EXISTS veil_connection_grants (
47
+ connectionId TEXT NOT NULL, agentId TEXT NOT NULL, tools TEXT NOT NULL,
48
+ grantor TEXT NOT NULL, version INTEGER NOT NULL, active INTEGER NOT NULL,
49
+ PRIMARY KEY (connectionId, agentId)) STRICT;
50
+ CREATE TABLE IF NOT EXISTS veil_connection_receipts (
51
+ requestId TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, result TEXT NOT NULL) STRICT;
52
+ CREATE TABLE IF NOT EXISTS veil_connection_cleanup (
53
+ id INTEGER PRIMARY KEY AUTOINCREMENT, connectionId TEXT NOT NULL, locator TEXT NOT NULL,
54
+ credential TEXT NOT NULL, attempts INTEGER NOT NULL) STRICT;
55
+ CREATE TABLE IF NOT EXISTS veil_oauth_clients (
56
+ issuer TEXT PRIMARY KEY, locator TEXT NOT NULL) STRICT;
57
+ CREATE TABLE IF NOT EXISTS veil_connection_epoch (id INTEGER PRIMARY KEY CHECK (id = 1), revision INTEGER NOT NULL) STRICT;
58
+ INSERT OR IGNORE INTO veil_connection_epoch VALUES (1, 1);`);
59
+ const rows = (schema, statement, ...bindings) => sql
60
+ .exec(statement, ...bindings)
61
+ .toArray()
62
+ .map((row) => decode(schema, row));
63
+ const bump = () => sql.exec("UPDATE veil_connection_epoch SET revision = revision + 1");
64
+ const authorize = (actor, purpose) => {
65
+ decode(identifier, actor);
66
+ if (!options.authorize(actor, purpose))
67
+ throw new ConnectionsError("denied");
68
+ };
69
+ const connection = (id) => {
70
+ const row = rows(connectionRow, "SELECT * FROM veil_connections WHERE id = ?", id)[0];
71
+ if (!row)
72
+ throw new ConnectionsError("denied");
73
+ return row;
74
+ };
75
+ /** One live connection per binding name, so an Agent's module name is unambiguous. */
76
+ const assertNameFree = (name, exceptId) => {
77
+ const taken = rows(count, "SELECT COUNT(*) AS n FROM veil_connections WHERE name = ? AND state != 'revoked' AND id != ?", name, exceptId ?? "")[0];
78
+ if (taken === undefined || taken.n > 0)
79
+ throw new ConnectionsError("conflict");
80
+ };
81
+ const grant = (id, agentId) => rows(grantRow, "SELECT * FROM veil_connection_grants WHERE connectionId = ? AND agentId = ?", id, agentId)[0];
82
+ const address = (row) => ({
83
+ ownerId,
84
+ connectionId: row.id,
85
+ credential: row.credential,
86
+ });
87
+ /** The surface URL must be public https with nothing the owner did not choose to send. */
88
+ const publicUrl = (value) => {
89
+ if (!URL.canParse(value))
90
+ throw new ConnectionsError("invalid_input");
91
+ const url = new URL(value);
92
+ if (url.protocol !== "https:" ||
93
+ url.username ||
94
+ url.password ||
95
+ url.search ||
96
+ url.hash)
97
+ throw new ConnectionsError("invalid_input");
98
+ try {
99
+ assertPublicHostName(url.hostname);
100
+ policy.destination(url);
101
+ }
102
+ catch (cause) {
103
+ throw new ConnectionsError("denied", { cause });
104
+ }
105
+ return url.href;
106
+ };
107
+ const { cleanupPending, sweep, enqueueLocator, enqueueCleanup, markReconnect, retireAttempt, } = createCleanup({
108
+ ownerId,
109
+ custody: options.custody,
110
+ clock: options.clock,
111
+ sql,
112
+ transactionSync,
113
+ rows,
114
+ bump,
115
+ });
116
+ const fingerprint = (draft) => JSON.stringify([
117
+ draft.name,
118
+ draft.label ?? null,
119
+ draft.surface.kind,
120
+ "url" in draft.surface ? draft.surface.url : draft.surface.baseUrl,
121
+ draft.credential,
122
+ draft.secret !== undefined,
123
+ ]);
124
+ const begin = (actor, input) => Effect.gen(function* () {
125
+ const draft = yield* sync(() => {
126
+ authorize(actor, "manage");
127
+ const parsed = decode(draftSchema, input);
128
+ const wantsSecret = parsed.credential.kind === "static"
129
+ ? "required"
130
+ : parsed.credential.kind === "oauth"
131
+ ? "client"
132
+ : "never";
133
+ if ((wantsSecret === "required" && parsed.secret === undefined) ||
134
+ (wantsSecret === "never" && parsed.secret !== undefined) ||
135
+ (parsed.credential.kind === "oauth" &&
136
+ // An MCP server signs in at itself; a REST connection through a
137
+ // provider the operator registered, with the operator's client; or
138
+ // a custodian that runs the authorization (its integration id).
139
+ (parsed.credential.provider === undefined
140
+ ? parsed.surface.kind !== "mcp" ||
141
+ (parsed.secret !== undefined &&
142
+ parsed.credential.clientId === undefined)
143
+ : parsed.secret !== undefined ||
144
+ parsed.credential.clientId !== undefined ||
145
+ (options.oauth?.providers?.[parsed.credential.provider] ===
146
+ undefined
147
+ ? options.custody.connect === undefined
148
+ : parsed.surface.kind !== "openapi"))))
149
+ throw new ConnectionsError("invalid_input");
150
+ return parsed;
151
+ });
152
+ const replay = yield* sync(() => {
153
+ const receipt = rows(receiptRow, "SELECT fingerprint, result FROM veil_connection_receipts WHERE requestId = ?", draft.requestId)[0];
154
+ if (!receipt)
155
+ return null;
156
+ if (receipt.fingerprint !== fingerprint(draft))
157
+ throw new ConnectionsError("conflict");
158
+ return receipt.result;
159
+ });
160
+ if (replay !== null)
161
+ return {
162
+ connectionId: replay.connectionId,
163
+ authorizeUrl: replay.authorizeUrl ?? null,
164
+ };
165
+ yield* sync(() => {
166
+ assertNameFree(draft.name, null);
167
+ });
168
+ const endpoint = yield* sync(() => publicUrl(draft.surface.kind === "mcp"
169
+ ? draft.surface.url
170
+ : draft.surface.baseUrl));
171
+ const surface = draft.surface;
172
+ const decoded = surface.kind === "openapi"
173
+ ? yield* io(() => decodeTable(surface.table))
174
+ : null;
175
+ if (draft.credential.kind === "oauth") {
176
+ const table = decoded === null
177
+ ? null
178
+ : { table: decoded.operations, digest: decoded.digest };
179
+ const viaCustodian = draft.credential.provider !== undefined &&
180
+ options.oauth?.providers?.[draft.credential.provider] === undefined;
181
+ return yield* viaCustodian
182
+ ? oauthFlow.beginCustody(actor, draft, draft.credential, endpoint, fingerprint(draft), table)
183
+ : oauthFlow.begin(actor, draft, draft.credential, endpoint, fingerprint(draft), table);
184
+ }
185
+ const connectionId = options.ids();
186
+ const state = options.ids();
187
+ yield* sync(() => {
188
+ decode(identifier, connectionId);
189
+ sql.exec("INSERT INTO veil_connection_attempts (state, connectionId, actor, status, expiresAt) VALUES (?, ?, ?, 'pending', ?)", state, connectionId, actor, options.clock() + attemptTtlMs);
190
+ });
191
+ const addr = { ownerId, connectionId, credential: draft.credential };
192
+ const locator = draft.secret === undefined
193
+ ? null
194
+ : yield* options.custody.put(addr, draft.secret);
195
+ const stored = {
196
+ id: connectionId,
197
+ name: draft.name,
198
+ credential: draft.credential,
199
+ locator,
200
+ state: "active",
201
+ authorityVersion: 1,
202
+ version: 1,
203
+ surface: {
204
+ kind: draft.surface.kind,
205
+ url: endpoint,
206
+ table: decoded?.operations ?? null,
207
+ digest: decoded?.digest ?? null,
208
+ label: draft.label ?? draft.name,
209
+ },
210
+ };
211
+ return yield* sync(() => transactionSync(() => {
212
+ authorize(actor, "manage");
213
+ assertNameFree(stored.name, null);
214
+ const attempt = rows(count, "SELECT COUNT(*) AS n FROM veil_connection_attempts WHERE state = ? AND status = 'pending' AND expiresAt > ?", state, options.clock())[0];
215
+ if (attempt?.n !== 1)
216
+ throw new ConnectionsError("denied");
217
+ sql.exec("INSERT INTO veil_connections VALUES (?, ?, ?, ?, ?, 'active', 1, 1)", stored.id, stored.name, JSON.stringify(stored.surface), JSON.stringify(stored.credential), stored.locator);
218
+ sql.exec("UPDATE veil_connection_attempts SET status = 'completed' WHERE state = ?", state);
219
+ sql.exec("INSERT INTO veil_connection_receipts VALUES (?, ?, ?)", draft.requestId, fingerprint(draft), JSON.stringify({ connectionId }));
220
+ bump();
221
+ return { connectionId, authorizeUrl: null };
222
+ })).pipe(Effect.tapError(() => sync(() => {
223
+ enqueueCleanup(stored);
224
+ })));
225
+ });
226
+ const oauthStore = {
227
+ connection,
228
+ recordPending: (input) => {
229
+ transactionSync(() => {
230
+ authorize(input.actor, "manage");
231
+ const stored = input.connection;
232
+ assertNameFree(stored.name, null);
233
+ sql.exec("INSERT INTO veil_connections VALUES (?, ?, ?, ?, NULL, 'pending', 1, 1)", stored.id, stored.name, JSON.stringify(stored.surface), JSON.stringify(stored.credential));
234
+ sql.exec("INSERT INTO veil_connection_attempts VALUES (?, ?, ?, 'pending', ?, ?, ?)", input.state, stored.id, input.actor, options.clock() + attemptTtlMs, input.attemptLocator, input.issuer);
235
+ sql.exec("INSERT INTO veil_connection_receipts VALUES (?, ?, ?)", input.requestId, input.fingerprint, JSON.stringify({
236
+ connectionId: stored.id,
237
+ authorizeUrl: input.authorizeUrl,
238
+ }));
239
+ bump();
240
+ });
241
+ },
242
+ claimCustody: (state) => transactionSync(() => {
243
+ if (!state.startsWith(`${ownerId}.`))
244
+ throw new ConnectionsError("denied");
245
+ const attempt = rows(attemptRow, "SELECT * FROM veil_connection_attempts WHERE state = ?", state)[0];
246
+ // A browser attempt holds a sealed verifier; only a custodian's has none.
247
+ if (attempt?.status !== "pending" ||
248
+ attempt.pkceVerifierSealed !== null)
249
+ throw new ConnectionsError("denied");
250
+ if (attempt.expiresAt <= options.clock()) {
251
+ retireAttempt(attempt, "expired");
252
+ return { attempt, expired: true };
253
+ }
254
+ sql.exec("UPDATE veil_connection_attempts SET status = 'consumed' WHERE state = ?", state);
255
+ return { attempt, expired: false };
256
+ }),
257
+ claim: (actor, input) => transactionSync(() => {
258
+ authorize(actor, "manage");
259
+ const wanted = decode(completeInputSchema, input);
260
+ if (!wanted.state.startsWith(`${ownerId}.`))
261
+ throw new ConnectionsError("denied");
262
+ const attempt = rows(attemptRow, "SELECT * FROM veil_connection_attempts WHERE state = ?", wanted.state)[0];
263
+ if (attempt?.status !== "pending" ||
264
+ attempt.actor !== actor ||
265
+ attempt.pkceVerifierSealed === null)
266
+ throw new ConnectionsError("denied");
267
+ if (attempt.expiresAt <= options.clock()) {
268
+ retireAttempt(attempt, "expired");
269
+ return { attempt, wanted, expired: true };
270
+ }
271
+ sql.exec("UPDATE veil_connection_attempts SET status = 'consumed' WHERE state = ?", attempt.state);
272
+ return { attempt, wanted, expired: false };
273
+ }),
274
+ fail: (attempt) => {
275
+ transactionSync(() => {
276
+ retireAttempt(attempt, "failed");
277
+ });
278
+ },
279
+ activate: (row, attempt, locator) => transactionSync(() => {
280
+ const current = connection(row.id);
281
+ if (current.state !== "pending")
282
+ return false;
283
+ sql.exec("UPDATE veil_connections SET state = 'active', locator = ?, authorityVersion = authorityVersion + 1 WHERE id = ?", locator, row.id);
284
+ sql.exec("UPDATE veil_connection_attempts SET status = 'completed', pkceVerifierSealed = NULL WHERE state = ?", attempt.state);
285
+ if (attempt.pkceVerifierSealed !== null)
286
+ enqueueLocator(row.id, attempt.pkceVerifierSealed, row.credential);
287
+ bump();
288
+ return true;
289
+ }),
290
+ discard: (connectionId, locator, credential) => {
291
+ transactionSync(() => {
292
+ enqueueLocator(connectionId, locator, credential);
293
+ });
294
+ },
295
+ swap: (row, held, locator) => transactionSync(() => {
296
+ const current = connection(row.id);
297
+ if (current.state !== "active" || current.locator !== held)
298
+ return current.state;
299
+ sql.exec("UPDATE veil_connections SET locator = ? WHERE id = ?", locator, row.id);
300
+ enqueueLocator(row.id, held, row.credential);
301
+ return "applied";
302
+ }),
303
+ clientLocator: (issuer) => rows(registryRow, "SELECT locator FROM veil_oauth_clients WHERE issuer = ?", issuer)[0]?.locator,
304
+ saveClientLocator: (issuer, locator) => {
305
+ transactionSync(() => {
306
+ const old = rows(registryRow, "SELECT locator FROM veil_oauth_clients WHERE issuer = ?", issuer)[0];
307
+ sql.exec("INSERT OR REPLACE INTO veil_oauth_clients VALUES (?, ?)", issuer, locator);
308
+ if (old !== undefined)
309
+ enqueueLocator("oauth-clients", old.locator, { kind: "oauth" });
310
+ });
311
+ },
312
+ };
313
+ const oauthFlow = createOAuthFlow({
314
+ ownerId,
315
+ custody: options.custody,
316
+ oauth: options.oauth,
317
+ clock: options.clock,
318
+ ids: options.ids,
319
+ policy,
320
+ store: oauthStore,
321
+ sweep,
322
+ });
323
+ const grantTo = (actor, input) => sync(() => transactionSync(() => {
324
+ authorize(actor, "manage");
325
+ const wanted = decode(grantInputSchema, input);
326
+ const row = connection(wanted.connectionId);
327
+ if (row.state === "revoked")
328
+ throw new ConnectionsError("revoked");
329
+ if (wanted.tools !== "all" &&
330
+ (Array.isArray(wanted.tools)
331
+ ? row.surface.kind !== "mcp" && wanted.tools.length === 0
332
+ : row.surface.kind !== "openapi"))
333
+ throw new ConnectionsError("invalid_input");
334
+ const tools = JSON.stringify(wanted.tools);
335
+ const existing = grant(wanted.connectionId, wanted.agentId);
336
+ if (existing?.active === 1 && JSON.stringify(existing.tools) === tools)
337
+ return { grantVersion: existing.version };
338
+ const next = (existing?.version ?? 0) + 1;
339
+ sql.exec(`INSERT INTO veil_connection_grants VALUES (?, ?, ?, ?, ?, 1)
340
+ ON CONFLICT (connectionId, agentId) DO UPDATE SET tools = excluded.tools, grantor = excluded.grantor, version = excluded.version, active = 1`, wanted.connectionId, wanted.agentId, tools, actor, next);
341
+ bump();
342
+ return { grantVersion: next };
343
+ }));
344
+ const ungrant = (actor, input) => sync(() => {
345
+ transactionSync(() => {
346
+ authorize(actor, "manage");
347
+ const wanted = decode(Schema.Struct({ connectionId: identifier, agentId: identifier }), input);
348
+ const existing = grant(wanted.connectionId, wanted.agentId);
349
+ if (existing?.active !== 1)
350
+ return;
351
+ sql.exec("UPDATE veil_connection_grants SET active = 0, version = version + 1 WHERE connectionId = ? AND agentId = ?", wanted.connectionId, wanted.agentId);
352
+ bump();
353
+ });
354
+ });
355
+ const revoke = (actor, input) => Effect.gen(function* () {
356
+ yield* sync(() => {
357
+ transactionSync(() => {
358
+ authorize(actor, "manage");
359
+ const row = connection(decode(identifier, input.connectionId));
360
+ if (row.state === "revoked")
361
+ return;
362
+ for (const attempt of rows(attemptRow, "SELECT * FROM veil_connection_attempts WHERE connectionId = ? AND status IN ('pending', 'consumed')", row.id))
363
+ retireAttempt(attempt, "revoked");
364
+ sql.exec("UPDATE veil_connections SET state = 'revoked', locator = NULL, authorityVersion = authorityVersion + 1 WHERE id = ?", row.id);
365
+ sql.exec("UPDATE veil_connection_grants SET active = 0, version = version + 1 WHERE connectionId = ? AND active = 1", row.id);
366
+ enqueueCleanup(row);
367
+ bump();
368
+ });
369
+ });
370
+ yield* sweep();
371
+ });
372
+ const projection = (agentId) => Effect.gen(function* () {
373
+ const stored = yield* sync(() => {
374
+ decode(identifier, agentId);
375
+ const revision = rows(Schema.Struct({ revision: Schema.Number }), "SELECT revision FROM veil_connection_epoch")[0]?.revision ?? 1;
376
+ const granted = rows(grantRow, "SELECT * FROM veil_connection_grants WHERE agentId = ? AND active = 1", agentId);
377
+ return {
378
+ revision,
379
+ granted: granted
380
+ .map((g) => ({ g, row: connection(g.connectionId) }))
381
+ .filter(({ row }) => row.state === "active"),
382
+ };
383
+ });
384
+ const connections = [];
385
+ for (const { g, row } of stored.granted) {
386
+ let surface = { kind: "mcp", url: row.surface.url };
387
+ if (row.surface.kind === "openapi") {
388
+ const decoded = yield* io(() => decodeTable(row.surface.table));
389
+ surface = {
390
+ kind: "openapi",
391
+ baseUrl: row.surface.url,
392
+ table: decoded.operations,
393
+ digest: decoded.digest,
394
+ };
395
+ }
396
+ connections.push({
397
+ connectionId: row.id,
398
+ name: row.name,
399
+ surface,
400
+ tools: g.tools,
401
+ version: row.version,
402
+ authorityVersion: row.authorityVersion,
403
+ grantVersion: g.version,
404
+ });
405
+ }
406
+ return {
407
+ revision: stored.revision,
408
+ connections,
409
+ };
410
+ });
411
+ const list = (actor) => sync(() => {
412
+ authorize(actor, "manage");
413
+ return rows(connectionRow, "SELECT * FROM veil_connections ORDER BY id").map((row) => ({
414
+ connectionId: row.id,
415
+ name: row.name,
416
+ label: row.surface.label ?? row.name,
417
+ surface: {
418
+ kind: row.surface.kind,
419
+ origin: new URL(row.surface.url).origin,
420
+ url: row.surface.url,
421
+ digest: row.surface.digest ?? null,
422
+ },
423
+ credentialKind: row.credential.kind,
424
+ credential: row.credential,
425
+ state: row.state,
426
+ version: row.version,
427
+ authorityVersion: row.authorityVersion,
428
+ grants: rows(grantRow, "SELECT * FROM veil_connection_grants WHERE connectionId = ? AND active = 1", row.id).map((g) => ({
429
+ agentId: g.agentId,
430
+ tools: g.tools,
431
+ version: g.version,
432
+ grantor: g.grantor,
433
+ })),
434
+ }));
435
+ });
436
+ /** The grant, the connection and the grantor must all still stand. */
437
+ const admitted = (agentId, ref) => {
438
+ decode(identifier, agentId);
439
+ const row = connection(ref.connectionId);
440
+ const held = grant(ref.connectionId, agentId);
441
+ if (row.state === "revoked" ||
442
+ held?.active !== 1 ||
443
+ held.version !== ref.grantVersion)
444
+ throw new ConnectionsError("revoked");
445
+ if (row.state === "pending")
446
+ throw new ConnectionsError("pending");
447
+ if (row.state === "reconnect_required")
448
+ throw new ConnectionsError("reconnect_required");
449
+ authorize(held.grantor, "grantor");
450
+ return { row, tools: held.tools, grantor: held.grantor, agentId };
451
+ };
452
+ /**
453
+ * One request through every gate: the grant, the host's last live check, the
454
+ * credential at the owner, and the grant again on the way back.
455
+ */
456
+ const deliver = (admit, prepare) => Effect.gen(function* () {
457
+ const { row, tools, grantor, agentId } = yield* sync(admit);
458
+ const { request, operation } = yield* prepare(row, tools);
459
+ const verdict = options.check;
460
+ if (verdict !== undefined &&
461
+ !(yield* io(() => verdict(grantor, { agentId, connectionId: row.id, tools }, operation))))
462
+ return yield* Effect.fail(new ConnectionsError("denied"));
463
+ yield* sync(admit);
464
+ if (row.locator === null && row.credential.kind === "static")
465
+ return yield* Effect.fail(new ConnectionsError("denied"));
466
+ const response = yield* // A custodian that runs the sign-in refreshes the token itself.
467
+ (row.credential.kind === "oauth" && options.custody.open !== undefined
468
+ ? oauthFlow.send(row, request)
469
+ : options.custody.inject(address(row), row.locator ?? "", request)).pipe(Effect.tapError((error) => error.code === "reconnect_required"
470
+ ? sync(() => {
471
+ markReconnect(row.id);
472
+ })
473
+ : Effect.void));
474
+ const stillCurrent = yield* sync(admit).pipe(Effect.result);
475
+ if (stillCurrent._tag === "Failure" ||
476
+ response.redirected ||
477
+ (response.status >= 300 && response.status < 400)) {
478
+ yield* io(() => response.body?.cancel() ?? Promise.resolve());
479
+ return yield* Effect.fail(stillCurrent._tag === "Failure"
480
+ ? stillCurrent.failure
481
+ : new ConnectionsError("invalid_response"));
482
+ }
483
+ return response;
484
+ });
485
+ const fetchThrough = (admit, target, init) => Effect.runPromise(deliver(admit, (row, tools) => row.surface.kind === "openapi" && tools !== "all"
486
+ ? // Effect and name grants are enforced per operation, by `call`.
487
+ Effect.fail(new ConnectionsError("denied"))
488
+ : io(() => admitRequest({ kind: row.surface.kind, url: row.surface.url }, tools, new Request(target, init), policy)).pipe(Effect.map((request) => ({ request, operation: null })))));
489
+ const egress = (agentId, input) => {
490
+ const ref = decode(egressRefSchema, input);
491
+ return (target, init) => fetchThrough(() => admitted(agentId, ref), target, init);
492
+ };
493
+ /** A typed OpenAPI operation: built from the stored table, checked against its stored schemas. */
494
+ const callThrough = (admit, raw) => Effect.gen(function* () {
495
+ const wanted = yield* sync(() => decode(callInputSchema, raw));
496
+ let chosen;
497
+ const response = yield* deliver(admit, (row, tools) => Effect.gen(function* () {
498
+ if (row.surface.kind !== "openapi")
499
+ return yield* Effect.fail(new ConnectionsError("unsupported"));
500
+ const table = yield* io(() => decodeTable(row.surface.table));
501
+ chosen = table.operations.find((candidate) => candidate.name === wanted.operation);
502
+ if (chosen === undefined || !admits(tools, chosen))
503
+ return yield* Effect.fail(new ConnectionsError("denied"));
504
+ const picked = chosen;
505
+ const built = yield* io(() => operationRequest(row.surface.url, picked, wanted.input));
506
+ const request = yield* io(() => admitRequest({ kind: "openapi", url: row.surface.url }, "all", built, policy));
507
+ return {
508
+ request,
509
+ operation: { name: picked.name, effect: picked.effect },
510
+ };
511
+ }));
512
+ const operation = chosen;
513
+ if (operation === undefined)
514
+ return yield* Effect.fail(new ConnectionsError("denied"));
515
+ const text = yield* io(() => boundedText(response));
516
+ // A grant ended while the body streamed: nothing read may be returned.
517
+ yield* sync(admit);
518
+ if (!response.ok)
519
+ return yield* Effect.fail(new ConnectionsError("invalid_response"));
520
+ return yield* io(() => operationOutput(operation, text));
521
+ });
522
+ const call = (agentId, input, raw) => {
523
+ const ref = decode(egressRefSchema, input);
524
+ return callThrough(() => admitted(agentId, ref), raw);
525
+ };
526
+ /** The owner as sender: no Agent grant, and a version they last saw, when they name one. */
527
+ const ownerAdmit = (actor, connectionId, expectedVersion) => {
528
+ authorize(actor, "manage");
529
+ const row = connection(connectionId);
530
+ if (row.state === "revoked")
531
+ throw new ConnectionsError("revoked");
532
+ if (expectedVersion !== null && row.version !== expectedVersion)
533
+ throw new ConnectionsError("conflict");
534
+ if (row.state !== "active")
535
+ throw new ConnectionsError("pending");
536
+ return {
537
+ row,
538
+ tools: "all",
539
+ grantor: actor,
540
+ agentId: `owner:${actor}`,
541
+ };
542
+ };
543
+ const ownerCall = (actor, at, raw) => callThrough(() => ownerAdmit(actor, at.connectionId, at.expectedVersion), raw);
544
+ /** The owner's own raw fetch to one connection, through the same gates. */
545
+ const ownerFetch = (actor, connectionId) => (target, init) => fetchThrough(() => ownerAdmit(actor, connectionId, null), target, init);
546
+ /** The tools an MCP connection offers, listed by its owner. */
547
+ const tools = (actor, connectionId) => Effect.gen(function* () {
548
+ const row = yield* sync(() => {
549
+ authorize(actor, "manage");
550
+ return connection(connectionId);
551
+ });
552
+ if (row.surface.kind !== "mcp")
553
+ return yield* Effect.fail(new ConnectionsError("unsupported"));
554
+ return yield* io(() => listMcpTools(row.surface.url, ownerFetch(actor, connectionId)));
555
+ });
556
+ /** The decoded operations of one OpenAPI connection, for its owner. */
557
+ const operations = (actor, id) => Effect.gen(function* () {
558
+ const row = yield* sync(() => {
559
+ authorize(actor, "manage");
560
+ return connection(id);
561
+ });
562
+ if (row.surface.kind !== "openapi")
563
+ return yield* Effect.fail(new ConnectionsError("unsupported"));
564
+ return yield* io(() => decodeTable(row.surface.table));
565
+ });
566
+ const update = createUpdate({
567
+ ownerId,
568
+ custody: options.custody,
569
+ store: {
570
+ authorize: (actor) => {
571
+ authorize(actor, "manage");
572
+ },
573
+ connection,
574
+ receipt: (requestId) => {
575
+ const found = rows(receiptRow, "SELECT fingerprint, result FROM veil_connection_receipts WHERE requestId = ?", requestId)[0];
576
+ return found === undefined
577
+ ? null
578
+ : {
579
+ stamp: found.fingerprint,
580
+ connectionId: found.result.connectionId,
581
+ version: found.result.version ?? 0,
582
+ };
583
+ },
584
+ checkName: assertNameFree,
585
+ publicUrl,
586
+ commit: ({ actor, update: input, stamp, row, ...next }) => transactionSync(() => {
587
+ authorize(actor, "manage");
588
+ const current = connection(row.id);
589
+ if (current.version !== input.expectedVersion ||
590
+ current.state !== row.state)
591
+ throw new ConnectionsError("conflict");
592
+ if (input.name !== undefined)
593
+ assertNameFree(input.name, row.id);
594
+ const version = current.version + 1;
595
+ sql.exec("UPDATE veil_connections SET name = ?, surface = ?, credential = ?, locator = ?, version = ?, authorityVersion = authorityVersion + 1 WHERE id = ?", input.name ?? current.name, JSON.stringify({
596
+ ...current.surface,
597
+ url: next.baseUrl,
598
+ table: next.table,
599
+ digest: next.digest,
600
+ label: input.label ?? current.surface.label ?? current.name,
601
+ }), JSON.stringify(next.credential), next.locator, version, row.id);
602
+ if (input.secret !== undefined && row.locator !== null)
603
+ enqueueLocator(row.id, row.locator, row.credential);
604
+ sql.exec("INSERT INTO veil_connection_receipts VALUES (?, ?, ?)", input.requestId, stamp, JSON.stringify({ connectionId: row.id, version }));
605
+ bump();
606
+ return { connectionId: row.id, version };
607
+ }),
608
+ discard: (connectionId, locator, credential) => {
609
+ enqueueLocator(connectionId, locator, credential);
610
+ },
611
+ },
612
+ });
613
+ return {
614
+ begin,
615
+ complete: oauthFlow.complete,
616
+ completeCustody: oauthFlow.completeCustody,
617
+ grant: grantTo,
618
+ ungrant,
619
+ revoke,
620
+ projection,
621
+ list,
622
+ egress,
623
+ call,
624
+ ownerCall,
625
+ tools,
626
+ operations,
627
+ update,
628
+ sweep,
629
+ cleanupPending,
630
+ };
631
+ }
@@ -0,0 +1,3 @@
1
+ import type { NarrowFetch } from "./contract.js";
2
+ /** The tool names an MCP server offers, listed with the maintained SDK client. */
3
+ export declare function listMcpTools(url: string, fetch: NarrowFetch): Promise<readonly string[]>;
@@ -0,0 +1,23 @@
1
+ const toolLimit = 1_024;
2
+ /** The tool names an MCP server offers, listed with the maintained SDK client. */
3
+ export async function listMcpTools(url, fetch) {
4
+ const sdk = await import("@modelcontextprotocol/client");
5
+ const client = new sdk.Client({ name: "fungi-connections", version: "1" });
6
+ await client.connect(new sdk.StreamableHTTPClientTransport(new URL(url), {
7
+ fetch,
8
+ requestInit: { redirect: "error" },
9
+ }));
10
+ try {
11
+ const names = [];
12
+ let cursor;
13
+ do {
14
+ const page = await client.listTools(cursor === undefined ? undefined : { cursor });
15
+ names.push(...page.tools.map((tool) => tool.name));
16
+ cursor = page.nextCursor;
17
+ } while (cursor !== undefined && names.length < toolLimit);
18
+ return names.slice(0, toolLimit);
19
+ }
20
+ finally {
21
+ await client.close();
22
+ }
23
+ }
@@ -0,0 +1,8 @@
1
+ import type { createPublicEgressPolicy } from "../public-egress-policy.js";
2
+ import { type Tools } from "./contract.js";
3
+ export type Endpoint = Readonly<{
4
+ kind: "mcp" | "openapi";
5
+ url: string;
6
+ }>;
7
+ /** One request, one verdict. The caller holds the grant; this proves the request stays inside it. */
8
+ export declare function admitRequest(endpoint: Endpoint, tools: Tools, request: Request, policy: ReturnType<typeof createPublicEgressPolicy>): Promise<Request>;