@fungi.computer/veil 0.5.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/contract.d.ts +40 -1
- package/dist/connections/contract.js +2 -0
- package/dist/connections/core.d.ts +3 -0
- package/dist/connections/core.js +82 -14
- package/dist/connections/nango.js +44 -1
- package/dist/connections/oauth-flow.d.ts +4 -2
- package/dist/connections/oauth-flow.js +13 -5
- package/dist/gatekeeper.d.ts +29 -91
- package/dist/gatekeeper.js +86 -105
- 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 +0 -16
- package/dist/nango.js +1 -236
- package/package.json +5 -9
- package/dist/nango-git.d.ts +0 -19
- package/dist/nango-git.js +0 -125
package/dist/gatekeeper.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
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 CustodyProvider } from "./connections/contract.js";
|
|
5
|
+
import { type GitHubAppCustody } from "./github-app.js";
|
|
4
6
|
import { type ConnectionsCustodian } from "./connections/nango.js";
|
|
5
7
|
import { type WorldSeatCustodian } from "./world-seat.js";
|
|
6
8
|
import type { ModelKeyCustodian } from "./model-keys.js";
|
|
7
9
|
import { createModelKeyEgress, type ModelKeyGrants } from "./model-key-grants.js";
|
|
8
10
|
import { providerFetch, type ProviderEgress, type ProviderKeyBindings, type ProviderRoute } from "./provider-egress.js";
|
|
9
|
-
import * as Schema from "effect/Schema";
|
|
10
11
|
/**
|
|
11
12
|
* The Gatekeeper: the one place the Hub's Nango key, webhook signing key,
|
|
12
13
|
* provider/model-key integration ids, and platform provider keys (Camel, the
|
|
@@ -32,11 +33,17 @@ export type VeilBindings = ProviderKeyBindings & Readonly<{
|
|
|
32
33
|
NANGO_SERVER_URL?: string;
|
|
33
34
|
NANGO_API_KEY?: string;
|
|
34
35
|
NANGO_WEBHOOK_SIGNING_KEY?: string;
|
|
36
|
+
/** The GitHub REST sign-in integration; it joins the connections sign-in list. */
|
|
35
37
|
NANGO_GITHUB_INTEGRATION_ID?: string;
|
|
36
|
-
|
|
38
|
+
/** The Nango `github-app` integration that hands a Computer its installation token. */
|
|
39
|
+
NANGO_GITHUB_APP_INTEGRATION_ID?: string;
|
|
37
40
|
/** API-key integration that holds pasted credentials for Team connections. */
|
|
38
41
|
NANGO_CONNECTIONS_INTEGRATION_ID?: string;
|
|
39
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Nango integrations an owner may sign in through for connections, as a
|
|
44
|
+
* JSON object from integration id to `{ "apiOrigins": ["https://…"] }`:
|
|
45
|
+
* the only origins that integration's credential is sent to.
|
|
46
|
+
*/
|
|
40
47
|
NANGO_CONNECTIONS_OAUTH_INTEGRATIONS?: string;
|
|
41
48
|
/** Public Connect UI base that Nango's issued links must stay under. */
|
|
42
49
|
NANGO_CONNECT_BASE_URL?: string;
|
|
@@ -62,17 +69,20 @@ export type VeilBindings = ProviderKeyBindings & Readonly<{
|
|
|
62
69
|
* bindings keeps sealed custody and still connects OAuth MCP and OpenAPI.
|
|
63
70
|
*/
|
|
64
71
|
export declare function configuredConnectionsCustody(env: VeilBindings): ConnectionsCustodian | undefined;
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
/**
|
|
73
|
+
* The Nango integrations an owner may sign in through for connections, each
|
|
74
|
+
* pinned to the origins its credential may reach. Absent or malformed means
|
|
75
|
+
* none: a sign-in through an undeclared integration is refused, never guessed.
|
|
76
|
+
*/
|
|
77
|
+
export declare function configuredConnectionsCustodyProviders(env: VeilBindings): Readonly<Record<string, CustodyProvider>>;
|
|
78
|
+
/**
|
|
79
|
+
* The Nango `github-app` integration through which a Computer's git gets its
|
|
80
|
+
* installation token. It is its own thing: never in the generic list, and
|
|
81
|
+
* never the same integration as GitHub REST.
|
|
82
|
+
*/
|
|
83
|
+
export declare function configuredGitHubAppIntegration(env: VeilBindings): string | undefined;
|
|
84
|
+
/** Nango's GitHub App custody for Computers; absent when any binding is. */
|
|
85
|
+
export declare function configuredGitHubAppCustody(env: VeilBindings): GitHubAppCustody | undefined;
|
|
76
86
|
/**
|
|
77
87
|
* `veil.fetch` for platform provider keys. The Hub names a route and sends a
|
|
78
88
|
* credential-free request; Veil checks the destination and attaches the key.
|
|
@@ -122,85 +132,13 @@ export declare function configuredModelKeyEgress(env: VeilBindings, options: Rea
|
|
|
122
132
|
provider: import("./model-keys.js").ModelKeyProvider;
|
|
123
133
|
}>, request: Request): Promise<Response>;
|
|
124
134
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
principalId: string;
|
|
130
|
-
integrationId: string;
|
|
131
|
-
}>, signal: AbortSignal) => Promise<Readonly<{
|
|
132
|
-
token: string;
|
|
133
|
-
connectUrl: string;
|
|
134
|
-
expiresAt: number;
|
|
135
|
-
}>>;
|
|
136
|
-
verifyCompletion: (rawBody: string, signature: string) => Promise<import("./index.js").VerifiedCompletion>;
|
|
137
|
-
verifyWebhook: (rawBody: string, signature: string) => Promise<import("./index.js").VerifiedWebhook>;
|
|
138
|
-
findCompletion: (attempt: import("./index.js").CustodyCorrelation, signal: AbortSignal) => Promise<Schema.Schema.Type<Schema.Union<readonly [Schema.Struct<{
|
|
139
|
-
readonly status: Schema.Literal<"unresolved">;
|
|
140
|
-
}>, Schema.Struct<{
|
|
141
|
-
readonly status: Schema.Literal<"ambiguous">;
|
|
142
|
-
}>, Schema.Struct<{
|
|
143
|
-
readonly status: Schema.Literal<"verified">;
|
|
144
|
-
readonly completion: Schema.Struct<{
|
|
145
|
-
readonly teamId: Schema.NonEmptyString;
|
|
146
|
-
readonly attemptId: Schema.NonEmptyString;
|
|
147
|
-
readonly principalId: Schema.NonEmptyString;
|
|
148
|
-
readonly integrationId: Schema.NonEmptyString;
|
|
149
|
-
readonly locator: Schema.NonEmptyString;
|
|
150
|
-
}>;
|
|
151
|
-
}>]>>>;
|
|
152
|
-
deleteConnection: (locator: string, signal: AbortSignal) => Promise<"deleted" | "absent">;
|
|
153
|
-
readRepository: (access: import("./index.js").RepositoryAccess, signal: AbortSignal) => Promise<string>;
|
|
154
|
-
}>;
|
|
155
|
-
/** Git Connect/cleanup and final egress use the same Git-profile integration.
|
|
156
|
-
* No metadata Connection locator is translated into this profile. Veil confines
|
|
157
|
-
* the issued Connect document to the configured public Connect URLs. */
|
|
158
|
-
export declare function configuredComputerGitCustodian(env: VeilBindings): {
|
|
159
|
-
integrationId: string;
|
|
160
|
-
openSession: (input: Readonly<{
|
|
161
|
-
teamId: string;
|
|
162
|
-
attemptId: string;
|
|
163
|
-
principalId: string;
|
|
164
|
-
integrationId: string;
|
|
165
|
-
}>, signal: AbortSignal) => Promise<Readonly<{
|
|
166
|
-
token: string;
|
|
167
|
-
connectUrl: string;
|
|
168
|
-
expiresAt: number;
|
|
169
|
-
}>>;
|
|
170
|
-
verifyCompletion: (rawBody: string, signature: string) => Promise<import("./index.js").VerifiedCompletion>;
|
|
171
|
-
verifyWebhook: (rawBody: string, signature: string) => Promise<import("./index.js").VerifiedWebhook>;
|
|
172
|
-
findCompletion: (attempt: import("./index.js").CustodyCorrelation, signal: AbortSignal) => Promise<Schema.Schema.Type<Schema.Union<readonly [Schema.Struct<{
|
|
173
|
-
readonly status: Schema.Literal<"unresolved">;
|
|
174
|
-
}>, Schema.Struct<{
|
|
175
|
-
readonly status: Schema.Literal<"ambiguous">;
|
|
176
|
-
}>, Schema.Struct<{
|
|
177
|
-
readonly status: Schema.Literal<"verified">;
|
|
178
|
-
readonly completion: Schema.Struct<{
|
|
179
|
-
readonly teamId: Schema.NonEmptyString;
|
|
180
|
-
readonly attemptId: Schema.NonEmptyString;
|
|
181
|
-
readonly principalId: Schema.NonEmptyString;
|
|
182
|
-
readonly integrationId: Schema.NonEmptyString;
|
|
183
|
-
readonly locator: Schema.NonEmptyString;
|
|
184
|
-
}>;
|
|
185
|
-
}>]>>>;
|
|
186
|
-
deleteConnection: (locator: string, signal: AbortSignal) => Promise<"deleted" | "absent">;
|
|
187
|
-
readGit: (access: import("./nango-git.js").GitRepositoryAccess, request: Request) => Promise<Response>;
|
|
188
|
-
};
|
|
189
|
-
/** The decoded key selects one of the two configured profiles only. The
|
|
190
|
-
* selected maintained custodian still verifies the original signed body. */
|
|
135
|
+
/** The decoded key must name an integration this host runs a sign-in through;
|
|
136
|
+
* the maintained custodian then verifies the original signed body. The GitHub
|
|
137
|
+
* App integration settles only a Computer's git, a generic one only a
|
|
138
|
+
* connection: neither can settle the other's attempt. */
|
|
191
139
|
export declare function verifyVeilWebhook(env: VeilBindings, rawBody: string, signature: string): Promise<{
|
|
192
|
-
profile: "connections";
|
|
140
|
+
profile: "connections" | "computer-git";
|
|
193
141
|
ownerId: string;
|
|
194
|
-
} | {
|
|
195
|
-
operation: "creation" | "refresh_failed";
|
|
196
|
-
completion: import("./index.js").VerifiedCompletion;
|
|
197
|
-
profile: "computer-git";
|
|
198
|
-
ownerId?: never;
|
|
199
|
-
} | {
|
|
200
|
-
operation: "creation" | "refresh_failed";
|
|
201
|
-
completion: import("./index.js").VerifiedCompletion;
|
|
202
|
-
profile: "agent-metadata";
|
|
203
|
-
ownerId?: never;
|
|
204
142
|
}>;
|
|
205
143
|
export type OpenApiHostOptions = Readonly<{
|
|
206
144
|
connect: PublicConnect;
|
package/dist/gatekeeper.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { createOpenApiDoor, } from "./openapi.js";
|
|
2
2
|
import { createOpenApiEffects } from "./openapi-effect.js";
|
|
3
3
|
import { createDnsOverHttpsResolver } from "./model-key-network.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { ConnectionsError, } from "./connections/contract.js";
|
|
5
|
+
import { createGitHubAppCustody } from "./github-app.js";
|
|
6
6
|
import { createNangoCustody, } from "./connections/nango.js";
|
|
7
7
|
import { createWebhookTriggerSecrets } from "./webhook-trigger-secrets.js";
|
|
8
|
-
import { createNangoGitCustodian } from "./nango-git.js";
|
|
9
8
|
import { createNangoModelKeyCustodian } from "./nango-model-keys.js";
|
|
10
9
|
import { createNangoWorldSeatCustodian } from "./nango-world-seat.js";
|
|
11
10
|
import { worldSeatEndpoint } from "./world-seat.js";
|
|
@@ -54,25 +53,72 @@ export function configuredConnectionsCustody(env) {
|
|
|
54
53
|
throw new VeilError("unavailable");
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
/** GitHub's REST API: the only place the GitHub REST integration's token may go. */
|
|
57
|
+
const GITHUB_REST_ORIGIN = "https://api.github.com";
|
|
58
|
+
const custodyProvidersJson = Schema.fromJsonString(Schema.Record(Schema.String.pipe(Schema.check(Schema.isPattern(/^[A-Za-z0-9_.:-]{1,128}$/u))), Schema.Struct({
|
|
59
|
+
apiOrigins: Schema.Array(Schema.String).pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(16))),
|
|
60
|
+
})));
|
|
61
|
+
const isHttpsOrigin = (value) => URL.canParse(value) &&
|
|
62
|
+
new URL(value).protocol === "https:" &&
|
|
63
|
+
new URL(value).origin === value;
|
|
64
|
+
/**
|
|
65
|
+
* The Nango integrations an owner may sign in through for connections, each
|
|
66
|
+
* pinned to the origins its credential may reach. Absent or malformed means
|
|
67
|
+
* none: a sign-in through an undeclared integration is refused, never guessed.
|
|
68
|
+
*/
|
|
69
|
+
export function configuredConnectionsCustodyProviders(env) {
|
|
70
|
+
const raw = env.NANGO_CONNECTIONS_OAUTH_INTEGRATIONS;
|
|
71
|
+
const decoded = raw === undefined || raw.trim() === ""
|
|
72
|
+
? undefined
|
|
73
|
+
: Schema.decodeUnknownExit(custodyProvidersJson)(raw);
|
|
74
|
+
const operator = decoded === undefined || Exit.isFailure(decoded) ? {} : decoded.value;
|
|
75
|
+
const declared = {};
|
|
76
|
+
for (const [id, provider] of Object.entries(operator))
|
|
77
|
+
if (provider.apiOrigins.every(isHttpsOrigin))
|
|
78
|
+
declared[id] = provider;
|
|
79
|
+
// GitHub REST is pinned to its API host whatever the operator declared.
|
|
80
|
+
const rest = env.NANGO_GITHUB_INTEGRATION_ID;
|
|
81
|
+
if (rest !== undefined && rest !== "")
|
|
82
|
+
declared[rest] = { apiOrigins: [GITHUB_REST_ORIGIN] };
|
|
83
|
+
const app = configuredGitHubAppIntegration(env);
|
|
84
|
+
return Object.fromEntries(Object.entries(declared).filter(([id]) => id !== app));
|
|
63
85
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The Nango `github-app` integration through which a Computer's git gets its
|
|
88
|
+
* installation token. It is its own thing: never in the generic list, and
|
|
89
|
+
* never the same integration as GitHub REST.
|
|
90
|
+
*/
|
|
91
|
+
export function configuredGitHubAppIntegration(env) {
|
|
92
|
+
const id = env.NANGO_GITHUB_APP_INTEGRATION_ID;
|
|
93
|
+
if (id === undefined || id === "" || id === env.NANGO_GITHUB_INTEGRATION_ID)
|
|
94
|
+
return undefined;
|
|
95
|
+
return id;
|
|
96
|
+
}
|
|
97
|
+
/** Nango's GitHub App custody for Computers; absent when any binding is. */
|
|
98
|
+
export function configuredGitHubAppCustody(env) {
|
|
99
|
+
const appIntegrationId = configuredGitHubAppIntegration(env);
|
|
100
|
+
if (appIntegrationId === undefined ||
|
|
101
|
+
!env.NANGO_SERVER_URL ||
|
|
102
|
+
!env.NANGO_API_KEY ||
|
|
103
|
+
!env.NANGO_WEBHOOK_SIGNING_KEY ||
|
|
104
|
+
!env.NANGO_CONNECT_BASE_URL ||
|
|
105
|
+
!env.NANGO_PUBLIC_API_URL ||
|
|
106
|
+
!env.NANGO_CONNECTIONS_INTEGRATION_ID)
|
|
107
|
+
return undefined;
|
|
108
|
+
try {
|
|
109
|
+
return createGitHubAppCustody({
|
|
110
|
+
serverUrl: env.NANGO_SERVER_URL,
|
|
111
|
+
apiKey: env.NANGO_API_KEY,
|
|
112
|
+
webhookSigningKey: env.NANGO_WEBHOOK_SIGNING_KEY,
|
|
113
|
+
staticIntegrationId: env.NANGO_CONNECTIONS_INTEGRATION_ID,
|
|
114
|
+
connectBaseUrl: env.NANGO_CONNECT_BASE_URL,
|
|
115
|
+
publicApiUrl: env.NANGO_PUBLIC_API_URL,
|
|
116
|
+
appIntegrationId,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new VeilError("unavailable");
|
|
121
|
+
}
|
|
76
122
|
}
|
|
77
123
|
/**
|
|
78
124
|
* `veil.fetch` for platform provider keys. The Hub names a route and sends a
|
|
@@ -151,75 +197,32 @@ export function configuredModelKeyEgress(env, options) {
|
|
|
151
197
|
ownerIsMember: options.ownerIsMember,
|
|
152
198
|
});
|
|
153
199
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return configuredCustodian(env, state.metadataIntegrationId);
|
|
159
|
-
}
|
|
160
|
-
/** Git Connect/cleanup and final egress use the same Git-profile integration.
|
|
161
|
-
* No metadata Connection locator is translated into this profile. Veil confines
|
|
162
|
-
* the issued Connect document to the configured public Connect URLs. */
|
|
163
|
-
export function configuredComputerGitCustodian(env) {
|
|
164
|
-
const state = configuredGitHubState(env);
|
|
165
|
-
if (state.github === "off")
|
|
166
|
-
throw new VeilError("github_not_configured");
|
|
167
|
-
const custody = configuredCustodian(env, state.gitIntegrationId);
|
|
168
|
-
if (!env.NANGO_SERVER_URL || !env.NANGO_API_KEY)
|
|
169
|
-
throw new VeilError("unavailable");
|
|
170
|
-
try {
|
|
171
|
-
const git = createNangoGitCustodian({
|
|
172
|
-
serverUrl: env.NANGO_SERVER_URL,
|
|
173
|
-
apiKey: env.NANGO_API_KEY,
|
|
174
|
-
integrationId: state.gitIntegrationId,
|
|
175
|
-
});
|
|
176
|
-
return {
|
|
177
|
-
integrationId: state.gitIntegrationId,
|
|
178
|
-
openSession: custody.openSession,
|
|
179
|
-
verifyCompletion: custody.verifyCompletion,
|
|
180
|
-
verifyWebhook: custody.verifyWebhook,
|
|
181
|
-
findCompletion: custody.findCompletion,
|
|
182
|
-
deleteConnection: custody.deleteConnection,
|
|
183
|
-
readGit: git.readGit.bind(git),
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
throw new VeilError("unavailable");
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
/** The decoded key selects one of the two configured profiles only. The
|
|
191
|
-
* selected maintained custodian still verifies the original signed body. */
|
|
200
|
+
/** The decoded key must name an integration this host runs a sign-in through;
|
|
201
|
+
* the maintained custodian then verifies the original signed body. The GitHub
|
|
202
|
+
* App integration settles only a Computer's git, a generic one only a
|
|
203
|
+
* connection: neither can settle the other's attempt. */
|
|
192
204
|
export async function verifyVeilWebhook(env, rawBody, signature) {
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
205
|
+
const generic = Object.keys(configuredConnectionsCustodyProviders(env));
|
|
206
|
+
const app = configuredGitHubAppIntegration(env);
|
|
207
|
+
if (generic.length === 0 && app === undefined)
|
|
196
208
|
throw new VeilError("github_not_configured");
|
|
197
209
|
const decoded = Schema.decodeUnknownExit(Schema.fromJsonString(Schema.Struct({
|
|
198
210
|
providerConfigKey: Schema.NonEmptyString,
|
|
199
211
|
})))(rawBody);
|
|
200
212
|
if (Exit.isFailure(decoded))
|
|
201
213
|
throw new VeilError("invalid_completion");
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
throw new VeilError("
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
if (decoded.value.providerConfigKey === state.metadataIntegrationId) {
|
|
217
|
-
return {
|
|
218
|
-
profile: "agent-metadata",
|
|
219
|
-
...(await configuredVeilCustodian(env).verifyWebhook(rawBody, signature)),
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
throw new VeilError("invalid_completion");
|
|
214
|
+
const key = decoded.value.providerConfigKey;
|
|
215
|
+
const profile = generic.includes(key)
|
|
216
|
+
? "connections"
|
|
217
|
+
: key === app
|
|
218
|
+
? "computer-git"
|
|
219
|
+
: undefined;
|
|
220
|
+
if (profile === undefined)
|
|
221
|
+
throw new VeilError("invalid_completion");
|
|
222
|
+
return {
|
|
223
|
+
profile,
|
|
224
|
+
ownerId: await verifiedConnectionOwner(env, rawBody, signature),
|
|
225
|
+
};
|
|
223
226
|
}
|
|
224
227
|
/** The signed sign-in names the Team that began it; only a verified body is believed. */
|
|
225
228
|
async function verifiedConnectionOwner(env, rawBody, signature) {
|
|
@@ -239,28 +242,6 @@ async function verifiedConnectionOwner(env, rawBody, signature) {
|
|
|
239
242
|
throw cause;
|
|
240
243
|
}
|
|
241
244
|
}
|
|
242
|
-
function configuredCustodian(env, integrationId) {
|
|
243
|
-
if (!env.NANGO_SERVER_URL ||
|
|
244
|
-
!env.NANGO_API_KEY ||
|
|
245
|
-
!env.NANGO_WEBHOOK_SIGNING_KEY ||
|
|
246
|
-
!env.NANGO_CONNECT_BASE_URL ||
|
|
247
|
-
!env.NANGO_PUBLIC_API_URL ||
|
|
248
|
-
!integrationId)
|
|
249
|
-
throw new VeilError("unavailable");
|
|
250
|
-
try {
|
|
251
|
-
return createNangoCustodian({
|
|
252
|
-
serverUrl: env.NANGO_SERVER_URL,
|
|
253
|
-
apiKey: env.NANGO_API_KEY,
|
|
254
|
-
webhookSigningKey: env.NANGO_WEBHOOK_SIGNING_KEY,
|
|
255
|
-
integrationId,
|
|
256
|
-
connectBaseUrl: env.NANGO_CONNECT_BASE_URL,
|
|
257
|
-
publicApiUrl: env.NANGO_PUBLIC_API_URL,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
throw new VeilError("unavailable");
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
245
|
/** The keyring that seals OpenAPI credentials; only Gatekeeper reads the master key. */
|
|
265
246
|
export function openApiCredentialKeyring(env, previous) {
|
|
266
247
|
const key = env.OPENAPI_CREDENTIAL_MASTER_KEY;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type CustodyLookup } from "./connections/contract.js";
|
|
2
|
+
import { type NangoCustodyOptions } from "./connections/nango.js";
|
|
3
|
+
/** A completion Nango signed for the GitHub App sign-in, once verified. */
|
|
4
|
+
export type GitHubAppCompletion = Readonly<{
|
|
5
|
+
teamId: string;
|
|
6
|
+
state: string;
|
|
7
|
+
principalId: string;
|
|
8
|
+
locator: string;
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* Nango's `github-app` integration as the one source of a Computer's GitHub
|
|
12
|
+
* token. Nango holds the App's credentials and returns an installation token
|
|
13
|
+
* limited to the repositories the person picked at install. The token is read
|
|
14
|
+
* here, in memory, to be placed on a request to `github.com`; it is never
|
|
15
|
+
* stored, listed or sent anywhere else.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createGitHubAppCustody(options: NangoCustodyOptions & Readonly<{
|
|
18
|
+
appIntegrationId: string;
|
|
19
|
+
}>): {
|
|
20
|
+
integrationId: string;
|
|
21
|
+
/** The page that sends the member to GitHub's installation consent. */
|
|
22
|
+
connect: (ownerId: string, session: {
|
|
23
|
+
state: string;
|
|
24
|
+
actor: string;
|
|
25
|
+
}) => Promise<Readonly<{
|
|
26
|
+
connectUrl: string;
|
|
27
|
+
expiresAt: number;
|
|
28
|
+
}>>;
|
|
29
|
+
/** The signed completion, and only one for this App integration. */
|
|
30
|
+
verifyCompletion: (rawBody: string, signature: string) => Promise<GitHubAppCompletion>;
|
|
31
|
+
/** Ask Nango whether a sign-in finished whose signed completion was lost. */
|
|
32
|
+
find: (ownerId: string, session: {
|
|
33
|
+
state: string;
|
|
34
|
+
actor: string;
|
|
35
|
+
}) => Promise<CustodyLookup>;
|
|
36
|
+
remove: (ownerId: string, locator: string) => Promise<"absent" | "deleted">;
|
|
37
|
+
/**
|
|
38
|
+
* A current installation token. `fresh` asks Nango to refresh it first.
|
|
39
|
+
* A connection Nango no longer holds, or whose App access GitHub ended,
|
|
40
|
+
* is `reconnect_required`; an outage is `unavailable`.
|
|
41
|
+
*/
|
|
42
|
+
token: (locator: string, fresh: boolean, signal: AbortSignal) => Promise<string>;
|
|
43
|
+
};
|
|
44
|
+
export type GitHubAppCustody = ReturnType<typeof createGitHubAppCustody>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { ConnectionsError, } from "./connections/contract.js";
|
|
4
|
+
import { createNangoCustody, } from "./connections/nango.js";
|
|
5
|
+
import { createNangoCustodyHttp } from "./nango-custody-http.js";
|
|
6
|
+
const tokenResponse = Schema.Struct({
|
|
7
|
+
credentials: Schema.Struct({
|
|
8
|
+
access_token: Schema.NonEmptyString,
|
|
9
|
+
expires_at: Schema.optional(Schema.String),
|
|
10
|
+
}),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Nango's `github-app` integration as the one source of a Computer's GitHub
|
|
14
|
+
* token. Nango holds the App's credentials and returns an installation token
|
|
15
|
+
* limited to the repositories the person picked at install. The token is read
|
|
16
|
+
* here, in memory, to be placed on a request to `github.com`; it is never
|
|
17
|
+
* stored, listed or sent anywhere else.
|
|
18
|
+
*/
|
|
19
|
+
export function createGitHubAppCustody(options) {
|
|
20
|
+
const nango = createNangoCustody(options);
|
|
21
|
+
const http = createNangoCustodyHttp(options);
|
|
22
|
+
const address = (ownerId) => ({
|
|
23
|
+
ownerId,
|
|
24
|
+
connectionId: "github-app",
|
|
25
|
+
credential: { kind: "oauth", provider: options.appIntegrationId },
|
|
26
|
+
});
|
|
27
|
+
const run = (effect) => Effect.runPromise(effect);
|
|
28
|
+
const connect = nango.connect;
|
|
29
|
+
if (connect === undefined)
|
|
30
|
+
throw new ConnectionsError("unsupported");
|
|
31
|
+
const find = nango.find;
|
|
32
|
+
if (find === undefined)
|
|
33
|
+
throw new ConnectionsError("unsupported");
|
|
34
|
+
return {
|
|
35
|
+
integrationId: options.appIntegrationId,
|
|
36
|
+
/** The page that sends the member to GitHub's installation consent. */
|
|
37
|
+
connect: (ownerId, session) => run(connect(address(ownerId), {
|
|
38
|
+
...session,
|
|
39
|
+
integrationId: options.appIntegrationId,
|
|
40
|
+
})),
|
|
41
|
+
/** The signed completion, and only one for this App integration. */
|
|
42
|
+
verifyCompletion: async (rawBody, signature) => {
|
|
43
|
+
const verified = await nango.verifyCompletion(rawBody, signature);
|
|
44
|
+
if (verified.integrationId !== options.appIntegrationId)
|
|
45
|
+
throw new ConnectionsError("denied");
|
|
46
|
+
return {
|
|
47
|
+
teamId: verified.ownerId,
|
|
48
|
+
state: verified.state,
|
|
49
|
+
principalId: verified.principalId,
|
|
50
|
+
locator: verified.locator,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
/** Ask Nango whether a sign-in finished whose signed completion was lost. */
|
|
54
|
+
find: (ownerId, session) => run(find(address(ownerId), {
|
|
55
|
+
...session,
|
|
56
|
+
integrationId: options.appIntegrationId,
|
|
57
|
+
})),
|
|
58
|
+
remove: (ownerId, locator) => run(nango.remove(address(ownerId), locator)),
|
|
59
|
+
/**
|
|
60
|
+
* A current installation token. `fresh` asks Nango to refresh it first.
|
|
61
|
+
* A connection Nango no longer holds, or whose App access GitHub ended,
|
|
62
|
+
* is `reconnect_required`; an outage is `unavailable`.
|
|
63
|
+
*/
|
|
64
|
+
token: async (locator, fresh, signal) => {
|
|
65
|
+
const query = new URLSearchParams({
|
|
66
|
+
provider_config_key: options.appIntegrationId,
|
|
67
|
+
});
|
|
68
|
+
if (fresh)
|
|
69
|
+
query.set("force_refresh", "true");
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
response = await http.call(`/connection/${encodeURIComponent(locator)}?${query.toString()}`, signal, {});
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
throw new ConnectionsError("unavailable", { cause });
|
|
76
|
+
}
|
|
77
|
+
if ([400, 401, 403, 404].includes(response.status)) {
|
|
78
|
+
await response.body?.cancel();
|
|
79
|
+
throw new ConnectionsError("reconnect_required");
|
|
80
|
+
}
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
await response.body?.cancel();
|
|
83
|
+
throw new ConnectionsError("unavailable");
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return Schema.decodeUnknownSync(Schema.fromJsonString(tokenResponse))(await response.text()).credentials.access_token;
|
|
87
|
+
}
|
|
88
|
+
catch (cause) {
|
|
89
|
+
throw new ConnectionsError("invalid_response", { cause });
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|