@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/nango.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
|
-
import
|
|
3
|
-
import { VeilError, } from "./index.js";
|
|
2
|
+
import { VeilError } from "./index.js";
|
|
4
3
|
const text = Schema.NonEmptyString;
|
|
5
4
|
export const tagsSchema = Schema.Struct({
|
|
6
5
|
veil_team_id: text,
|
|
@@ -28,30 +27,6 @@ export 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
30
|
export function publicUrl(value) {
|
|
56
31
|
let url;
|
|
57
32
|
try {
|
|
@@ -88,213 +63,3 @@ export 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.
|
|
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"
|
|
@@ -91,6 +83,10 @@
|
|
|
91
83
|
"types": "./dist/connections/nango.d.ts",
|
|
92
84
|
"import": "./dist/connections/nango.js"
|
|
93
85
|
},
|
|
86
|
+
"./github-app": {
|
|
87
|
+
"types": "./dist/github-app.d.ts",
|
|
88
|
+
"import": "./dist/github-app.js"
|
|
89
|
+
},
|
|
94
90
|
"./connections/source": {
|
|
95
91
|
"types": "./dist/connections/source.d.ts",
|
|
96
92
|
"import": "./dist/connections/source.js"
|
package/dist/nango-git.d.ts
DELETED
|
@@ -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
|
-
}
|