@fungi.computer/veil 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) 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 +153 -7
  4. package/dist/connections/contract.js +42 -2
  5. package/dist/connections/core.d.ts +55 -5
  6. package/dist/connections/core.js +365 -106
  7. package/dist/connections/discover.d.ts +3 -0
  8. package/dist/connections/discover.js +23 -0
  9. package/dist/connections/egress.js +3 -2
  10. package/dist/connections/index.d.ts +1 -0
  11. package/dist/connections/index.js +1 -0
  12. package/dist/connections/internal.d.ts +78 -0
  13. package/dist/connections/internal.js +66 -0
  14. package/dist/connections/mcp-agent.d.ts +37 -0
  15. package/dist/connections/mcp-agent.js +75 -0
  16. package/dist/connections/nango.d.ts +30 -0
  17. package/dist/connections/nango.js +136 -0
  18. package/dist/connections/oauth-flow.d.ts +81 -0
  19. package/dist/connections/oauth-flow.js +322 -0
  20. package/dist/connections/oauth-provider.d.ts +31 -0
  21. package/dist/connections/oauth-provider.js +96 -0
  22. package/dist/connections/oauth.d.ts +93 -0
  23. package/dist/connections/oauth.js +239 -0
  24. package/dist/connections/openapi.d.ts +36 -0
  25. package/dist/connections/openapi.js +120 -0
  26. package/dist/connections/sealed.js +23 -4
  27. package/dist/connections/source.d.ts +17 -0
  28. package/dist/connections/source.js +111 -0
  29. package/dist/connections/update.d.ts +41 -0
  30. package/dist/connections/update.js +81 -0
  31. package/dist/gatekeeper.d.ts +21 -1
  32. package/dist/gatekeeper.js +74 -12
  33. package/dist/nango.d.ts +41 -0
  34. package/dist/nango.js +6 -6
  35. package/package.json +11 -1
@@ -0,0 +1,81 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { ConnectionsError, updateInputSchema, } from "./contract.js";
3
+ import { decode, io, sync } from "./internal.js";
4
+ import { decodeTable } from "./openapi.js";
5
+ /** Owner edits guarded by the version the owner last saw; replays are idempotent. */
6
+ export function createUpdate(deps) {
7
+ const { store } = deps;
8
+ return (actor, raw) => Effect.gen(function* () {
9
+ const input = yield* sync(() => {
10
+ store.authorize(actor);
11
+ const parsed = decode(updateInputSchema, raw);
12
+ if (parsed.credential?.kind === "oauth" ||
13
+ (parsed.credential?.kind === "static") !==
14
+ (parsed.secret !== undefined))
15
+ throw new ConnectionsError(parsed.credential === undefined ? "invalid_input" : "unsupported");
16
+ return parsed;
17
+ });
18
+ const stamp = JSON.stringify([
19
+ "update",
20
+ input.connectionId,
21
+ input.expectedVersion,
22
+ input.name ?? null,
23
+ input.label ?? null,
24
+ input.baseUrl ?? null,
25
+ input.table === undefined ? null : JSON.stringify(input.table),
26
+ input.credential ?? null,
27
+ input.secret !== undefined,
28
+ ]);
29
+ const replay = yield* sync(() => {
30
+ const receipt = store.receipt(input.requestId);
31
+ if (receipt !== null && receipt.stamp !== stamp)
32
+ throw new ConnectionsError("conflict");
33
+ return receipt;
34
+ });
35
+ if (replay !== null)
36
+ return { connectionId: replay.connectionId, version: replay.version };
37
+ const row = yield* sync(() => {
38
+ const found = store.connection(input.connectionId);
39
+ if (found.version !== input.expectedVersion)
40
+ throw new ConnectionsError("conflict");
41
+ if (found.state === "revoked" || found.state === "pending")
42
+ throw new ConnectionsError(found.state === "revoked" ? "revoked" : "pending");
43
+ if ((input.table !== undefined || input.baseUrl !== undefined) &&
44
+ found.surface.kind !== "openapi")
45
+ throw new ConnectionsError("invalid_input");
46
+ // A new destination must not inherit a credential sealed for the old one.
47
+ if (input.baseUrl !== undefined &&
48
+ input.credential === undefined &&
49
+ found.credential.kind !== "none" &&
50
+ new URL(input.baseUrl).href !== found.surface.url)
51
+ throw new ConnectionsError("invalid_input");
52
+ if (input.name !== undefined)
53
+ store.checkName(input.name, found.id);
54
+ return found;
55
+ });
56
+ const baseUrl = yield* sync(() => input.baseUrl === undefined
57
+ ? row.surface.url
58
+ : store.publicUrl(input.baseUrl));
59
+ const next = input.table === undefined
60
+ ? null
61
+ : yield* io(() => decodeTable(input.table));
62
+ const credential = input.credential ?? row.credential;
63
+ const locator = input.secret === undefined
64
+ ? row.locator
65
+ : yield* deps.custody.put({ ownerId: deps.ownerId, connectionId: row.id, credential }, input.secret);
66
+ return yield* sync(() => store.commit({
67
+ actor,
68
+ update: input,
69
+ stamp,
70
+ row,
71
+ baseUrl,
72
+ table: next?.operations ?? row.surface.table,
73
+ digest: next?.digest ?? row.surface.digest ?? null,
74
+ credential,
75
+ locator,
76
+ })).pipe(Effect.tapError(() => sync(() => {
77
+ if (input.secret !== undefined && locator !== null)
78
+ store.discard(row.id, locator, credential);
79
+ })));
80
+ });
81
+ }
@@ -1,6 +1,7 @@
1
- import { type OpenApiCredentialStore, type OpenApiOAuthProvider, type OpenApiDoorOptions } from "./openapi.js";
1
+ import { type OpenApiCredentialKeyring, type OpenApiCredentialStore, type OpenApiOAuthProvider, type OpenApiDoorOptions } from "./openapi.js";
2
2
  import { type OpenApiEffects } from "./openapi-effect.js";
3
3
  import type { PublicConnect } from "./public-fetch.js";
4
+ import { type ConnectionsCustodian } from "./connections/nango.js";
4
5
  import { type WorldSeatCustodian } from "./world-seat.js";
5
6
  import type { ModelKeyCustodian } from "./model-keys.js";
6
7
  import { createModelKeyEgress, type ModelKeyGrants } from "./model-key-grants.js";
@@ -21,6 +22,7 @@ import * as Schema from "effect/Schema";
21
22
  */
22
23
  export { providerFetch };
23
24
  export type { ProviderEgress, ProviderRoute };
25
+ export type { ConnectionsCustodian };
24
26
  /** Private Worker bindings, never Team storage or public response fields. */
25
27
  export type VeilBindings = ProviderKeyBindings & Readonly<{
26
28
  WEBHOOK_TRIGGER_MASTER_KEY?: string;
@@ -32,6 +34,10 @@ export type VeilBindings = ProviderKeyBindings & Readonly<{
32
34
  NANGO_WEBHOOK_SIGNING_KEY?: string;
33
35
  NANGO_GITHUB_INTEGRATION_ID?: string;
34
36
  NANGO_GITHUB_GIT_INTEGRATION_ID?: string;
37
+ /** API-key integration that holds pasted credentials for Team connections. */
38
+ NANGO_CONNECTIONS_INTEGRATION_ID?: string;
39
+ /** Comma-separated Nango integrations an owner may sign in through for connections. */
40
+ NANGO_CONNECTIONS_OAUTH_INTEGRATIONS?: string;
35
41
  /** Public Connect UI base that Nango's issued links must stay under. */
36
42
  NANGO_CONNECT_BASE_URL?: string;
37
43
  /** Public Nango API origin the Connect UI in the browser must call. */
@@ -51,6 +57,13 @@ export type VeilBindings = ProviderKeyBindings & Readonly<{
51
57
  */
52
58
  HUB_LOCAL_MODE?: string;
53
59
  }>;
60
+ /**
61
+ * Optional Nango custody for Team connections. A Hub with none of the Nango
62
+ * bindings keeps sealed custody and still connects OAuth MCP and OpenAPI.
63
+ */
64
+ export declare function configuredConnectionsCustody(env: VeilBindings): ConnectionsCustodian | undefined;
65
+ /** The Nango integrations an owner may sign in through for connections. */
66
+ export declare function configuredConnectionsOAuthIntegrations(env: VeilBindings): readonly string[];
54
67
  export declare function configuredGitHubState(env: VeilBindings): {
55
68
  github: "off";
56
69
  metadataIntegrationId?: never;
@@ -176,13 +189,18 @@ export declare function configuredComputerGitCustodian(env: VeilBindings): {
176
189
  /** The decoded key selects one of the two configured profiles only. The
177
190
  * selected maintained custodian still verifies the original signed body. */
178
191
  export declare function verifyVeilWebhook(env: VeilBindings, rawBody: string, signature: string): Promise<{
192
+ profile: "connections";
193
+ ownerId: string;
194
+ } | {
179
195
  operation: "creation" | "refresh_failed";
180
196
  completion: import("./index.js").VerifiedCompletion;
181
197
  profile: "computer-git";
198
+ ownerId?: never;
182
199
  } | {
183
200
  operation: "creation" | "refresh_failed";
184
201
  completion: import("./index.js").VerifiedCompletion;
185
202
  profile: "agent-metadata";
203
+ ownerId?: never;
186
204
  }>;
187
205
  export type OpenApiHostOptions = Readonly<{
188
206
  connect: PublicConnect;
@@ -192,6 +210,8 @@ export type OpenApiHostOptions = Readonly<{
192
210
  previousCredentialKeys?: Readonly<Record<string, string>>;
193
211
  oauthProviders?: Readonly<Record<string, OpenApiOAuthProvider>>;
194
212
  }>;
213
+ /** The keyring that seals OpenAPI credentials; only Gatekeeper reads the master key. */
214
+ export declare function openApiCredentialKeyring(env: VeilBindings, previous?: Readonly<Record<string, string>>): OpenApiCredentialKeyring | undefined;
195
215
  /** Anonymous calls need no secret; API-key/bearer calls require configured Veil custody. */
196
216
  export declare function configuredOpenApiDoor(env: VeilBindings, options: OpenApiHostOptions): Readonly<{
197
217
  seal: (access: import("./openapi-contract.js").OpenApiAccess, credential: string, signal: AbortSignal) => Promise<void>;
@@ -2,6 +2,8 @@ import { createOpenApiDoor, } from "./openapi.js";
2
2
  import { createOpenApiEffects } from "./openapi-effect.js";
3
3
  import { createDnsOverHttpsResolver } from "./model-key-network.js";
4
4
  import { createNangoCustodian } from "./nango.js";
5
+ import { ConnectionsError } from "./connections/contract.js";
6
+ import { createNangoCustody, } from "./connections/nango.js";
5
7
  import { createWebhookTriggerSecrets } from "./webhook-trigger-secrets.js";
6
8
  import { createNangoGitCustodian } from "./nango-git.js";
7
9
  import { createNangoModelKeyCustodian } from "./nango-model-keys.js";
@@ -26,6 +28,39 @@ import * as Exit from "effect/Exit";
26
28
  * Hub code reaches a provider only through `configuredProviderEgress`.
27
29
  */
28
30
  export { providerFetch };
31
+ /**
32
+ * Optional Nango custody for Team connections. A Hub with none of the Nango
33
+ * bindings keeps sealed custody and still connects OAuth MCP and OpenAPI.
34
+ */
35
+ export function configuredConnectionsCustody(env) {
36
+ if (!env.NANGO_SERVER_URL ||
37
+ !env.NANGO_API_KEY ||
38
+ !env.NANGO_WEBHOOK_SIGNING_KEY ||
39
+ !env.NANGO_CONNECT_BASE_URL ||
40
+ !env.NANGO_PUBLIC_API_URL ||
41
+ !env.NANGO_CONNECTIONS_INTEGRATION_ID)
42
+ return undefined;
43
+ try {
44
+ return createNangoCustody({
45
+ serverUrl: env.NANGO_SERVER_URL,
46
+ apiKey: env.NANGO_API_KEY,
47
+ webhookSigningKey: env.NANGO_WEBHOOK_SIGNING_KEY,
48
+ staticIntegrationId: env.NANGO_CONNECTIONS_INTEGRATION_ID,
49
+ connectBaseUrl: env.NANGO_CONNECT_BASE_URL,
50
+ publicApiUrl: env.NANGO_PUBLIC_API_URL,
51
+ });
52
+ }
53
+ catch {
54
+ throw new VeilError("unavailable");
55
+ }
56
+ }
57
+ /** The Nango integrations an owner may sign in through for connections. */
58
+ export function configuredConnectionsOAuthIntegrations(env) {
59
+ return (env.NANGO_CONNECTIONS_OAUTH_INTEGRATIONS ?? "")
60
+ .split(",")
61
+ .map((id) => id.trim())
62
+ .filter((id) => id.length > 0);
63
+ }
29
64
  export function configuredGitHubState(env) {
30
65
  const metadataIntegrationId = env.NANGO_GITHUB_INTEGRATION_ID;
31
66
  const gitIntegrationId = env.NANGO_GITHUB_GIT_INTEGRATION_ID;
@@ -156,13 +191,22 @@ export function configuredComputerGitCustodian(env) {
156
191
  * selected maintained custodian still verifies the original signed body. */
157
192
  export async function verifyVeilWebhook(env, rawBody, signature) {
158
193
  const state = configuredGitHubState(env);
159
- if (state.github === "off")
194
+ const connectionIntegrations = configuredConnectionsOAuthIntegrations(env);
195
+ if (state.github === "off" && connectionIntegrations.length === 0)
160
196
  throw new VeilError("github_not_configured");
161
197
  const decoded = Schema.decodeUnknownExit(Schema.fromJsonString(Schema.Struct({
162
198
  providerConfigKey: Schema.NonEmptyString,
163
199
  })))(rawBody);
164
200
  if (Exit.isFailure(decoded))
165
201
  throw new VeilError("invalid_completion");
202
+ // A connection's sign-in runs through an integration the operator listed for it.
203
+ if (connectionIntegrations.includes(decoded.value.providerConfigKey))
204
+ return {
205
+ profile: "connections",
206
+ ownerId: await verifiedConnectionOwner(env, rawBody, signature),
207
+ };
208
+ if (state.github === "off")
209
+ throw new VeilError("github_not_configured");
166
210
  if (decoded.value.providerConfigKey === state.gitIntegrationId) {
167
211
  return {
168
212
  profile: "computer-git",
@@ -177,6 +221,24 @@ export async function verifyVeilWebhook(env, rawBody, signature) {
177
221
  }
178
222
  throw new VeilError("invalid_completion");
179
223
  }
224
+ /** The signed sign-in names the Team that began it; only a verified body is believed. */
225
+ async function verifiedConnectionOwner(env, rawBody, signature) {
226
+ const custody = configuredConnectionsCustody(env);
227
+ if (custody === undefined)
228
+ throw new VeilError("unavailable");
229
+ try {
230
+ return (await custody.verifyCompletion(rawBody, signature)).ownerId;
231
+ }
232
+ catch (cause) {
233
+ if (cause instanceof ConnectionsError)
234
+ throw new VeilError(cause.code === "unsupported"
235
+ ? "unsupported"
236
+ : cause.code === "unavailable"
237
+ ? "unavailable"
238
+ : "invalid_completion", { cause });
239
+ throw cause;
240
+ }
241
+ }
180
242
  function configuredCustodian(env, integrationId) {
181
243
  if (!env.NANGO_SERVER_URL ||
182
244
  !env.NANGO_API_KEY ||
@@ -199,6 +261,14 @@ function configuredCustodian(env, integrationId) {
199
261
  throw new VeilError("unavailable");
200
262
  }
201
263
  }
264
+ /** The keyring that seals OpenAPI credentials; only Gatekeeper reads the master key. */
265
+ export function openApiCredentialKeyring(env, previous) {
266
+ const key = env.OPENAPI_CREDENTIAL_MASTER_KEY;
267
+ if (!key)
268
+ return undefined;
269
+ const id = env.OPENAPI_CREDENTIAL_KEY_ID ?? "primary";
270
+ return { activeKeyId: id, keys: { ...previous, [id]: key } };
271
+ }
202
272
  /** Gatekeeper owns the common opaque configuration for both runtime facades. */
203
273
  function openApiHostConfiguration(env, options) {
204
274
  const doorOptions = {
@@ -208,17 +278,9 @@ function openApiHostConfiguration(env, options) {
208
278
  };
209
279
  if (options.oauthProviders)
210
280
  doorOptions.oauthProviders = options.oauthProviders;
211
- if (env.OPENAPI_CREDENTIAL_MASTER_KEY)
212
- doorOptions.custody = {
213
- keys: {
214
- activeKeyId: env.OPENAPI_CREDENTIAL_KEY_ID ?? "primary",
215
- keys: {
216
- ...options.previousCredentialKeys,
217
- [env.OPENAPI_CREDENTIAL_KEY_ID ?? "primary"]: env.OPENAPI_CREDENTIAL_MASTER_KEY,
218
- },
219
- },
220
- store: options.store,
221
- };
281
+ const keys = openApiCredentialKeyring(env, options.previousCredentialKeys);
282
+ if (keys)
283
+ doorOptions.custody = { keys, store: options.store };
222
284
  return doorOptions;
223
285
  }
224
286
  /** Anonymous calls need no secret; API-key/bearer calls require configured Veil custody. */
package/dist/nango.d.ts CHANGED
@@ -1,4 +1,38 @@
1
+ import * as Schema from "effect/Schema";
1
2
  import { type CredentialCustodian } from "./index.js";
3
+ export declare const tagsSchema: Schema.Struct<{
4
+ readonly veil_team_id: Schema.NonEmptyString;
5
+ readonly veil_attempt_id: Schema.NonEmptyString;
6
+ readonly veil_principal_id: Schema.NonEmptyString;
7
+ }>;
8
+ export declare const sessionSchema: Schema.Struct<{
9
+ readonly data: Schema.Struct<{
10
+ readonly token: Schema.NonEmptyString;
11
+ readonly connect_link: Schema.NonEmptyString;
12
+ readonly expires_at: Schema.DateFromString;
13
+ }>;
14
+ }>;
15
+ export declare const webhookEnvelopeSchema: Schema.Struct<{
16
+ readonly type: Schema.NonEmptyString;
17
+ readonly operation: Schema.optional<Schema.NonEmptyString>;
18
+ }>;
19
+ /** Nango auth webhook fields shared by creation and refresh notifications. */
20
+ export declare const authWebhookSchema: Schema.Struct<{
21
+ readonly type: Schema.Literal<"auth">;
22
+ readonly operation: Schema.Literals<readonly ["creation", "refresh"]>;
23
+ readonly success: Schema.Boolean;
24
+ readonly connectionId: Schema.NonEmptyString;
25
+ readonly providerConfigKey: Schema.NonEmptyString;
26
+ readonly tags: Schema.Struct<{
27
+ readonly veil_team_id: Schema.NonEmptyString;
28
+ readonly veil_attempt_id: Schema.NonEmptyString;
29
+ readonly veil_principal_id: Schema.NonEmptyString;
30
+ }>;
31
+ readonly endUser: Schema.Struct<{
32
+ readonly endUserId: Schema.NonEmptyString;
33
+ readonly organizationId: Schema.NonEmptyString;
34
+ }>;
35
+ }>;
2
36
  /** Trusted host configuration; never supplied by a model or Connect browser.
3
37
  * The public Connect document and API URLs are independent of the internal
4
38
  * serverUrl the Hub uses for custody. */
@@ -12,5 +46,12 @@ export type NangoCustodianOptions = Readonly<{
12
46
  /** Deadline for one custodian request and its body; a miss is `unavailable`. */
13
47
  requestTimeoutMs?: number;
14
48
  }>;
49
+ export declare function publicUrl(value: string): URL;
50
+ /** The Connect document handed to browsers: Nango's issued link, confined to
51
+ * the configured Connect UI and pinned to the configured public API. */
52
+ export declare function connectDocument(session: Readonly<{
53
+ token: string;
54
+ connectUrl: string;
55
+ }>, base: URL, api: URL): string;
15
56
  /** Pinned Nango Connect/connection REST contract and maintained raw-body verifier. */
16
57
  export declare function createNangoCustodian(options: NangoCustodianOptions): CredentialCustodian;
package/dist/nango.js CHANGED
@@ -2,24 +2,24 @@ import * as Schema from "effect/Schema";
2
2
  import * as Exit from "effect/Exit";
3
3
  import { VeilError, } from "./index.js";
4
4
  const text = Schema.NonEmptyString;
5
- const tagsSchema = Schema.Struct({
5
+ export const tagsSchema = Schema.Struct({
6
6
  veil_team_id: text,
7
7
  veil_attempt_id: text,
8
8
  veil_principal_id: text,
9
9
  });
10
- const sessionSchema = Schema.Struct({
10
+ export const sessionSchema = Schema.Struct({
11
11
  data: Schema.Struct({
12
12
  token: text,
13
13
  connect_link: text,
14
14
  expires_at: Schema.DateFromString,
15
15
  }),
16
16
  });
17
- const webhookEnvelopeSchema = Schema.Struct({
17
+ export const webhookEnvelopeSchema = Schema.Struct({
18
18
  type: text,
19
19
  operation: Schema.optional(text),
20
20
  });
21
21
  /** Nango auth webhook fields shared by creation and refresh notifications. */
22
- const authWebhookSchema = Schema.Struct({
22
+ export const authWebhookSchema = Schema.Struct({
23
23
  type: Schema.Literal("auth"),
24
24
  operation: Schema.Literals(["creation", "refresh"]),
25
25
  success: Schema.Boolean,
@@ -52,7 +52,7 @@ const absentSchema = Schema.Struct({
52
52
  });
53
53
  /** A custodian that stops answering is an outage, retried later, never a dead credential. */
54
54
  const defaultRequestTimeoutMs = 10_000;
55
- function publicUrl(value) {
55
+ export function publicUrl(value) {
56
56
  let url;
57
57
  try {
58
58
  url = new URL(value);
@@ -70,7 +70,7 @@ function publicUrl(value) {
70
70
  }
71
71
  /** The Connect document handed to browsers: Nango's issued link, confined to
72
72
  * the configured Connect UI and pinned to the configured public API. */
73
- function connectDocument(session, base, api) {
73
+ export function connectDocument(session, base, api) {
74
74
  let document;
75
75
  try {
76
76
  document = new URL(session.connectUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/veil",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "Team-authorized credential custody and provider access",
6
6
  "type": "module",
@@ -86,6 +86,14 @@
86
86
  "./connections": {
87
87
  "types": "./dist/connections/index.d.ts",
88
88
  "import": "./dist/connections/index.js"
89
+ },
90
+ "./connections/nango": {
91
+ "types": "./dist/connections/nango.d.ts",
92
+ "import": "./dist/connections/nango.js"
93
+ },
94
+ "./connections/source": {
95
+ "types": "./dist/connections/source.d.ts",
96
+ "import": "./dist/connections/source.js"
89
97
  }
90
98
  },
91
99
  "files": [
@@ -96,6 +104,7 @@
96
104
  "dependencies": {
97
105
  "@fungi.computer/cairn": "0.1.0",
98
106
  "@fungi.computer/forage": "0.3.1",
107
+ "@fungi.computer/mycelium": "0.3.4",
99
108
  "@modelcontextprotocol/client": "2.0.0",
100
109
  "@nangohq/node": "0.71.6",
101
110
  "effect": "4.0.2",
@@ -105,6 +114,7 @@
105
114
  },
106
115
  "devDependencies": {
107
116
  "@fungi.computer/watchdog": "0.1.0",
117
+ "@modelcontextprotocol/server": "2.0.0",
108
118
  "@types/node": "24.13.3",
109
119
  "typescript": "5.9.3",
110
120
  "vitest": "4.1.11"