@flow-industries/id 0.26.5 → 0.27.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/contracts/v1/sdk-exports.json +85 -0
- package/dist/sdk/client/github-connector.d.ts +19 -0
- package/dist/sdk/client/github-connector.js +128 -0
- package/dist/sdk/client/index.d.ts +3 -0
- package/dist/sdk/client/index.js +2 -0
- package/dist/sdk/client/oauth.d.ts +9 -0
- package/dist/sdk/client/oauth.js +68 -0
- package/dist/sdk/github/operations.d.ts +909 -0
- package/dist/sdk/github/operations.js +543 -0
- package/dist/sdk/github/scopes.d.ts +31 -0
- package/dist/sdk/github/scopes.js +70 -0
- package/dist/sdk/oauth/stores.d.ts +3 -0
- package/dist/sdk/oauth/stores.js +4 -0
- package/dist/sdk/profile.css +1 -1
- package/dist/sdk/types/auth.d.ts +3 -0
- package/dist/sdk/types/github-connector.d.ts +88 -0
- package/dist/sdk/types/github-connector.js +0 -0
- package/dist/sdk/types/index.d.ts +3 -1
- package/dist/sdk/types/landing.d.ts +20 -0
- package/dist/sdk/types/oauth.d.ts +64 -0
- package/dist/sdk/types/oauth.js +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The only host the connector ever sends a GitHub token to. */
|
|
3
|
+
export const GITHUB_API_ORIGIN = "https://api.github.com";
|
|
4
|
+
export const GITHUB_REQUEST_MAX_BYTES = 1_572_864;
|
|
5
|
+
export const GITHUB_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
6
|
+
const FILE_CONTENT_MAX_BASE64 = 1_400_000;
|
|
7
|
+
const positiveInt = z.number().int().min(1).max(2_147_483_647);
|
|
8
|
+
const login = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/);
|
|
9
|
+
const repoName = z
|
|
10
|
+
.string()
|
|
11
|
+
.regex(/^[A-Za-z0-9._-]{1,100}$/)
|
|
12
|
+
.refine((name) => name !== "." && name !== "..");
|
|
13
|
+
const perPage = z.number().int().min(1).max(100);
|
|
14
|
+
const page = z.number().int().min(1).max(1000);
|
|
15
|
+
const direction = z.enum(["asc", "desc"]);
|
|
16
|
+
const since = z.iso.datetime({ offset: true });
|
|
17
|
+
const labelName = z.string().min(1).max(100);
|
|
18
|
+
const labelFilter = labelName.regex(/^[^,]+$/);
|
|
19
|
+
const markdown = z.string().max(65_536);
|
|
20
|
+
const blobSha = z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/);
|
|
21
|
+
const gitRef = z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(255)
|
|
25
|
+
.regex(/^[A-Za-z0-9._/-]+$/)
|
|
26
|
+
.refine((ref) => !/^[-/]|[/.]$|\.\.|\/\/|\.lock$/.test(ref) &&
|
|
27
|
+
ref.split("/").every((part) => !part.startsWith(".")));
|
|
28
|
+
const headFilter = z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9-]{0,38}:[A-Za-z0-9._/-]{1,255}$/);
|
|
31
|
+
function isSafePathSegment(segment) {
|
|
32
|
+
if (!segment || segment === "." || segment === ".." || segment.length > 255)
|
|
33
|
+
return false;
|
|
34
|
+
for (const char of segment) {
|
|
35
|
+
const code = char.codePointAt(0) ?? 0;
|
|
36
|
+
if (code < 0x20 || code === 0x7f || char === "\\")
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const contentPath = z
|
|
42
|
+
.string()
|
|
43
|
+
.min(1)
|
|
44
|
+
.max(1024)
|
|
45
|
+
.refine((path) => path.split("/").every(isSafePathSegment));
|
|
46
|
+
const base64 = z
|
|
47
|
+
.string()
|
|
48
|
+
.max(FILE_CONTENT_MAX_BASE64)
|
|
49
|
+
.regex(/^[A-Za-z0-9+/]*={0,2}$/)
|
|
50
|
+
.refine((value) => value.length % 4 === 0);
|
|
51
|
+
const repository = { owner: login, repo: repoName };
|
|
52
|
+
const paging = { per_page: perPage.optional(), page: page.optional() };
|
|
53
|
+
const text = (max) => z.string().max(max);
|
|
54
|
+
const url = text(2048);
|
|
55
|
+
const timestamp = text(64);
|
|
56
|
+
const githubAccount = z.object({
|
|
57
|
+
login: text(100),
|
|
58
|
+
id: z.number().int(),
|
|
59
|
+
avatar_url: url.optional(),
|
|
60
|
+
html_url: url.optional(),
|
|
61
|
+
type: text(32).optional(),
|
|
62
|
+
});
|
|
63
|
+
const viewer = githubAccount.extend({
|
|
64
|
+
name: text(512).nullish(),
|
|
65
|
+
email: text(512).nullish(),
|
|
66
|
+
bio: text(1024).nullish(),
|
|
67
|
+
company: text(512).nullish(),
|
|
68
|
+
blog: url.nullish(),
|
|
69
|
+
location: text(512).nullish(),
|
|
70
|
+
public_repos: z.number().int().optional(),
|
|
71
|
+
followers: z.number().int().optional(),
|
|
72
|
+
following: z.number().int().optional(),
|
|
73
|
+
created_at: timestamp.optional(),
|
|
74
|
+
});
|
|
75
|
+
const repositorySummary = z.object({
|
|
76
|
+
id: z.number().int(),
|
|
77
|
+
name: text(100),
|
|
78
|
+
full_name: text(256),
|
|
79
|
+
owner: githubAccount,
|
|
80
|
+
private: z.boolean(),
|
|
81
|
+
visibility: text(32).optional(),
|
|
82
|
+
html_url: url,
|
|
83
|
+
description: text(4096).nullish(),
|
|
84
|
+
fork: z.boolean().optional(),
|
|
85
|
+
archived: z.boolean().optional(),
|
|
86
|
+
default_branch: text(255).optional(),
|
|
87
|
+
language: text(128).nullish(),
|
|
88
|
+
stargazers_count: z.number().int().optional(),
|
|
89
|
+
forks_count: z.number().int().optional(),
|
|
90
|
+
open_issues_count: z.number().int().optional(),
|
|
91
|
+
pushed_at: timestamp.nullish(),
|
|
92
|
+
created_at: timestamp.nullish(),
|
|
93
|
+
updated_at: timestamp.nullish(),
|
|
94
|
+
permissions: z
|
|
95
|
+
.object({
|
|
96
|
+
admin: z.boolean(),
|
|
97
|
+
maintain: z.boolean().optional(),
|
|
98
|
+
push: z.boolean(),
|
|
99
|
+
triage: z.boolean().optional(),
|
|
100
|
+
pull: z.boolean(),
|
|
101
|
+
})
|
|
102
|
+
.optional(),
|
|
103
|
+
});
|
|
104
|
+
const label = z.object({
|
|
105
|
+
id: z.number().int().optional(),
|
|
106
|
+
name: text(256),
|
|
107
|
+
color: text(16).nullish(),
|
|
108
|
+
description: text(1024).nullish(),
|
|
109
|
+
});
|
|
110
|
+
const issue = z.object({
|
|
111
|
+
id: z.number().int(),
|
|
112
|
+
number: z.number().int(),
|
|
113
|
+
title: text(1024),
|
|
114
|
+
body: text(262_144).nullish(),
|
|
115
|
+
state: text(32),
|
|
116
|
+
state_reason: text(32).nullish(),
|
|
117
|
+
locked: z.boolean().optional(),
|
|
118
|
+
user: githubAccount.nullable(),
|
|
119
|
+
labels: z.array(z.union([text(256), label])).max(100),
|
|
120
|
+
assignees: z.array(githubAccount).max(100).nullish(),
|
|
121
|
+
milestone: z
|
|
122
|
+
.object({ number: z.number().int(), title: text(1024) })
|
|
123
|
+
.nullish(),
|
|
124
|
+
comments: z.number().int().optional(),
|
|
125
|
+
html_url: url,
|
|
126
|
+
created_at: timestamp,
|
|
127
|
+
updated_at: timestamp,
|
|
128
|
+
closed_at: timestamp.nullish(),
|
|
129
|
+
author_association: text(64).optional(),
|
|
130
|
+
pull_request: z
|
|
131
|
+
.object({ html_url: url.nullish(), merged_at: timestamp.nullish() })
|
|
132
|
+
.optional(),
|
|
133
|
+
});
|
|
134
|
+
const comment = z.object({
|
|
135
|
+
id: z.number().int(),
|
|
136
|
+
body: text(262_144).nullish(),
|
|
137
|
+
user: githubAccount.nullable(),
|
|
138
|
+
html_url: url,
|
|
139
|
+
created_at: timestamp,
|
|
140
|
+
updated_at: timestamp,
|
|
141
|
+
author_association: text(64).optional(),
|
|
142
|
+
});
|
|
143
|
+
const branchRef = z.object({
|
|
144
|
+
label: text(512).optional(),
|
|
145
|
+
ref: text(255),
|
|
146
|
+
sha: text(64),
|
|
147
|
+
repo: z.object({ full_name: text(256) }).nullish(),
|
|
148
|
+
});
|
|
149
|
+
const pullRequest = z.object({
|
|
150
|
+
id: z.number().int(),
|
|
151
|
+
number: z.number().int(),
|
|
152
|
+
title: text(1024),
|
|
153
|
+
body: text(262_144).nullish(),
|
|
154
|
+
state: text(32),
|
|
155
|
+
draft: z.boolean().optional(),
|
|
156
|
+
locked: z.boolean().optional(),
|
|
157
|
+
user: githubAccount.nullable(),
|
|
158
|
+
html_url: url,
|
|
159
|
+
created_at: timestamp,
|
|
160
|
+
updated_at: timestamp,
|
|
161
|
+
closed_at: timestamp.nullish(),
|
|
162
|
+
merged_at: timestamp.nullish(),
|
|
163
|
+
merged: z.boolean().optional(),
|
|
164
|
+
mergeable: z.boolean().nullish(),
|
|
165
|
+
head: branchRef,
|
|
166
|
+
base: branchRef,
|
|
167
|
+
labels: z.array(label).max(100).optional(),
|
|
168
|
+
assignees: z.array(githubAccount).max(100).nullish(),
|
|
169
|
+
requested_reviewers: z.array(githubAccount).max(100).nullish(),
|
|
170
|
+
comments: z.number().int().optional(),
|
|
171
|
+
commits: z.number().int().optional(),
|
|
172
|
+
additions: z.number().int().optional(),
|
|
173
|
+
deletions: z.number().int().optional(),
|
|
174
|
+
changed_files: z.number().int().optional(),
|
|
175
|
+
});
|
|
176
|
+
const contentEntry = z.object({
|
|
177
|
+
type: z.enum(["file", "dir", "symlink", "submodule"]),
|
|
178
|
+
name: text(255),
|
|
179
|
+
path: text(4096),
|
|
180
|
+
sha: text(64),
|
|
181
|
+
size: z.number().int(),
|
|
182
|
+
html_url: url.nullish(),
|
|
183
|
+
download_url: url.nullish(),
|
|
184
|
+
});
|
|
185
|
+
const content = contentEntry.extend({
|
|
186
|
+
encoding: text(32).optional(),
|
|
187
|
+
content: text(2_000_000).optional(),
|
|
188
|
+
target: text(4096).optional(),
|
|
189
|
+
submodule_git_url: url.optional(),
|
|
190
|
+
});
|
|
191
|
+
function define(definition) {
|
|
192
|
+
return {
|
|
193
|
+
...definition,
|
|
194
|
+
plan(raw) {
|
|
195
|
+
const parsed = definition.input.safeParse(raw);
|
|
196
|
+
if (!parsed.success)
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
fields: [
|
|
200
|
+
...new Set(parsed.error.issues.map((issue) => issue.path.map(String).join(".") || "(body)")),
|
|
201
|
+
].slice(0, 20),
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
request: {
|
|
206
|
+
method: definition.method,
|
|
207
|
+
...definition.request(parsed.data),
|
|
208
|
+
},
|
|
209
|
+
scopes: definition.scopes?.(parsed.data) ?? [],
|
|
210
|
+
repository: definition.repository ?? false,
|
|
211
|
+
paginated: definition.paginated ?? false,
|
|
212
|
+
parse: (body) => {
|
|
213
|
+
const result = definition.response.safeParse(body);
|
|
214
|
+
return result.success
|
|
215
|
+
? { ok: true, data: result.data }
|
|
216
|
+
: { ok: false };
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const optionalNumber = (value) => value === undefined ? undefined : String(value);
|
|
223
|
+
const writeScopes = () => ["public_repo"];
|
|
224
|
+
/**
|
|
225
|
+
* Every GitHub call the connector can make, keyed by a stable operation id.
|
|
226
|
+
* Inputs and outputs use GitHub's own field names; responses are projected to
|
|
227
|
+
* the listed fields, so anything else GitHub returns is dropped.
|
|
228
|
+
*/
|
|
229
|
+
export const githubOperations = {
|
|
230
|
+
"viewer.get": define({
|
|
231
|
+
method: "GET",
|
|
232
|
+
input: z.object({}).strict(),
|
|
233
|
+
response: viewer,
|
|
234
|
+
request: () => ({ segments: ["user"] }),
|
|
235
|
+
}),
|
|
236
|
+
"repos.listForViewer": define({
|
|
237
|
+
method: "GET",
|
|
238
|
+
paginated: true,
|
|
239
|
+
input: z
|
|
240
|
+
.object({
|
|
241
|
+
visibility: z.enum(["all", "public", "private"]).default("all"),
|
|
242
|
+
affiliation: z
|
|
243
|
+
.array(z.enum(["owner", "collaborator", "organization_member"]))
|
|
244
|
+
.min(1)
|
|
245
|
+
.max(3)
|
|
246
|
+
.optional(),
|
|
247
|
+
sort: z.enum(["created", "updated", "pushed", "full_name"]).optional(),
|
|
248
|
+
direction: direction.optional(),
|
|
249
|
+
...paging,
|
|
250
|
+
})
|
|
251
|
+
.strict(),
|
|
252
|
+
response: z.array(repositorySummary).max(100),
|
|
253
|
+
scopes: (input) => (input.visibility === "public" ? [] : ["repo"]),
|
|
254
|
+
request: (input) => ({
|
|
255
|
+
segments: ["user", "repos"],
|
|
256
|
+
query: {
|
|
257
|
+
visibility: input.visibility,
|
|
258
|
+
affiliation: input.affiliation
|
|
259
|
+
? [...new Set(input.affiliation)].join(",")
|
|
260
|
+
: undefined,
|
|
261
|
+
sort: input.sort,
|
|
262
|
+
direction: input.direction,
|
|
263
|
+
per_page: optionalNumber(input.per_page),
|
|
264
|
+
page: optionalNumber(input.page),
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
}),
|
|
268
|
+
"repos.get": define({
|
|
269
|
+
method: "GET",
|
|
270
|
+
repository: true,
|
|
271
|
+
input: z.object(repository).strict(),
|
|
272
|
+
response: repositorySummary,
|
|
273
|
+
request: (input) => ({ segments: ["repos", input.owner, input.repo] }),
|
|
274
|
+
}),
|
|
275
|
+
"issues.list": define({
|
|
276
|
+
method: "GET",
|
|
277
|
+
repository: true,
|
|
278
|
+
paginated: true,
|
|
279
|
+
input: z
|
|
280
|
+
.object({
|
|
281
|
+
...repository,
|
|
282
|
+
state: z.enum(["open", "closed", "all"]).optional(),
|
|
283
|
+
labels: z.array(labelFilter).min(1).max(20).optional(),
|
|
284
|
+
sort: z.enum(["created", "updated", "comments"]).optional(),
|
|
285
|
+
direction: direction.optional(),
|
|
286
|
+
since: since.optional(),
|
|
287
|
+
...paging,
|
|
288
|
+
})
|
|
289
|
+
.strict(),
|
|
290
|
+
response: z.array(issue).max(100),
|
|
291
|
+
request: (input) => ({
|
|
292
|
+
segments: ["repos", input.owner, input.repo, "issues"],
|
|
293
|
+
query: {
|
|
294
|
+
state: input.state,
|
|
295
|
+
labels: input.labels?.join(","),
|
|
296
|
+
sort: input.sort,
|
|
297
|
+
direction: input.direction,
|
|
298
|
+
since: input.since,
|
|
299
|
+
per_page: optionalNumber(input.per_page),
|
|
300
|
+
page: optionalNumber(input.page),
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
}),
|
|
304
|
+
"issues.get": define({
|
|
305
|
+
method: "GET",
|
|
306
|
+
repository: true,
|
|
307
|
+
input: z.object({ ...repository, issue_number: positiveInt }).strict(),
|
|
308
|
+
response: issue,
|
|
309
|
+
request: (input) => ({
|
|
310
|
+
segments: [
|
|
311
|
+
"repos",
|
|
312
|
+
input.owner,
|
|
313
|
+
input.repo,
|
|
314
|
+
"issues",
|
|
315
|
+
String(input.issue_number),
|
|
316
|
+
],
|
|
317
|
+
}),
|
|
318
|
+
}),
|
|
319
|
+
"issues.create": define({
|
|
320
|
+
method: "POST",
|
|
321
|
+
repository: true,
|
|
322
|
+
input: z
|
|
323
|
+
.object({
|
|
324
|
+
...repository,
|
|
325
|
+
title: z.string().min(1).max(256),
|
|
326
|
+
body: markdown.optional(),
|
|
327
|
+
labels: z.array(labelName).max(100).optional(),
|
|
328
|
+
assignees: z.array(login).max(10).optional(),
|
|
329
|
+
milestone: positiveInt.nullable().optional(),
|
|
330
|
+
})
|
|
331
|
+
.strict(),
|
|
332
|
+
response: issue,
|
|
333
|
+
scopes: writeScopes,
|
|
334
|
+
request: ({ owner, repo, ...body }) => ({
|
|
335
|
+
segments: ["repos", owner, repo, "issues"],
|
|
336
|
+
body,
|
|
337
|
+
}),
|
|
338
|
+
}),
|
|
339
|
+
"issues.update": define({
|
|
340
|
+
method: "PATCH",
|
|
341
|
+
repository: true,
|
|
342
|
+
input: z
|
|
343
|
+
.object({
|
|
344
|
+
...repository,
|
|
345
|
+
issue_number: positiveInt,
|
|
346
|
+
title: z.string().min(1).max(256).optional(),
|
|
347
|
+
body: markdown.nullable().optional(),
|
|
348
|
+
state: z.enum(["open", "closed"]).optional(),
|
|
349
|
+
state_reason: z
|
|
350
|
+
.enum(["completed", "not_planned", "reopened"])
|
|
351
|
+
.nullable()
|
|
352
|
+
.optional(),
|
|
353
|
+
labels: z.array(labelName).max(100).optional(),
|
|
354
|
+
assignees: z.array(login).max(10).optional(),
|
|
355
|
+
milestone: positiveInt.nullable().optional(),
|
|
356
|
+
})
|
|
357
|
+
.strict()
|
|
358
|
+
.refine((input) => [
|
|
359
|
+
input.title,
|
|
360
|
+
input.body,
|
|
361
|
+
input.state,
|
|
362
|
+
input.state_reason,
|
|
363
|
+
input.labels,
|
|
364
|
+
input.assignees,
|
|
365
|
+
input.milestone,
|
|
366
|
+
].some((value) => value !== undefined), { message: "Nothing to update" }),
|
|
367
|
+
response: issue,
|
|
368
|
+
scopes: writeScopes,
|
|
369
|
+
request: ({ owner, repo, issue_number, ...body }) => ({
|
|
370
|
+
segments: ["repos", owner, repo, "issues", String(issue_number)],
|
|
371
|
+
body,
|
|
372
|
+
}),
|
|
373
|
+
}),
|
|
374
|
+
"issues.listComments": define({
|
|
375
|
+
method: "GET",
|
|
376
|
+
repository: true,
|
|
377
|
+
paginated: true,
|
|
378
|
+
input: z
|
|
379
|
+
.object({
|
|
380
|
+
...repository,
|
|
381
|
+
issue_number: positiveInt,
|
|
382
|
+
since: since.optional(),
|
|
383
|
+
...paging,
|
|
384
|
+
})
|
|
385
|
+
.strict(),
|
|
386
|
+
response: z.array(comment).max(100),
|
|
387
|
+
request: (input) => ({
|
|
388
|
+
segments: [
|
|
389
|
+
"repos",
|
|
390
|
+
input.owner,
|
|
391
|
+
input.repo,
|
|
392
|
+
"issues",
|
|
393
|
+
String(input.issue_number),
|
|
394
|
+
"comments",
|
|
395
|
+
],
|
|
396
|
+
query: {
|
|
397
|
+
since: input.since,
|
|
398
|
+
per_page: optionalNumber(input.per_page),
|
|
399
|
+
page: optionalNumber(input.page),
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
}),
|
|
403
|
+
"issues.createComment": define({
|
|
404
|
+
method: "POST",
|
|
405
|
+
repository: true,
|
|
406
|
+
input: z
|
|
407
|
+
.object({
|
|
408
|
+
...repository,
|
|
409
|
+
issue_number: positiveInt,
|
|
410
|
+
body: markdown.min(1),
|
|
411
|
+
})
|
|
412
|
+
.strict(),
|
|
413
|
+
response: comment,
|
|
414
|
+
scopes: writeScopes,
|
|
415
|
+
request: ({ owner, repo, issue_number, body }) => ({
|
|
416
|
+
segments: [
|
|
417
|
+
"repos",
|
|
418
|
+
owner,
|
|
419
|
+
repo,
|
|
420
|
+
"issues",
|
|
421
|
+
String(issue_number),
|
|
422
|
+
"comments",
|
|
423
|
+
],
|
|
424
|
+
body: { body },
|
|
425
|
+
}),
|
|
426
|
+
}),
|
|
427
|
+
"pulls.list": define({
|
|
428
|
+
method: "GET",
|
|
429
|
+
repository: true,
|
|
430
|
+
paginated: true,
|
|
431
|
+
input: z
|
|
432
|
+
.object({
|
|
433
|
+
...repository,
|
|
434
|
+
state: z.enum(["open", "closed", "all"]).optional(),
|
|
435
|
+
head: headFilter.optional(),
|
|
436
|
+
base: gitRef.optional(),
|
|
437
|
+
sort: z
|
|
438
|
+
.enum(["created", "updated", "popularity", "long-running"])
|
|
439
|
+
.optional(),
|
|
440
|
+
direction: direction.optional(),
|
|
441
|
+
...paging,
|
|
442
|
+
})
|
|
443
|
+
.strict(),
|
|
444
|
+
response: z.array(pullRequest).max(100),
|
|
445
|
+
request: (input) => ({
|
|
446
|
+
segments: ["repos", input.owner, input.repo, "pulls"],
|
|
447
|
+
query: {
|
|
448
|
+
state: input.state,
|
|
449
|
+
head: input.head,
|
|
450
|
+
base: input.base,
|
|
451
|
+
sort: input.sort,
|
|
452
|
+
direction: input.direction,
|
|
453
|
+
per_page: optionalNumber(input.per_page),
|
|
454
|
+
page: optionalNumber(input.page),
|
|
455
|
+
},
|
|
456
|
+
}),
|
|
457
|
+
}),
|
|
458
|
+
"pulls.get": define({
|
|
459
|
+
method: "GET",
|
|
460
|
+
repository: true,
|
|
461
|
+
input: z.object({ ...repository, pull_number: positiveInt }).strict(),
|
|
462
|
+
response: pullRequest,
|
|
463
|
+
request: (input) => ({
|
|
464
|
+
segments: [
|
|
465
|
+
"repos",
|
|
466
|
+
input.owner,
|
|
467
|
+
input.repo,
|
|
468
|
+
"pulls",
|
|
469
|
+
String(input.pull_number),
|
|
470
|
+
],
|
|
471
|
+
}),
|
|
472
|
+
}),
|
|
473
|
+
"repos.getContent": define({
|
|
474
|
+
method: "GET",
|
|
475
|
+
repository: true,
|
|
476
|
+
input: z
|
|
477
|
+
.object({
|
|
478
|
+
...repository,
|
|
479
|
+
path: contentPath.optional(),
|
|
480
|
+
ref: gitRef.optional(),
|
|
481
|
+
})
|
|
482
|
+
.strict(),
|
|
483
|
+
response: z.union([z.array(contentEntry).max(1000), content]),
|
|
484
|
+
request: (input) => ({
|
|
485
|
+
segments: [
|
|
486
|
+
"repos",
|
|
487
|
+
input.owner,
|
|
488
|
+
input.repo,
|
|
489
|
+
"contents",
|
|
490
|
+
...(input.path ? input.path.split("/") : [""]),
|
|
491
|
+
],
|
|
492
|
+
query: { ref: input.ref },
|
|
493
|
+
}),
|
|
494
|
+
}),
|
|
495
|
+
"repos.createOrUpdateFile": define({
|
|
496
|
+
method: "PUT",
|
|
497
|
+
repository: true,
|
|
498
|
+
input: z
|
|
499
|
+
.object({
|
|
500
|
+
...repository,
|
|
501
|
+
path: contentPath,
|
|
502
|
+
message: z.string().min(1).max(4096),
|
|
503
|
+
content: base64,
|
|
504
|
+
sha: blobSha.optional(),
|
|
505
|
+
branch: gitRef.optional(),
|
|
506
|
+
})
|
|
507
|
+
.strict(),
|
|
508
|
+
response: z.object({
|
|
509
|
+
content: contentEntry.nullable(),
|
|
510
|
+
commit: z.object({
|
|
511
|
+
sha: text(64),
|
|
512
|
+
html_url: url.nullish(),
|
|
513
|
+
message: text(65_536).optional(),
|
|
514
|
+
}),
|
|
515
|
+
}),
|
|
516
|
+
// GitHub refuses workflow-file writes from a token without `workflow`.
|
|
517
|
+
scopes: (input) => /^\.github\/workflows\//i.test(input.path)
|
|
518
|
+
? ["public_repo", "workflow"]
|
|
519
|
+
: ["public_repo"],
|
|
520
|
+
request: ({ owner, repo, path, ...body }) => ({
|
|
521
|
+
segments: ["repos", owner, repo, "contents", ...path.split("/")],
|
|
522
|
+
body,
|
|
523
|
+
}),
|
|
524
|
+
}),
|
|
525
|
+
};
|
|
526
|
+
export function isGitHubOperationId(value) {
|
|
527
|
+
return Object.hasOwn(githubOperations, value);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* The api.github.com URL for an operation's path segments. Each segment is
|
|
531
|
+
* percent-encoded on its own, and the result must keep exactly that path on
|
|
532
|
+
* exactly this host, so no input can steer the token elsewhere.
|
|
533
|
+
*/
|
|
534
|
+
export function githubRequestUrl(segments, query = {}) {
|
|
535
|
+
const pathname = `/${segments.map(encodeURIComponent).join("/")}`;
|
|
536
|
+
const target = new URL(pathname, GITHUB_API_ORIGIN);
|
|
537
|
+
if (target.origin !== GITHUB_API_ORIGIN || target.pathname !== pathname)
|
|
538
|
+
throw new Error("GITHUB_PATH_REJECTED");
|
|
539
|
+
for (const [name, value] of Object.entries(query))
|
|
540
|
+
if (value !== undefined)
|
|
541
|
+
target.searchParams.set(name, value);
|
|
542
|
+
return target;
|
|
543
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth App scope rules shared by the connector route, the account page
|
|
3
|
+
* and the SDK. Pure and dependency-free so the SDK ships it unchanged.
|
|
4
|
+
*/
|
|
5
|
+
/** The scopes a Flow app may ask a user to add to their GitHub connection. */
|
|
6
|
+
export declare const GITHUB_REQUESTABLE_SCOPES: readonly ["read:user", "user:email", "public_repo", "repo", "read:org", "gist", "workflow"];
|
|
7
|
+
export type GitHubScope = (typeof GITHUB_REQUESTABLE_SCOPES)[number];
|
|
8
|
+
/** What each scope lets Flow do, in the words the consent screen uses. */
|
|
9
|
+
export declare const GITHUB_SCOPE_DESCRIPTIONS: {
|
|
10
|
+
readonly "read:user": "See your GitHub profile";
|
|
11
|
+
readonly "user:email": "See your email addresses";
|
|
12
|
+
readonly public_repo: "Read and change your public repositories, issues and pull requests";
|
|
13
|
+
readonly repo: "Read and change all your repositories, including private ones";
|
|
14
|
+
readonly "read:org": "See your organizations and team memberships";
|
|
15
|
+
readonly gist: "Create and edit your gists";
|
|
16
|
+
readonly workflow: "Change GitHub Actions workflow files";
|
|
17
|
+
};
|
|
18
|
+
/** What every connection asks for; more is requested only when a feature needs it. */
|
|
19
|
+
export declare const GITHUB_BASELINE_SCOPES: readonly GitHubScope[];
|
|
20
|
+
export declare function isGitHubScope(value: string): value is GitHubScope;
|
|
21
|
+
/** Whether the granted set carries `scope` directly or through a parent scope. */
|
|
22
|
+
export declare function hasGitHubScope(granted: readonly string[], scope: string): boolean;
|
|
23
|
+
export declare function missingGitHubScopes<Scope extends string>(granted: readonly string[], required: readonly Scope[]): Scope[];
|
|
24
|
+
/**
|
|
25
|
+
* The Flow ID account-page address that connects GitHub, or adds `scopes` to
|
|
26
|
+
* an existing connection. The one place this URL shape is written; `oauth=`
|
|
27
|
+
* is not used because it marks a return from the provider's callback.
|
|
28
|
+
*/
|
|
29
|
+
export declare function githubConsentUrl(issuer: string, scopes?: readonly GitHubScope[]): string;
|
|
30
|
+
/** The scopes a consent URL asks for, or null when the URL is not one. */
|
|
31
|
+
export declare function githubConsentRequest(search: string): GitHubScope[] | null;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth App scope rules shared by the connector route, the account page
|
|
3
|
+
* and the SDK. Pure and dependency-free so the SDK ships it unchanged.
|
|
4
|
+
*/
|
|
5
|
+
/** The scopes a Flow app may ask a user to add to their GitHub connection. */
|
|
6
|
+
export const GITHUB_REQUESTABLE_SCOPES = [
|
|
7
|
+
"read:user",
|
|
8
|
+
"user:email",
|
|
9
|
+
"public_repo",
|
|
10
|
+
"repo",
|
|
11
|
+
"read:org",
|
|
12
|
+
"gist",
|
|
13
|
+
"workflow",
|
|
14
|
+
];
|
|
15
|
+
/** What each scope lets Flow do, in the words the consent screen uses. */
|
|
16
|
+
export const GITHUB_SCOPE_DESCRIPTIONS = {
|
|
17
|
+
"read:user": "See your GitHub profile",
|
|
18
|
+
"user:email": "See your email addresses",
|
|
19
|
+
public_repo: "Read and change your public repositories, issues and pull requests",
|
|
20
|
+
repo: "Read and change all your repositories, including private ones",
|
|
21
|
+
"read:org": "See your organizations and team memberships",
|
|
22
|
+
gist: "Create and edit your gists",
|
|
23
|
+
workflow: "Change GitHub Actions workflow files",
|
|
24
|
+
};
|
|
25
|
+
/** What every connection asks for; more is requested only when a feature needs it. */
|
|
26
|
+
export const GITHUB_BASELINE_SCOPES = ["read:user"];
|
|
27
|
+
// GitHub grants a parent scope's children implicitly, so a token holding
|
|
28
|
+
// `repo` never lists `public_repo` even though it carries that authority.
|
|
29
|
+
const impliedBy = new Map([
|
|
30
|
+
["public_repo", ["repo"]],
|
|
31
|
+
["read:user", ["user"]],
|
|
32
|
+
["user:email", ["user"]],
|
|
33
|
+
["read:org", ["write:org", "admin:org"]],
|
|
34
|
+
]);
|
|
35
|
+
export function isGitHubScope(value) {
|
|
36
|
+
return GITHUB_REQUESTABLE_SCOPES.some((scope) => scope === value);
|
|
37
|
+
}
|
|
38
|
+
/** Whether the granted set carries `scope` directly or through a parent scope. */
|
|
39
|
+
export function hasGitHubScope(granted, scope) {
|
|
40
|
+
return (granted.includes(scope) ||
|
|
41
|
+
(impliedBy.get(scope) ?? []).some((parent) => granted.includes(parent)));
|
|
42
|
+
}
|
|
43
|
+
export function missingGitHubScopes(granted, required) {
|
|
44
|
+
return [
|
|
45
|
+
...new Set(required.filter((scope) => !hasGitHubScope(granted, scope))),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The Flow ID account-page address that connects GitHub, or adds `scopes` to
|
|
50
|
+
* an existing connection. The one place this URL shape is written; `oauth=`
|
|
51
|
+
* is not used because it marks a return from the provider's callback.
|
|
52
|
+
*/
|
|
53
|
+
export function githubConsentUrl(issuer, scopes = []) {
|
|
54
|
+
const url = new URL("/account", issuer);
|
|
55
|
+
url.searchParams.set("section", "connections");
|
|
56
|
+
url.searchParams.set("connect", "github");
|
|
57
|
+
const requested = [...new Set(scopes)];
|
|
58
|
+
if (requested.length)
|
|
59
|
+
url.searchParams.set("scopes", requested.join(" "));
|
|
60
|
+
return url.toString();
|
|
61
|
+
}
|
|
62
|
+
/** The scopes a consent URL asks for, or null when the URL is not one. */
|
|
63
|
+
export function githubConsentRequest(search) {
|
|
64
|
+
const params = new URLSearchParams(search);
|
|
65
|
+
if (params.get("connect") !== "github")
|
|
66
|
+
return null;
|
|
67
|
+
return [
|
|
68
|
+
...new Set((params.get("scopes") ?? "").split(/[\s,]+/).filter(isGitHubScope)),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AppDb } from "../db/handle";
|
|
2
|
+
import { oauthSession } from "../db/schema";
|
|
2
3
|
export interface OAuthCredentialStore<T> {
|
|
3
4
|
set(id: string, value: T): Promise<void>;
|
|
4
5
|
get(id: string): Promise<T | undefined>;
|
|
@@ -13,4 +14,6 @@ export declare function createOAuthStores<State, Session>(database: AppDb, stora
|
|
|
13
14
|
};
|
|
14
15
|
export declare function cleanupOAuthCredentials(database: AppDb): Promise<void>;
|
|
15
16
|
export declare function peekOAuthState<State>(database: AppDb, storageKey: string, providerId: string, id: string): Promise<State | undefined>;
|
|
17
|
+
/** Opens a sealed `oauth_session` row read outside a store, such as one an erasure deleted. */
|
|
18
|
+
export declare function openOAuthSessionRow<Session>(storageKey: string, row: Pick<typeof oauthSession.$inferSelect, "providerId" | "accountId" | "data" | "formatVersion">): Session;
|
|
16
19
|
export declare function lockOAuthSession(database: AppDb, providerId: string, accountId: string): Promise<void>;
|
package/dist/sdk/oauth/stores.js
CHANGED
|
@@ -144,6 +144,10 @@ export async function peekOAuthState(database, storageKey, providerId, id) {
|
|
|
144
144
|
return undefined;
|
|
145
145
|
return createCredentialCipher(storageKey).open(row.data, JSON.stringify(["oauth-state", providerId, id]));
|
|
146
146
|
}
|
|
147
|
+
/** Opens a sealed `oauth_session` row read outside a store, such as one an erasure deleted. */
|
|
148
|
+
export function openOAuthSessionRow(storageKey, row) {
|
|
149
|
+
return createCredentialCipher(storageKey).open(row.data, sessionContext(row.providerId, row.accountId, row.formatVersion));
|
|
150
|
+
}
|
|
147
151
|
function sessionContext(providerId, accountId, formatVersion) {
|
|
148
152
|
if (formatVersion === 2)
|
|
149
153
|
return JSON.stringify(["oauth-session", providerId, accountId]);
|