@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.
Files changed (43) hide show
  1. package/README.md +48 -54
  2. package/dist/best-effort-cleanup.d.ts +1 -1
  3. package/dist/connections/cleanup.d.ts +21 -0
  4. package/dist/connections/cleanup.js +66 -0
  5. package/dist/connections/contract.d.ts +192 -7
  6. package/dist/connections/contract.js +44 -2
  7. package/dist/connections/core.d.ts +58 -5
  8. package/dist/connections/core.js +438 -111
  9. package/dist/connections/discover.d.ts +3 -0
  10. package/dist/connections/discover.js +23 -0
  11. package/dist/connections/egress.js +3 -2
  12. package/dist/connections/index.d.ts +1 -0
  13. package/dist/connections/index.js +1 -0
  14. package/dist/connections/internal.d.ts +78 -0
  15. package/dist/connections/internal.js +66 -0
  16. package/dist/connections/mcp-agent.d.ts +37 -0
  17. package/dist/connections/mcp-agent.js +75 -0
  18. package/dist/connections/nango.d.ts +30 -0
  19. package/dist/connections/nango.js +179 -0
  20. package/dist/connections/oauth-flow.d.ts +83 -0
  21. package/dist/connections/oauth-flow.js +330 -0
  22. package/dist/connections/oauth-provider.d.ts +31 -0
  23. package/dist/connections/oauth-provider.js +96 -0
  24. package/dist/connections/oauth.d.ts +93 -0
  25. package/dist/connections/oauth.js +239 -0
  26. package/dist/connections/openapi.d.ts +36 -0
  27. package/dist/connections/openapi.js +120 -0
  28. package/dist/connections/sealed.js +23 -4
  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/gatekeeper.d.ts +43 -85
  34. package/dist/gatekeeper.js +138 -95
  35. package/dist/github-app.d.ts +44 -0
  36. package/dist/github-app.js +93 -0
  37. package/dist/index.d.ts +0 -241
  38. package/dist/index.js +0 -256
  39. package/dist/nango.d.ts +40 -15
  40. package/dist/nango.js +7 -242
  41. package/package.json +15 -9
  42. package/dist/nango-git.d.ts +0 -19
  43. 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 Team doors. It owns
4
- GitHub Connection custody, model-key grants and the Agent's anonymous public-web
5
- fetch. A guest receives an authorized operation, not the provider token or its
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 Team Connection authority and a `CredentialCustodian` to
11
- `createGitHubConnections`. Model input cannot select a repository or private
12
- locator. The `@fungi.computer/veil/nango` adapter needs a configured Nango
13
- origin, API key, webhook signing key, integration ID and public Connect URLs;
14
- these are host secrets or configuration, never browser or model input. Without
15
- custody configuration, report unavailable rather than manufacturing a
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. For an exercised connection
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
- The root export in `src/index.ts` defines Team Connection receipts and
24
- `createGitHubConnections`. `package.json` lists the Nango, model-key and
25
- public-fetch subpaths used by trusted hosts; do not import private locators from
26
- these modules into guest code.
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
- `@fungi.computer/veil/nango` exports `createNangoCustodian`. The trusted host
31
- supplies a Nango server origin, API key, separate webhook signing key, one
32
- integration ID, and the public Connect UI base and public API URL. These are
33
- host configuration, never browser or model input. `openSession` returns the
34
- Connect document browsers open: Nango's issued link, confined to the configured
35
- Connect UI and pinned to the public API with `apiURL`. Unconfigured hosts must
36
- report unavailable instead of substituting fake custody.
37
-
38
- The adapter uses `@nangohq/node@0.71.6` to verify the raw callback body and
39
- ordinary `fetch` with each invocation's AbortSignal for REST operations. It does
40
- not call credential-returning connection inspection. Connect sessions carry
41
- Team-created attempt/principal/Team tags and restrict the integration. Signed
42
- webhooks are verified before decoding; the adapter handles only a successful
43
- `creation` and a failed `refresh` (reconnect evidence) and refuses every other
44
- operation as `unsupported`. It also checks end-user and organization
45
- correlation; Team owns final activation and replay decisions. A verified
46
- completion the Team refuses (late, unknown, superseded, or failing activation)
47
- is deleted from custody, and the settled deletion is recorded.
48
-
49
- Reconciliation queries `/connections` with exact integration, end-user,
50
- organization and attempt tags and a limit of two. The selected server filters
51
- before applying that limit: zero matches remain unresolved, multiple matches are
52
- ambiguous, and every returned row is checked again before a single completion is
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: "nango-git-response-body" | "public-fetch-socket", cleanup: () => Promise<void>): Promise<void>;
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" | "invalid_response" | "unavailable";
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
- /** How the final request is authorized. OAuth joins this union in slice 3. */
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.$Array<Schema.String>]>;
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.$Array<Schema.String>]>;
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
- state: "active" | "reconnect_required" | "revoked";
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
- /** How the final request is authorized. OAuth joins this union in slice 3. */
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,