@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/dist/nango.js CHANGED
@@ -1,25 +1,24 @@
1
1
  import * as Schema from "effect/Schema";
2
- import * as Exit from "effect/Exit";
3
- import { VeilError, } from "./index.js";
2
+ import { VeilError } from "./index.js";
4
3
  const text = Schema.NonEmptyString;
5
- const tagsSchema = Schema.Struct({
4
+ export const tagsSchema = Schema.Struct({
6
5
  veil_team_id: text,
7
6
  veil_attempt_id: text,
8
7
  veil_principal_id: text,
9
8
  });
10
- const sessionSchema = Schema.Struct({
9
+ export const sessionSchema = Schema.Struct({
11
10
  data: Schema.Struct({
12
11
  token: text,
13
12
  connect_link: text,
14
13
  expires_at: Schema.DateFromString,
15
14
  }),
16
15
  });
17
- const webhookEnvelopeSchema = Schema.Struct({
16
+ export const webhookEnvelopeSchema = Schema.Struct({
18
17
  type: text,
19
18
  operation: Schema.optional(text),
20
19
  });
21
20
  /** Nango auth webhook fields shared by creation and refresh notifications. */
22
- const authWebhookSchema = Schema.Struct({
21
+ export const authWebhookSchema = Schema.Struct({
23
22
  type: Schema.Literal("auth"),
24
23
  operation: Schema.Literals(["creation", "refresh"]),
25
24
  success: Schema.Boolean,
@@ -28,31 +27,7 @@ const authWebhookSchema = Schema.Struct({
28
27
  tags: tagsSchema,
29
28
  endUser: Schema.Struct({ endUserId: text, organizationId: text }),
30
29
  });
31
- /** GitHub's own unauthorized body, as relayed verbatim by the Nango proxy.
32
- * Nango's own errors use an `error` envelope and remain unavailable. */
33
- const providerUnauthorizedSchema = Schema.Struct({
34
- message: text,
35
- documentation_url: text,
36
- });
37
- const connectionSchema = Schema.Struct({
38
- connection_id: text,
39
- provider_config_key: text,
40
- tags: tagsSchema,
41
- end_user: Schema.Struct({
42
- id: text,
43
- organization: Schema.Struct({ id: text }),
44
- }),
45
- });
46
- const connectionsSchema = Schema.Struct({
47
- connections: Schema.Array(connectionSchema),
48
- });
49
- const deletedSchema = Schema.Struct({ success: Schema.Literal(true) });
50
- const absentSchema = Schema.Struct({
51
- error: Schema.Struct({ code: Schema.Literal("unknown_connection") }),
52
- });
53
- /** A custodian that stops answering is an outage, retried later, never a dead credential. */
54
- const defaultRequestTimeoutMs = 10_000;
55
- function publicUrl(value) {
30
+ export function publicUrl(value) {
56
31
  let url;
57
32
  try {
58
33
  url = new URL(value);
@@ -70,7 +45,7 @@ function publicUrl(value) {
70
45
  }
71
46
  /** The Connect document handed to browsers: Nango's issued link, confined to
72
47
  * the configured Connect UI and pinned to the configured public API. */
73
- function connectDocument(session, base, api) {
48
+ export function connectDocument(session, base, api) {
74
49
  let document;
75
50
  try {
76
51
  document = new URL(session.connectUrl);
@@ -88,213 +63,3 @@ function connectDocument(session, base, api) {
88
63
  document.searchParams.set("apiURL", api.href);
89
64
  return document.href;
90
65
  }
91
- /** Pinned Nango Connect/connection REST contract and maintained raw-body verifier. */
92
- export function createNangoCustodian(options) {
93
- let server;
94
- try {
95
- server = new URL(options.serverUrl);
96
- }
97
- catch {
98
- throw new VeilError("invalid_input");
99
- }
100
- if (!options.apiKey ||
101
- !options.webhookSigningKey ||
102
- !options.integrationId ||
103
- server.username ||
104
- server.password ||
105
- server.search ||
106
- server.hash ||
107
- server.pathname !== "/" ||
108
- (server.protocol !== "https:" && server.protocol !== "http:"))
109
- throw new VeilError("invalid_input");
110
- const connectBase = publicUrl(options.connectBaseUrl);
111
- if (!connectBase.pathname.endsWith("/"))
112
- connectBase.pathname += "/";
113
- const publicApi = publicUrl(options.publicApiUrl);
114
- const decode = (schema, raw, code = "invalid_response") => {
115
- try {
116
- return Schema.decodeUnknownSync(Schema.fromJsonString(schema))(raw);
117
- }
118
- catch {
119
- throw new VeilError(code);
120
- }
121
- };
122
- const requestTimeoutMs = options.requestTimeoutMs ?? defaultRequestTimeoutMs;
123
- const request = async (path, signal, init = {}) => {
124
- try {
125
- signal.throwIfAborted();
126
- const headers = new Headers(init.headers);
127
- headers.set("Authorization", `Bearer ${options.apiKey}`);
128
- // The deadline also aborts the body stream that `body` reads next.
129
- return await fetch(new URL(path, server), {
130
- ...init,
131
- headers,
132
- signal: AbortSignal.any([
133
- signal,
134
- AbortSignal.timeout(requestTimeoutMs),
135
- ]),
136
- redirect: "manual",
137
- });
138
- }
139
- catch {
140
- throw new VeilError(signal.aborted ? "cancelled" : "unavailable");
141
- }
142
- };
143
- const body = async (response, signal) => {
144
- try {
145
- return await response.text();
146
- }
147
- catch {
148
- throw new VeilError(signal.aborted ? "cancelled" : "unavailable");
149
- }
150
- };
151
- const requireSuccess = (response) => {
152
- if (!response.ok)
153
- throw new VeilError("unavailable");
154
- };
155
- const tags = (teamId, attemptId, principalId) => ({
156
- veil_team_id: teamId,
157
- veil_attempt_id: attemptId,
158
- veil_principal_id: principalId,
159
- });
160
- const completion = (row, integrationId, locator) => ({
161
- teamId: row.veil_team_id,
162
- attemptId: row.veil_attempt_id,
163
- principalId: row.veil_principal_id,
164
- integrationId,
165
- locator,
166
- });
167
- /**
168
- * Maintained HMAC over the raw body first; only then decode the operation.
169
- * The Nango SDK (and its HTTP client) loads with the first webhook rather
170
- * than with every Worker that hosts a custodian.
171
- */
172
- const verifyWebhook = (rawBody, signature) => import("@nangohq/node").then(({ Nango }) => {
173
- const verifier = new Nango({
174
- host: server.origin,
175
- apiKey: options.apiKey,
176
- webhookSigningKey: options.webhookSigningKey,
177
- });
178
- if (!verifier.verifyIncomingWebhookRequest(rawBody, {
179
- "X-Nango-Hmac-Sha256": signature,
180
- }))
181
- throw new VeilError("invalid_completion");
182
- const envelope = decode(webhookEnvelopeSchema, rawBody, "invalid_completion");
183
- if (envelope.type !== "auth" ||
184
- (envelope.operation !== "creation" && envelope.operation !== "refresh"))
185
- throw new VeilError("unsupported");
186
- const row = decode(authWebhookSchema, rawBody, "invalid_completion");
187
- // Handled: a successful creation, or a failed credential refresh.
188
- if (row.success !== (row.operation === "creation"))
189
- throw new VeilError("unsupported");
190
- if (row.providerConfigKey !== options.integrationId ||
191
- row.endUser.endUserId !== row.tags.veil_principal_id ||
192
- row.endUser.organizationId !== row.tags.veil_team_id)
193
- throw new VeilError("invalid_completion");
194
- return {
195
- operation: row.operation === "creation" ? "creation" : "refresh_failed",
196
- completion: completion(row.tags, row.providerConfigKey, row.connectionId),
197
- };
198
- });
199
- return {
200
- async openSession(input, signal) {
201
- if (input.integrationId !== options.integrationId)
202
- throw new VeilError("denied");
203
- const response = await request("/connect/sessions", signal, {
204
- method: "POST",
205
- headers: { "Content-Type": "application/json" },
206
- body: JSON.stringify({
207
- allowed_integrations: [options.integrationId],
208
- end_user: { id: input.principalId },
209
- organization: { id: input.teamId },
210
- tags: tags(input.teamId, input.attemptId, input.principalId),
211
- }),
212
- });
213
- requireSuccess(response);
214
- const { data } = decode(sessionSchema, await body(response, signal));
215
- return {
216
- token: data.token,
217
- connectUrl: connectDocument({ token: data.token, connectUrl: data.connect_link }, connectBase, publicApi),
218
- expiresAt: data.expires_at.getTime(),
219
- };
220
- },
221
- verifyWebhook,
222
- async verifyCompletion(rawBody, signature) {
223
- const event = await verifyWebhook(rawBody, signature);
224
- if (event.operation !== "creation")
225
- throw new VeilError("unsupported");
226
- return event.completion;
227
- },
228
- async findCompletion(attempt, signal) {
229
- if (attempt.integrationId !== options.integrationId)
230
- throw new VeilError("denied");
231
- const expected = tags(attempt.teamId, attempt.attemptId, attempt.actorUserId);
232
- const query = new URLSearchParams({
233
- integrationId: options.integrationId,
234
- endUserId: attempt.actorUserId,
235
- endUserOrganizationId: attempt.teamId,
236
- limit: "2",
237
- page: "0",
238
- });
239
- for (const [key, value] of Object.entries(expected))
240
- query.set(`tags[${key}]`, value);
241
- const response = await request(`/connections?${query.toString()}`, signal);
242
- requireSuccess(response);
243
- const { connections } = decode(connectionsSchema, await body(response, signal));
244
- // Selected server filters before LIMIT; two rows are sufficient to prove ambiguity.
245
- for (const row of connections) {
246
- if (row.provider_config_key !== options.integrationId ||
247
- row.end_user.id !== attempt.actorUserId ||
248
- row.end_user.organization.id !== attempt.teamId ||
249
- row.tags.veil_team_id !== attempt.teamId ||
250
- row.tags.veil_attempt_id !== attempt.attemptId ||
251
- row.tags.veil_principal_id !== attempt.actorUserId)
252
- throw new VeilError("invalid_response");
253
- }
254
- if (connections.length === 0)
255
- return { status: "unresolved" };
256
- if (connections.length !== 1)
257
- return { status: "ambiguous" };
258
- const row = connections[0];
259
- if (!row)
260
- throw new VeilError("invalid_response");
261
- return {
262
- status: "verified",
263
- completion: completion(row.tags, row.provider_config_key, row.connection_id),
264
- };
265
- },
266
- async deleteConnection(locator, signal) {
267
- const query = new URLSearchParams({
268
- provider_config_key: options.integrationId,
269
- });
270
- const response = await request(`/connections/${encodeURIComponent(locator)}?${query.toString()}`, signal, { method: "DELETE" });
271
- const raw = await body(response, signal);
272
- // This pinned public endpoint reports absent as HTTP400, not a generic404.
273
- if (response.status === 400) {
274
- decode(absentSchema, raw);
275
- return "absent";
276
- }
277
- requireSuccess(response);
278
- decode(deletedSchema, raw);
279
- return "deleted";
280
- },
281
- async readRepository(access, signal) {
282
- // WHATWG URL normalizes dot segments, including percent-encoded dots.
283
- if ([access.repository.owner, access.repository.name].some((part) => part === "." || part === ".."))
284
- throw new VeilError("invalid_input");
285
- const path = `/proxy/repos/${encodeURIComponent(access.repository.owner)}/${encodeURIComponent(access.repository.name)}`;
286
- const response = await request(path, signal, {
287
- headers: {
288
- "Connection-Id": access.locator,
289
- "Provider-Config-Key": options.integrationId,
290
- },
291
- });
292
- if (response.status === 401) {
293
- const evidence = Schema.decodeUnknownExit(Schema.fromJsonString(providerUnauthorizedSchema))(await body(response, signal));
294
- throw new VeilError(Exit.isSuccess(evidence) ? "reconnect_required" : "unavailable");
295
- }
296
- requireSuccess(response);
297
- return await body(response, signal);
298
- },
299
- };
300
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/veil",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "Team-authorized credential custody and provider access",
6
6
  "type": "module",
@@ -11,14 +11,6 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
13
  },
14
- "./nango-git": {
15
- "types": "./dist/nango-git.d.ts",
16
- "import": "./dist/nango-git.js"
17
- },
18
- "./nango": {
19
- "types": "./dist/nango.d.ts",
20
- "import": "./dist/nango.js"
21
- },
22
14
  "./model-credentials": {
23
15
  "types": "./dist/model-credentials.d.ts",
24
16
  "import": "./dist/model-credentials.js"
@@ -86,6 +78,18 @@
86
78
  "./connections": {
87
79
  "types": "./dist/connections/index.d.ts",
88
80
  "import": "./dist/connections/index.js"
81
+ },
82
+ "./connections/nango": {
83
+ "types": "./dist/connections/nango.d.ts",
84
+ "import": "./dist/connections/nango.js"
85
+ },
86
+ "./github-app": {
87
+ "types": "./dist/github-app.d.ts",
88
+ "import": "./dist/github-app.js"
89
+ },
90
+ "./connections/source": {
91
+ "types": "./dist/connections/source.d.ts",
92
+ "import": "./dist/connections/source.js"
89
93
  }
90
94
  },
91
95
  "files": [
@@ -96,6 +100,7 @@
96
100
  "dependencies": {
97
101
  "@fungi.computer/cairn": "0.1.0",
98
102
  "@fungi.computer/forage": "0.3.1",
103
+ "@fungi.computer/mycelium": "0.3.4",
99
104
  "@modelcontextprotocol/client": "2.0.0",
100
105
  "@nangohq/node": "0.71.6",
101
106
  "effect": "4.0.2",
@@ -105,6 +110,7 @@
105
110
  },
106
111
  "devDependencies": {
107
112
  "@fungi.computer/watchdog": "0.1.0",
113
+ "@modelcontextprotocol/server": "2.0.0",
108
114
  "@types/node": "24.13.3",
109
115
  "typescript": "5.9.3",
110
116
  "vitest": "4.1.11"
@@ -1,19 +0,0 @@
1
- /** Private Team-owned Computer authorization, never an Agent metadata Grant. */
2
- export type GitRepositoryAccess = Readonly<{
3
- repository: Readonly<{
4
- owner: string;
5
- name: string;
6
- }>;
7
- locator: string;
8
- integrationId: string;
9
- }>;
10
- /** The caller must obtain fresh Computer/repository authorization before each
11
- * invocation. This adapter owns Nango control headers, not authorization state. */
12
- export declare function createNangoGitCustodian(options: Readonly<{
13
- serverUrl: string;
14
- apiKey: string;
15
- integrationId: string;
16
- }>): {
17
- /** Fetch, clone and push for one authorized repository. */
18
- readGit(access: GitRepositoryAccess, request: Request): Promise<Response>;
19
- };
package/dist/nango-git.js DELETED
@@ -1,125 +0,0 @@
1
- import { VeilError } from "./index.js";
2
- import { bestEffortCleanup } from "./best-effort-cleanup.js";
3
- /** The four smart-HTTP exchanges this proxy admits for one repository, the
4
- * same operation table as the Artifacts custodian. Receive-pack is admitted
5
- * here; GitHub's own installation permission and branch rules decide it. */
6
- function gitOperation(request, url, base) {
7
- for (const service of ["git-upload-pack", "git-receive-pack"]) {
8
- const push = service === "git-receive-pack";
9
- if (request.method === "GET" &&
10
- url.pathname === `${base}/info/refs` &&
11
- url.search === `?service=${service}`)
12
- return {
13
- push,
14
- requestType: null,
15
- responseType: `application/x-${service}-advertisement`,
16
- };
17
- if (request.method === "POST" &&
18
- url.pathname === `${base}/${service}` &&
19
- url.search === "")
20
- return {
21
- push,
22
- requestType: `application/x-${service}-request`,
23
- responseType: `application/x-${service}-result`,
24
- };
25
- }
26
- return null;
27
- }
28
- /** GitHub refusals keep a typed code; vendor bodies never cross this port. */
29
- function upstreamRefusal(response) {
30
- if (response.status === 429 ||
31
- (response.status === 403 &&
32
- response.headers.get("X-RateLimit-Remaining") === "0"))
33
- return new VeilError("rate_limited");
34
- // GitHub hides a repository outside the installation behind 404.
35
- if (response.status === 404)
36
- return new VeilError("repository_not_installed");
37
- // A read-only installation token is refused write with 403.
38
- if (response.status === 403)
39
- return new VeilError("denied");
40
- return new VeilError("unavailable");
41
- }
42
- /** The caller must obtain fresh Computer/repository authorization before each
43
- * invocation. This adapter owns Nango control headers, not authorization state. */
44
- export function createNangoGitCustodian(options) {
45
- if (!URL.canParse(options.serverUrl))
46
- throw new VeilError("invalid_input");
47
- const server = new URL(options.serverUrl);
48
- if (!options.apiKey ||
49
- !options.integrationId ||
50
- server.username ||
51
- server.password ||
52
- server.search ||
53
- server.hash ||
54
- server.pathname !== "/" ||
55
- !["http:", "https:"].includes(server.protocol))
56
- throw new VeilError("invalid_input");
57
- return {
58
- /** Fetch, clone and push for one authorized repository. */
59
- async readGit(access, request) {
60
- if (access.integrationId !== options.integrationId)
61
- throw new VeilError("denied");
62
- const url = new URL(request.url);
63
- const parts = [access.repository.owner, access.repository.name];
64
- if (!access.locator ||
65
- parts.some((part) => !part || part === "." || part === ".."))
66
- throw new VeilError("invalid_input");
67
- const base = `/${parts.map(encodeURIComponent).join("/")}.git`;
68
- const operation = gitOperation(request, url, base);
69
- if (url.origin !== "https://github.com" ||
70
- url.username ||
71
- url.password ||
72
- url.hash ||
73
- operation === null)
74
- throw new VeilError("denied");
75
- const headers = new Headers({
76
- Authorization: `Bearer ${options.apiKey}`,
77
- "Provider-Config-Key": options.integrationId,
78
- "Connection-Id": access.locator,
79
- });
80
- if (operation.requestType !== null) {
81
- headers.set("Content-Type", operation.requestType);
82
- headers.set("Nango-Proxy-Content-Type", operation.requestType);
83
- }
84
- const protocol = request.headers.get("Git-Protocol");
85
- if (protocol !== null)
86
- headers.set("Nango-Proxy-Git-Protocol", protocol);
87
- let response;
88
- try {
89
- request.signal.throwIfAborted();
90
- response = await fetch(new URL(`/proxy${url.pathname}${url.search}`, server), {
91
- method: request.method,
92
- headers,
93
- body: operation.requestType === null ? null : request.body,
94
- signal: request.signal,
95
- redirect: "manual",
96
- // Native Node fetch requires this for a stream; Workers accepts it.
97
- duplex: "half",
98
- });
99
- }
100
- catch (cause) {
101
- throw new VeilError(request.signal.aborted ? "cancelled" : "unavailable", cause);
102
- }
103
- if (!response.ok) {
104
- await bestEffortCleanup("nango-git-response-body", () => response.body?.cancel() ?? Promise.resolve());
105
- // Reads keep their retried `unavailable`; a push names its refusal.
106
- throw operation.push
107
- ? upstreamRefusal(response)
108
- : new VeilError("unavailable");
109
- }
110
- if (response.headers.get("Content-Type") !== operation.responseType) {
111
- await bestEffortCleanup("nango-git-response-body", () => response.body?.cancel() ?? Promise.resolve());
112
- throw new VeilError("invalid_response");
113
- }
114
- // A protected-branch or hook refusal is GitHub's own report-status
115
- // inside this 200 body; the Computer's git prints it unchanged.
116
- return new Response(response.body, {
117
- status: response.status,
118
- headers: {
119
- "Content-Type": operation.responseType,
120
- "Cache-Control": "no-store",
121
- },
122
- });
123
- },
124
- };
125
- }