@fungi.computer/veil 0.4.0 → 0.6.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/README.md +48 -54
- package/dist/best-effort-cleanup.d.ts +1 -1
- package/dist/connections/cleanup.d.ts +21 -0
- package/dist/connections/cleanup.js +66 -0
- package/dist/connections/contract.d.ts +192 -7
- package/dist/connections/contract.js +44 -2
- package/dist/connections/core.d.ts +58 -5
- package/dist/connections/core.js +438 -111
- 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 +179 -0
- package/dist/connections/oauth-flow.d.ts +83 -0
- package/dist/connections/oauth-flow.js +330 -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 +43 -85
- package/dist/gatekeeper.js +138 -95
- package/dist/github-app.d.ts +44 -0
- package/dist/github-app.js +93 -0
- package/dist/index.d.ts +0 -241
- package/dist/index.js +0 -256
- package/dist/nango.d.ts +40 -15
- package/dist/nango.js +7 -242
- package/package.json +15 -9
- package/dist/nango-git.d.ts +0 -19
- package/dist/nango-git.js +0 -125
package/README.md
CHANGED
|
@@ -1,69 +1,63 @@
|
|
|
1
1
|
# Veil (`@fungi.computer/veil`)
|
|
2
2
|
|
|
3
|
-
This package keeps provider credentials behind trusted
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
private locator.
|
|
3
|
+
This package keeps provider credentials behind trusted owner doors. It owns the
|
|
4
|
+
shared connections core (MCP, OpenAPI and a repository's git endpoint),
|
|
5
|
+
model-key grants and the Agent's anonymous public-web fetch. A guest receives an
|
|
6
|
+
authorized operation, not the provider token or its private locator.
|
|
7
7
|
|
|
8
8
|
## Set up and check
|
|
9
9
|
|
|
10
|
-
The host supplies
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
connection.
|
|
10
|
+
The host supplies the owner's SQLite, a `CustodyAdapter` and its live
|
|
11
|
+
authorization to `createConnections` (`@fungi.computer/veil/connections`).
|
|
12
|
+
Custody is `sealed` by default; Nango is the optional adapter
|
|
13
|
+
(`@fungi.computer/veil/connections/nango`). Model input cannot choose a
|
|
14
|
+
connection, repository or private locator. Without custody configuration, report
|
|
15
|
+
unavailable rather than manufacturing a connection.
|
|
17
16
|
|
|
18
17
|
Run `pnpm --filter @fungi.computer/veil test` for the package's synthetic
|
|
19
|
-
custody, signed-webhook and public-fetch checks.
|
|
20
|
-
lifecycle, start with `test/nango-git.test.ts`; a unit test does not prove a
|
|
18
|
+
custody, signed-webhook and public-fetch checks. A unit test does not prove a
|
|
21
19
|
live Nango service or provider account.
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
GitHub REST is an ordinary `openapi` connection with an `oauth` credential:
|
|
22
|
+
through an operator-registered provider on either custody adapter, or through a
|
|
23
|
+
Nango integration. A custodian-run integration is pinned by the host
|
|
24
|
+
(`configuredCustodyIntegrations`) to one surface kind and one origin, and the
|
|
25
|
+
core refuses it at `begin` and again before every send, so the credential Nango
|
|
26
|
+
holds never goes anywhere else. GitHub REST is pinned to `api.github.com`.
|
|
27
|
+
|
|
28
|
+
A Computer's git is not a connection in this core.
|
|
29
|
+
`@fungi.computer/veil/github-app` wraps Nango's `github-app` integration
|
|
30
|
+
(`NANGO_GITHUB_APP_INTEGRATION_ID`, never the same as
|
|
31
|
+
`NANGO_GITHUB_INTEGRATION_ID`): it opens the installation consent, verifies the
|
|
32
|
+
signed completion, finds a lost one, deletes a connection and reads the current
|
|
33
|
+
installation token. The Hub places that token on a request to `github.com` and
|
|
34
|
+
nowhere else.
|
|
27
35
|
|
|
28
36
|
## Nango adapter
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
completion
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
accepted. It never selects the first candidate from an arbitrary page.
|
|
54
|
-
|
|
55
|
-
The pinned public deletion endpoint returns `200 {success:true}` after deletion
|
|
56
|
-
and `400 {error:{code:"unknown_connection"}}` when the selected connection is
|
|
57
|
-
absent. Other responses, including generic 404 and `success:false`, do not
|
|
58
|
-
settle cleanup. This mapping is specific to the selected server implementation,
|
|
59
|
-
not a generic Nango error policy. Provider/proxy failures stay unavailable,
|
|
60
|
-
except a proxied provider `401` carrying GitHub's own error body, which marks
|
|
61
|
-
the Connection reconnect-required; Nango's own `401` envelope stays unavailable.
|
|
62
|
-
|
|
63
|
-
The one proxy operation is `GET /proxy/repos/{owner}/{name}` with private Nango
|
|
64
|
-
connection/integration headers. Veil's existing caller decodes credential-free
|
|
65
|
-
metadata and verifies the repository ID. Cancellation stops waiting through the
|
|
66
|
-
maintained transport; it does not prove upstream work stopped or rolled back.
|
|
38
|
+
`createNangoCustody` takes a Nango server origin, API key, a separate webhook
|
|
39
|
+
signing key, one API-key integration for pasted credentials, and the public
|
|
40
|
+
Connect UI base and public API URL. These are host configuration, never browser
|
|
41
|
+
or model input. `connect` returns the Connect document browsers open: Nango's
|
|
42
|
+
issued link, confined to the configured Connect UI and pinned to the public API
|
|
43
|
+
with `apiURL`. `verifyCompletion` verifies the raw callback body with
|
|
44
|
+
`@nangohq/node` before decoding it, and the core settles only an attempt the
|
|
45
|
+
owner began. Unconfigured hosts must report unavailable instead of substituting
|
|
46
|
+
fake custody.
|
|
47
|
+
|
|
48
|
+
`inject` sends through Nango's proxy, which attaches and refreshes the
|
|
49
|
+
credential; the pinned deletion endpoint returns `200 {success:true}` after
|
|
50
|
+
deletion and `400 {error:{code:"unknown_connection"}}` when the connection is
|
|
51
|
+
absent, and only those settle cleanup. A proxy `404` is the custodian no longer
|
|
52
|
+
holding the connection and marks it reconnect-required. `find` asks Nango for
|
|
53
|
+
the one connection that carries an attempt's state, actor and Team, so a sign-in
|
|
54
|
+
whose signed completion was lost still settles. Cancellation stops the caller
|
|
55
|
+
waiting; it does not prove upstream work stopped or rolled back.
|
|
56
|
+
|
|
57
|
+
`configuredComputerGitIntegration(env)` names the integration through which a
|
|
58
|
+
Computer's git signs in (`NANGO_GITHUB_APP_INTEGRATION_ID`);
|
|
59
|
+
`NANGO_GITHUB_INTEGRATION_ID` joins the integrations an owner may sign in
|
|
60
|
+
through for REST connections.
|
|
67
61
|
|
|
68
62
|
## Public-web door
|
|
69
63
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Report cleanup failures without exposing provider or transport details. */
|
|
2
|
-
export declare function bestEffortCleanup(operation: "
|
|
2
|
+
export declare function bestEffortCleanup(operation: "public-fetch-socket", cleanup: () => Promise<void>): Promise<void>;
|
|
@@ -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,13 +1,17 @@
|
|
|
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" | "
|
|
4
|
+
export type ConnectionsRefusal = "invalid_input" | "denied" | "revoked" | "conflict" | "unsupported" | "too_large" | "reconnect_required" | "pending" | "invalid_response"
|
|
5
|
+
/** The provider's rate limit refused the request; retry later. */
|
|
6
|
+
| "rate_limited" | "unavailable";
|
|
5
7
|
/** Fixed codes only: upstream bodies, URLs and credential bytes never enter errors. */
|
|
6
8
|
export declare class ConnectionsError extends Error {
|
|
7
9
|
readonly code: ConnectionsRefusal;
|
|
8
10
|
readonly name = "ConnectionsError";
|
|
9
11
|
constructor(code: ConnectionsRefusal, options?: ErrorOptions);
|
|
10
12
|
}
|
|
13
|
+
/** How long a sign-in attempt stays open before it expires. */
|
|
14
|
+
export declare const attemptTtlMs: number;
|
|
11
15
|
export declare const identifier: Schema.String;
|
|
12
16
|
declare const placementSchema: Schema.Union<readonly [Schema.Literal<"bearer">, Schema.Struct<{
|
|
13
17
|
readonly header: Schema.String;
|
|
@@ -15,7 +19,11 @@ declare const placementSchema: Schema.Union<readonly [Schema.Literal<"bearer">,
|
|
|
15
19
|
readonly query: Schema.String;
|
|
16
20
|
}>]>;
|
|
17
21
|
export type Placement = typeof placementSchema.Type;
|
|
18
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* How the final request is authorized. For `oauth` the owner may bring its own
|
|
24
|
+
* client (`clientId`, with the client secret as the draft's `secret`); without
|
|
25
|
+
* one the server's dynamic registration is used.
|
|
26
|
+
*/
|
|
19
27
|
export declare const credentialSchema: Schema.Union<readonly [Schema.Struct<{
|
|
20
28
|
readonly kind: Schema.Literal<"none">;
|
|
21
29
|
}>, Schema.Struct<{
|
|
@@ -25,6 +33,12 @@ export declare const credentialSchema: Schema.Union<readonly [Schema.Struct<{
|
|
|
25
33
|
}>, Schema.Struct<{
|
|
26
34
|
readonly query: Schema.String;
|
|
27
35
|
}>]>;
|
|
36
|
+
}>, Schema.Struct<{
|
|
37
|
+
readonly kind: Schema.Literal<"oauth">;
|
|
38
|
+
readonly scope: Schema.optional<Schema.String>;
|
|
39
|
+
readonly clientId: Schema.optional<Schema.String>;
|
|
40
|
+
/** An operator-registered REST provider; absent for an MCP server's own sign-in. */
|
|
41
|
+
readonly provider: Schema.optional<Schema.String>;
|
|
28
42
|
}>]>;
|
|
29
43
|
export type Credential = typeof credentialSchema.Type;
|
|
30
44
|
export type SurfaceView = Readonly<{
|
|
@@ -33,21 +47,27 @@ export type SurfaceView = Readonly<{
|
|
|
33
47
|
}> | Readonly<{
|
|
34
48
|
kind: "openapi";
|
|
35
49
|
baseUrl: string;
|
|
36
|
-
table: unknown;
|
|
50
|
+
table: readonly unknown[];
|
|
51
|
+
/** Digest of the decoded operation table. */
|
|
52
|
+
digest: string;
|
|
37
53
|
}>;
|
|
38
|
-
export declare const toolsSchema: Schema.Union<readonly [Schema.Literal<"all">, Schema
|
|
54
|
+
export declare const toolsSchema: Schema.Union<readonly [Schema.Literal<"all">, Schema.Struct<{
|
|
55
|
+
readonly effect: Schema.Literal<"read">;
|
|
56
|
+
}>, Schema.$Array<Schema.String>]>;
|
|
39
57
|
export type Tools = typeof toolsSchema.Type;
|
|
40
58
|
export declare const draftSchema: Schema.Struct<{
|
|
41
59
|
/** Idempotency key: a replay with the same request returns the same connection. */
|
|
42
60
|
readonly requestId: Schema.String;
|
|
43
61
|
readonly name: Schema.String;
|
|
62
|
+
/** Display text for owners; defaults to the name. */
|
|
63
|
+
readonly label: Schema.optional<Schema.String>;
|
|
44
64
|
readonly surface: Schema.Union<readonly [Schema.Struct<{
|
|
45
65
|
readonly kind: Schema.Literal<"mcp">;
|
|
46
66
|
readonly url: Schema.String;
|
|
47
67
|
}>, Schema.Struct<{
|
|
48
68
|
readonly kind: Schema.Literal<"openapi">;
|
|
49
69
|
readonly baseUrl: Schema.String;
|
|
50
|
-
readonly table: Schema.Unknown
|
|
70
|
+
readonly table: Schema.$Array<Schema.Unknown>;
|
|
51
71
|
}>]>;
|
|
52
72
|
readonly credential: Schema.Union<readonly [Schema.Struct<{
|
|
53
73
|
readonly kind: Schema.Literal<"none">;
|
|
@@ -58,6 +78,12 @@ export declare const draftSchema: Schema.Struct<{
|
|
|
58
78
|
}>, Schema.Struct<{
|
|
59
79
|
readonly query: Schema.String;
|
|
60
80
|
}>]>;
|
|
81
|
+
}>, Schema.Struct<{
|
|
82
|
+
readonly kind: Schema.Literal<"oauth">;
|
|
83
|
+
readonly scope: Schema.optional<Schema.String>;
|
|
84
|
+
readonly clientId: Schema.optional<Schema.String>;
|
|
85
|
+
/** An operator-registered REST provider; absent for an MCP server's own sign-in. */
|
|
86
|
+
readonly provider: Schema.optional<Schema.String>;
|
|
61
87
|
}>]>;
|
|
62
88
|
readonly secret: Schema.optional<Schema.String>;
|
|
63
89
|
}>;
|
|
@@ -65,9 +91,54 @@ export type ConnectionDraft = typeof draftSchema.Type;
|
|
|
65
91
|
export declare const grantInputSchema: Schema.Struct<{
|
|
66
92
|
readonly connectionId: Schema.String;
|
|
67
93
|
readonly agentId: Schema.String;
|
|
68
|
-
readonly tools: Schema.Union<readonly [Schema.Literal<"all">, Schema
|
|
94
|
+
readonly tools: Schema.Union<readonly [Schema.Literal<"all">, Schema.Struct<{
|
|
95
|
+
readonly effect: Schema.Literal<"read">;
|
|
96
|
+
}>, Schema.$Array<Schema.String>]>;
|
|
69
97
|
}>;
|
|
70
98
|
export type GrantInput = typeof grantInputSchema.Type;
|
|
99
|
+
/** The authorization server's redirect back to the owner. */
|
|
100
|
+
export declare const completeInputSchema: Schema.Struct<{
|
|
101
|
+
readonly state: Schema.String;
|
|
102
|
+
readonly code: Schema.String;
|
|
103
|
+
readonly iss: Schema.optional<Schema.String>;
|
|
104
|
+
}>;
|
|
105
|
+
export type CompleteInput = typeof completeInputSchema.Type;
|
|
106
|
+
/** Owner edits to a live connection, guarded by the version the owner last saw. */
|
|
107
|
+
export declare const updateInputSchema: Schema.Struct<{
|
|
108
|
+
readonly requestId: Schema.String;
|
|
109
|
+
readonly connectionId: Schema.String;
|
|
110
|
+
readonly expectedVersion: Schema.Number;
|
|
111
|
+
readonly label: Schema.optional<Schema.String>;
|
|
112
|
+
readonly name: Schema.optional<Schema.String>;
|
|
113
|
+
/** Replacement OpenAPI operation table, decoded by Forage. */
|
|
114
|
+
readonly table: Schema.optional<Schema.$Array<Schema.Unknown>>;
|
|
115
|
+
readonly baseUrl: Schema.optional<Schema.String>;
|
|
116
|
+
/** Credential swap; `none` and `static` only. */
|
|
117
|
+
readonly credential: Schema.optional<Schema.Union<readonly [Schema.Struct<{
|
|
118
|
+
readonly kind: Schema.Literal<"none">;
|
|
119
|
+
}>, Schema.Struct<{
|
|
120
|
+
readonly kind: Schema.Literal<"static">;
|
|
121
|
+
readonly placement: Schema.Union<readonly [Schema.Literal<"bearer">, Schema.Struct<{
|
|
122
|
+
readonly header: Schema.String;
|
|
123
|
+
}>, Schema.Struct<{
|
|
124
|
+
readonly query: Schema.String;
|
|
125
|
+
}>]>;
|
|
126
|
+
}>, Schema.Struct<{
|
|
127
|
+
readonly kind: Schema.Literal<"oauth">;
|
|
128
|
+
readonly scope: Schema.optional<Schema.String>;
|
|
129
|
+
readonly clientId: Schema.optional<Schema.String>;
|
|
130
|
+
/** An operator-registered REST provider; absent for an MCP server's own sign-in. */
|
|
131
|
+
readonly provider: Schema.optional<Schema.String>;
|
|
132
|
+
}>]>>;
|
|
133
|
+
readonly secret: Schema.optional<Schema.String>;
|
|
134
|
+
}>;
|
|
135
|
+
export type UpdateInput = typeof updateInputSchema.Type;
|
|
136
|
+
/** One typed OpenAPI operation call. */
|
|
137
|
+
export declare const callInputSchema: Schema.Struct<{
|
|
138
|
+
readonly operation: Schema.String;
|
|
139
|
+
readonly input: Schema.Unknown;
|
|
140
|
+
}>;
|
|
141
|
+
export type CallInput = typeof callInputSchema.Type;
|
|
71
142
|
export declare const egressRefSchema: Schema.Struct<{
|
|
72
143
|
readonly connectionId: Schema.String;
|
|
73
144
|
readonly grantVersion: Schema.Number;
|
|
@@ -87,6 +158,55 @@ export type CustodyAdapter = Readonly<{
|
|
|
87
158
|
put: (address: CustodyAddress, secret: string) => Effect.Effect<string, ConnectionsError>;
|
|
88
159
|
inject: (address: CustodyAddress, locator: string, request: Request) => Effect.Effect<Response, ConnectionsError>;
|
|
89
160
|
remove: (address: CustodyAddress, locator: string) => Effect.Effect<"deleted" | "absent", ConnectionsError>;
|
|
161
|
+
/**
|
|
162
|
+
* Release a sealed OAuth bundle to the owner's connections core, which runs
|
|
163
|
+
* the authorization flow and refreshes tokens. Never reaches an Agent.
|
|
164
|
+
*/
|
|
165
|
+
open?: (address: CustodyAddress, locator: string) => Effect.Effect<string, ConnectionsError>;
|
|
166
|
+
/**
|
|
167
|
+
* A custodian that runs the authorization itself (Nango's Connect) returns
|
|
168
|
+
* the page to send the owner to; it settles through a signed completion
|
|
169
|
+
* (`complete`) carrying the `state` given here, not through our callback.
|
|
170
|
+
*/
|
|
171
|
+
/**
|
|
172
|
+
* Ask a custodian that ran an authorization whether it finished, when its
|
|
173
|
+
* signed completion never arrived. Only an exact match of the attempt's
|
|
174
|
+
* state, actor and integration counts; two matches are ambiguous.
|
|
175
|
+
*/
|
|
176
|
+
find?: (address: CustodyAddress, attempt: Readonly<{
|
|
177
|
+
state: string;
|
|
178
|
+
actor: string;
|
|
179
|
+
integrationId: string;
|
|
180
|
+
}>) => Effect.Effect<CustodyLookup, ConnectionsError>;
|
|
181
|
+
connect?: (address: CustodyAddress, attempt: Readonly<{
|
|
182
|
+
state: string;
|
|
183
|
+
actor: string;
|
|
184
|
+
integrationId: string;
|
|
185
|
+
}>) => Effect.Effect<Readonly<{
|
|
186
|
+
connectUrl: string;
|
|
187
|
+
expiresAt: number;
|
|
188
|
+
}>, ConnectionsError>;
|
|
189
|
+
}>;
|
|
190
|
+
export type CustodyLookup = Readonly<{
|
|
191
|
+
status: "unresolved";
|
|
192
|
+
}> | Readonly<{
|
|
193
|
+
status: "ambiguous";
|
|
194
|
+
}> | Readonly<{
|
|
195
|
+
status: "found";
|
|
196
|
+
locator: string;
|
|
197
|
+
}>;
|
|
198
|
+
/**
|
|
199
|
+
* What a custodian proves after it ran an authorization: the attempt's state
|
|
200
|
+
* and where it keeps the credential. The host verifies the custodian's
|
|
201
|
+
* signature before it builds one.
|
|
202
|
+
*/
|
|
203
|
+
export type CustodyCompletion = Readonly<{
|
|
204
|
+
state: string;
|
|
205
|
+
locator: string;
|
|
206
|
+
/** Who the custodian says signed in; it must be who began the attempt. */
|
|
207
|
+
principalId: string;
|
|
208
|
+
/** The integration the custodian ran it through; it must be the one the attempt began with. */
|
|
209
|
+
integrationId: string;
|
|
90
210
|
}>;
|
|
91
211
|
export type SqlValue = string | number | null;
|
|
92
212
|
/** The owner's SQLite in Cloudflare `ctx.storage` shape. */
|
|
@@ -98,13 +218,69 @@ export type SqlStore = Readonly<{
|
|
|
98
218
|
}>;
|
|
99
219
|
transactionSync: <A>(operation: () => A) => A;
|
|
100
220
|
}>;
|
|
221
|
+
/**
|
|
222
|
+
* A REST provider the operator registered an app with. Such providers publish
|
|
223
|
+
* no metadata the SDK can discover, and many require a pre-registered client,
|
|
224
|
+
* so the host names the endpoints and holds the client. Only the operator's
|
|
225
|
+
* configuration reaches this type; neither an Agent nor an owner supplies one.
|
|
226
|
+
*/
|
|
227
|
+
export type StaticOAuthProvider = Readonly<{
|
|
228
|
+
issuer: string;
|
|
229
|
+
authorizationEndpoint: string;
|
|
230
|
+
tokenEndpoint: string;
|
|
231
|
+
/** Scopes requested when the owner names none. */
|
|
232
|
+
scope?: string;
|
|
233
|
+
client: {
|
|
234
|
+
client_id: string;
|
|
235
|
+
client_secret?: string | undefined;
|
|
236
|
+
};
|
|
237
|
+
/** The https origins this provider's API serves; its token goes nowhere else. */
|
|
238
|
+
apiOrigins: readonly string[];
|
|
239
|
+
}>;
|
|
240
|
+
/**
|
|
241
|
+
* An integration a custodian runs the sign-in for, as the operator declared
|
|
242
|
+
* it: the https origins its credential may reach. The custodian attaches the
|
|
243
|
+
* credential to whatever origin it is handed, so the core pins it here.
|
|
244
|
+
*/
|
|
245
|
+
export type CustodyProvider = Readonly<{
|
|
246
|
+
apiOrigins: readonly string[];
|
|
247
|
+
}>;
|
|
101
248
|
export type ConnectionsOptions = Readonly<{
|
|
102
249
|
ownerId: string;
|
|
103
250
|
store: SqlStore;
|
|
104
251
|
custody: CustodyAdapter;
|
|
105
252
|
/** Host authority read: may this actor manage connections, or still stand as a grantor? */
|
|
106
253
|
authorize: (actorId: string, purpose: "manage" | "grantor") => boolean;
|
|
254
|
+
/**
|
|
255
|
+
* Asked right before every send, with the grantor who stands behind the
|
|
256
|
+
* grant. The host keeps its own live recheck here (membership, write
|
|
257
|
+
* rights); the core never learns what the host's policy is.
|
|
258
|
+
*/
|
|
259
|
+
check?: (grantor: string, grant: Readonly<{
|
|
260
|
+
agentId: string;
|
|
261
|
+
connectionId: string;
|
|
262
|
+
tools: Tools;
|
|
263
|
+
}>, operation: Readonly<{
|
|
264
|
+
name: string;
|
|
265
|
+
effect: "read" | "write" | null;
|
|
266
|
+
}> | null) => Promise<boolean>;
|
|
107
267
|
egressPolicy?: PublicEgressPolicy;
|
|
268
|
+
/**
|
|
269
|
+
* Needed only for `oauth` connections: where the authorization server sends
|
|
270
|
+
* the owner back, and the checked public transport for discovery, token and
|
|
271
|
+
* refresh requests. Custody must also implement `open`.
|
|
272
|
+
*/
|
|
273
|
+
oauth?: Readonly<{
|
|
274
|
+
callbackUrl: string;
|
|
275
|
+
fetch: (request: Request) => Promise<Response>;
|
|
276
|
+
/** REST providers the operator registered an app with, by id. */
|
|
277
|
+
providers?: Readonly<Record<string, StaticOAuthProvider>>;
|
|
278
|
+
}> | undefined;
|
|
279
|
+
/**
|
|
280
|
+
* Integrations the custodian signs in through, by id. An OAuth provider id
|
|
281
|
+
* that is neither here nor in `oauth.providers` is refused.
|
|
282
|
+
*/
|
|
283
|
+
custodyProviders?: Readonly<Record<string, CustodyProvider>> | undefined;
|
|
108
284
|
clock: () => number;
|
|
109
285
|
ids: () => string;
|
|
110
286
|
}>;
|
|
@@ -116,6 +292,8 @@ export type ConnectionProjection = Readonly<{
|
|
|
116
292
|
name: string;
|
|
117
293
|
surface: SurfaceView;
|
|
118
294
|
tools: Tools;
|
|
295
|
+
/** Bumped by every `update`. */
|
|
296
|
+
version: number;
|
|
119
297
|
authorityVersion: number;
|
|
120
298
|
grantVersion: number;
|
|
121
299
|
}>[];
|
|
@@ -123,17 +301,24 @@ export type ConnectionProjection = Readonly<{
|
|
|
123
301
|
export type ConnectionView = Readonly<{
|
|
124
302
|
connectionId: string;
|
|
125
303
|
name: string;
|
|
304
|
+
label: string;
|
|
126
305
|
surface: Readonly<{
|
|
127
306
|
kind: "mcp" | "openapi";
|
|
128
307
|
origin: string;
|
|
308
|
+
url: string;
|
|
309
|
+
digest: string | null;
|
|
129
310
|
}>;
|
|
130
311
|
credentialKind: Credential["kind"];
|
|
131
|
-
|
|
312
|
+
/** How the secret is placed; the secret itself never appears. */
|
|
313
|
+
credential: Credential;
|
|
314
|
+
state: "pending" | "active" | "reconnect_required" | "revoked";
|
|
315
|
+
version: number;
|
|
132
316
|
authorityVersion: number;
|
|
133
317
|
grants: readonly Readonly<{
|
|
134
318
|
agentId: string;
|
|
135
319
|
tools: Tools;
|
|
136
320
|
version: number;
|
|
321
|
+
grantor: string;
|
|
137
322
|
}>[];
|
|
138
323
|
}>;
|
|
139
324
|
/** The Agent's narrowed capability: only the owner opens the credential. */
|
|
@@ -9,6 +9,8 @@ export class ConnectionsError extends Error {
|
|
|
9
9
|
this.code = code;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
/** How long a sign-in attempt stays open before it expires. */
|
|
13
|
+
export const attemptTtlMs = 10 * 60 * 1000;
|
|
12
14
|
export const identifier = Schema.String.pipe(Schema.check(Schema.isPattern(/^[A-Za-z0-9_.:-]{1,128}$/u)));
|
|
13
15
|
const headerName = Schema.String.pipe(Schema.check(Schema.isPattern(/^[A-Za-z0-9-]{1,64}$/u)));
|
|
14
16
|
const queryName = Schema.String.pipe(Schema.check(Schema.isPattern(/^[A-Za-z0-9_.-]{1,64}$/u)));
|
|
@@ -18,10 +20,21 @@ const placementSchema = Schema.Union([
|
|
|
18
20
|
Schema.Struct({ header: headerName }),
|
|
19
21
|
Schema.Struct({ query: queryName }),
|
|
20
22
|
]);
|
|
21
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* How the final request is authorized. For `oauth` the owner may bring its own
|
|
25
|
+
* client (`clientId`, with the client secret as the draft's `secret`); without
|
|
26
|
+
* one the server's dynamic registration is used.
|
|
27
|
+
*/
|
|
22
28
|
export const credentialSchema = Schema.Union([
|
|
23
29
|
Schema.Struct({ kind: Schema.Literal("none") }),
|
|
24
30
|
Schema.Struct({ kind: Schema.Literal("static"), placement: placementSchema }),
|
|
31
|
+
Schema.Struct({
|
|
32
|
+
kind: Schema.Literal("oauth"),
|
|
33
|
+
scope: Schema.optional(Schema.String),
|
|
34
|
+
clientId: Schema.optional(Schema.String),
|
|
35
|
+
/** An operator-registered REST provider; absent for an MCP server's own sign-in. */
|
|
36
|
+
provider: Schema.optional(Schema.String),
|
|
37
|
+
}),
|
|
25
38
|
]);
|
|
26
39
|
/** What the Agent can call. The OpenAPI table is decoded by Forage before it is stored. */
|
|
27
40
|
const surfaceDraftSchema = Schema.Union([
|
|
@@ -29,17 +42,21 @@ const surfaceDraftSchema = Schema.Union([
|
|
|
29
42
|
Schema.Struct({
|
|
30
43
|
kind: Schema.Literal("openapi"),
|
|
31
44
|
baseUrl: Schema.String,
|
|
32
|
-
table: Schema.Unknown,
|
|
45
|
+
table: Schema.Array(Schema.Unknown),
|
|
33
46
|
}),
|
|
34
47
|
]);
|
|
35
48
|
export const toolsSchema = Schema.Union([
|
|
36
49
|
Schema.Literal("all"),
|
|
50
|
+
/** OpenAPI only: every operation whose stored effect is `read`. */
|
|
51
|
+
Schema.Struct({ effect: Schema.Literal("read") }),
|
|
37
52
|
Schema.Array(Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(256)))).pipe(Schema.check(Schema.isMaxLength(256))),
|
|
38
53
|
]);
|
|
39
54
|
export const draftSchema = Schema.Struct({
|
|
40
55
|
/** Idempotency key: a replay with the same request returns the same connection. */
|
|
41
56
|
requestId: identifier,
|
|
42
57
|
name: Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(128))),
|
|
58
|
+
/** Display text for owners; defaults to the name. */
|
|
59
|
+
label: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(128)))),
|
|
43
60
|
surface: surfaceDraftSchema,
|
|
44
61
|
credential: credentialSchema,
|
|
45
62
|
secret: Schema.optional(Schema.String.pipe(Schema.check(Schema.isPattern(/^[\x21-\x7e]{1,16384}$/u)))),
|
|
@@ -49,6 +66,31 @@ export const grantInputSchema = Schema.Struct({
|
|
|
49
66
|
agentId: identifier,
|
|
50
67
|
tools: toolsSchema,
|
|
51
68
|
});
|
|
69
|
+
/** The authorization server's redirect back to the owner. */
|
|
70
|
+
export const completeInputSchema = Schema.Struct({
|
|
71
|
+
state: Schema.String.pipe(Schema.check(Schema.isMaxLength(512))),
|
|
72
|
+
code: Schema.String.pipe(Schema.check(Schema.isMaxLength(4096))),
|
|
73
|
+
iss: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMaxLength(2048)))),
|
|
74
|
+
});
|
|
75
|
+
/** Owner edits to a live connection, guarded by the version the owner last saw. */
|
|
76
|
+
export const updateInputSchema = Schema.Struct({
|
|
77
|
+
requestId: identifier,
|
|
78
|
+
connectionId: identifier,
|
|
79
|
+
expectedVersion: version,
|
|
80
|
+
label: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(128)))),
|
|
81
|
+
name: Schema.optional(Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(128)))),
|
|
82
|
+
/** Replacement OpenAPI operation table, decoded by Forage. */
|
|
83
|
+
table: Schema.optional(Schema.Array(Schema.Unknown)),
|
|
84
|
+
baseUrl: Schema.optional(Schema.String),
|
|
85
|
+
/** Credential swap; `none` and `static` only. */
|
|
86
|
+
credential: Schema.optional(credentialSchema),
|
|
87
|
+
secret: Schema.optional(Schema.String.pipe(Schema.check(Schema.isPattern(/^[\x21-\x7e]{1,16384}$/u)))),
|
|
88
|
+
});
|
|
89
|
+
/** One typed OpenAPI operation call. */
|
|
90
|
+
export const callInputSchema = Schema.Struct({
|
|
91
|
+
operation: Schema.String.pipe(Schema.check(Schema.isMaxLength(256))),
|
|
92
|
+
input: Schema.Unknown,
|
|
93
|
+
});
|
|
52
94
|
export const egressRefSchema = Schema.Struct({
|
|
53
95
|
connectionId: identifier,
|
|
54
96
|
grantVersion: version,
|