@fungi.computer/veil 0.5.0 → 0.6.1

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.
@@ -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
- NANGO_GITHUB_GIT_INTEGRATION_ID?: string;
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
- /** Comma-separated Nango integrations an owner may sign in through for connections. */
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
- /** The Nango integrations an owner may sign in through for connections. */
66
- export declare function configuredConnectionsOAuthIntegrations(env: VeilBindings): readonly string[];
67
- export declare function configuredGitHubState(env: VeilBindings): {
68
- github: "off";
69
- metadataIntegrationId?: never;
70
- gitIntegrationId?: never;
71
- } | {
72
- github: "on";
73
- metadataIntegrationId: string;
74
- gitIntegrationId: string;
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
- export declare function configuredVeilCustodian(env: VeilBindings): Readonly<{
126
- openSession: (input: Readonly<{
127
- teamId: string;
128
- attemptId: string;
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;
@@ -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 { createNangoCustodian } from "./nango.js";
5
- import { ConnectionsError } from "./connections/contract.js";
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
- /** 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);
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
- export function configuredGitHubState(env) {
65
- const metadataIntegrationId = env.NANGO_GITHUB_INTEGRATION_ID;
66
- const gitIntegrationId = env.NANGO_GITHUB_GIT_INTEGRATION_ID;
67
- if (!metadataIntegrationId ||
68
- !gitIntegrationId ||
69
- metadataIntegrationId === gitIntegrationId)
70
- return { github: "off" };
71
- return {
72
- github: "on",
73
- metadataIntegrationId,
74
- gitIntegrationId,
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
- export function configuredVeilCustodian(env) {
155
- const state = configuredGitHubState(env);
156
- if (state.github === "off")
157
- throw new VeilError("github_not_configured");
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 state = configuredGitHubState(env);
194
- const connectionIntegrations = configuredConnectionsOAuthIntegrations(env);
195
- if (state.github === "off" && connectionIntegrations.length === 0)
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
- // 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");
210
- if (decoded.value.providerConfigKey === state.gitIntegrationId) {
211
- return {
212
- profile: "computer-git",
213
- ...(await configuredComputerGitCustodian(env).verifyWebhook(rawBody, signature)),
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
+ }