@emulators/vercel 0.4.0 → 0.5.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 +90 -0
- package/dist/fonts/favicon.ico +0 -0
- package/dist/index.js +210 -45
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @emulators/vercel
|
|
2
|
+
|
|
3
|
+
Fully stateful Vercel API emulation with Vercel-style JSON responses and cursor-based pagination.
|
|
4
|
+
|
|
5
|
+
Part of [emulate](https://github.com/vercel-labs/emulate) — local drop-in replacement services for CI and no-network sandboxes.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @emulators/vercel
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Endpoints
|
|
14
|
+
|
|
15
|
+
### User & Teams
|
|
16
|
+
- `GET /v2/user` — authenticated user
|
|
17
|
+
- `PATCH /v2/user` — update user
|
|
18
|
+
- `GET /v2/teams` — list teams (cursor paginated)
|
|
19
|
+
- `GET /v2/teams/:teamId` — get team (by ID or slug)
|
|
20
|
+
- `POST /v2/teams` — create team
|
|
21
|
+
- `PATCH /v2/teams/:teamId` — update team
|
|
22
|
+
- `GET /v2/teams/:teamId/members` — list members
|
|
23
|
+
- `POST /v2/teams/:teamId/members` — add member
|
|
24
|
+
|
|
25
|
+
### Projects
|
|
26
|
+
- `POST /v11/projects` — create project (with optional env vars and git integration)
|
|
27
|
+
- `GET /v10/projects` — list projects (search, cursor pagination)
|
|
28
|
+
- `GET /v9/projects/:idOrName` — get project (includes env vars)
|
|
29
|
+
- `PATCH /v9/projects/:idOrName` — update project
|
|
30
|
+
- `DELETE /v9/projects/:idOrName` — delete project (cascades)
|
|
31
|
+
- `GET /v1/projects/:projectId/promote/aliases` — promote aliases status
|
|
32
|
+
- `PATCH /v1/projects/:idOrName/protection-bypass` — manage bypass secrets
|
|
33
|
+
|
|
34
|
+
### Deployments
|
|
35
|
+
- `POST /v13/deployments` — create deployment (auto-transitions to READY)
|
|
36
|
+
- `GET /v13/deployments/:idOrUrl` — get deployment (by ID or URL)
|
|
37
|
+
- `GET /v6/deployments` — list deployments (filter by project, target, state)
|
|
38
|
+
- `DELETE /v13/deployments/:id` — delete deployment (cascades)
|
|
39
|
+
- `PATCH /v12/deployments/:id/cancel` — cancel building deployment
|
|
40
|
+
- `GET /v2/deployments/:id/aliases` — list deployment aliases
|
|
41
|
+
- `GET /v3/deployments/:idOrUrl/events` — get build events/logs
|
|
42
|
+
- `GET /v6/deployments/:id/files` — list deployment files
|
|
43
|
+
- `POST /v2/files` — upload file (by SHA digest)
|
|
44
|
+
|
|
45
|
+
### Domains
|
|
46
|
+
- `POST /v10/projects/:idOrName/domains` — add domain (with verification challenge)
|
|
47
|
+
- `GET /v9/projects/:idOrName/domains` — list domains
|
|
48
|
+
- `GET /v9/projects/:idOrName/domains/:domain` — get domain
|
|
49
|
+
- `PATCH /v9/projects/:idOrName/domains/:domain` — update domain
|
|
50
|
+
- `DELETE /v9/projects/:idOrName/domains/:domain` — remove domain
|
|
51
|
+
- `POST /v9/projects/:idOrName/domains/:domain/verify` — verify domain
|
|
52
|
+
|
|
53
|
+
### Environment Variables
|
|
54
|
+
- `GET /v10/projects/:idOrName/env` — list env vars (with decrypt option)
|
|
55
|
+
- `POST /v10/projects/:idOrName/env` — create env vars (single, batch, upsert)
|
|
56
|
+
- `GET /v10/projects/:idOrName/env/:id` — get env var
|
|
57
|
+
- `PATCH /v9/projects/:idOrName/env/:id` — update env var
|
|
58
|
+
- `DELETE /v9/projects/:idOrName/env/:id` — delete env var
|
|
59
|
+
|
|
60
|
+
## Auth
|
|
61
|
+
|
|
62
|
+
All endpoints accept `teamId` or `slug` query params for team scoping. Pagination uses cursor-based `limit`/`since`/`until` with `pagination` response objects.
|
|
63
|
+
|
|
64
|
+
## Seed Configuration
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
vercel:
|
|
68
|
+
users:
|
|
69
|
+
- username: developer
|
|
70
|
+
name: Developer
|
|
71
|
+
email: dev@example.com
|
|
72
|
+
teams:
|
|
73
|
+
- slug: my-team
|
|
74
|
+
name: My Team
|
|
75
|
+
projects:
|
|
76
|
+
- name: my-app
|
|
77
|
+
team: my-team
|
|
78
|
+
framework: nextjs
|
|
79
|
+
integrations:
|
|
80
|
+
- client_id: "oac_abc123"
|
|
81
|
+
client_secret: "secret_abc123"
|
|
82
|
+
name: "My Vercel App"
|
|
83
|
+
redirect_uris:
|
|
84
|
+
- "http://localhost:3000/api/auth/callback/vercel"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Links
|
|
88
|
+
|
|
89
|
+
- [Full documentation](https://emulate.dev/vercel)
|
|
90
|
+
- [GitHub](https://github.com/vercel-labs/emulate)
|
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,10 @@ function getVercelStore(store) {
|
|
|
6
6
|
teamMembers: store.collection("vercel.team_members", ["teamId", "userId"]),
|
|
7
7
|
projects: store.collection("vercel.projects", ["uid", "name", "accountId"]),
|
|
8
8
|
deployments: store.collection("vercel.deployments", ["uid", "projectId", "url"]),
|
|
9
|
-
deploymentAliases: store.collection("vercel.deployment_aliases", [
|
|
9
|
+
deploymentAliases: store.collection("vercel.deployment_aliases", [
|
|
10
|
+
"deploymentId",
|
|
11
|
+
"projectId"
|
|
12
|
+
]),
|
|
10
13
|
builds: store.collection("vercel.builds", ["deploymentId"]),
|
|
11
14
|
deploymentEvents: store.collection("vercel.deployment_events", ["deploymentId"]),
|
|
12
15
|
files: store.collection("vercel.files", ["digest"]),
|
|
@@ -51,7 +54,7 @@ function resolveTeamScope(c, vs) {
|
|
|
51
54
|
return { accountId: user.uid, team: null };
|
|
52
55
|
}
|
|
53
56
|
function lookupProject(vs, idOrName, accountId) {
|
|
54
|
-
|
|
57
|
+
const project = vs.projects.findOneBy("uid", idOrName);
|
|
55
58
|
if (project && project.accountId === accountId) return project;
|
|
56
59
|
const byName = vs.projects.findBy("name", idOrName);
|
|
57
60
|
return byName.find((p) => p.accountId === accountId);
|
|
@@ -287,6 +290,7 @@ var FONTS = {
|
|
|
287
290
|
"geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
|
|
288
291
|
"GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
|
|
289
292
|
};
|
|
293
|
+
var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
|
|
290
294
|
function escapeHtml(s) {
|
|
291
295
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
292
296
|
}
|
|
@@ -438,6 +442,132 @@ body{
|
|
|
438
442
|
.app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
|
|
439
443
|
.app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
|
|
440
444
|
.empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
|
|
445
|
+
|
|
446
|
+
.inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
|
|
447
|
+
.inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
|
|
448
|
+
.inspector-tabs a{
|
|
449
|
+
padding:7px 16px;border-radius:6px;text-decoration:none;
|
|
450
|
+
font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
|
|
451
|
+
transition:color .15s,border-color .15s;
|
|
452
|
+
}
|
|
453
|
+
.inspector-tabs a:hover{color:#33ff00;}
|
|
454
|
+
.inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
|
|
455
|
+
.inspector-section{margin-bottom:24px;}
|
|
456
|
+
.inspector-section h2{
|
|
457
|
+
font-family:'Geist Pixel',monospace;
|
|
458
|
+
font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
|
|
459
|
+
}
|
|
460
|
+
.inspector-section h3{
|
|
461
|
+
font-family:'Geist Pixel',monospace;
|
|
462
|
+
font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
|
|
463
|
+
}
|
|
464
|
+
.inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
|
|
465
|
+
.inspector-table th,.inspector-table td{
|
|
466
|
+
text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
|
|
467
|
+
font-size:.8125rem;
|
|
468
|
+
}
|
|
469
|
+
.inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
|
|
470
|
+
.inspector-table td{color:#33ff00;}
|
|
471
|
+
.inspector-table tbody tr{transition:background .1s;}
|
|
472
|
+
.inspector-table tbody tr:hover{background:#0a3300;}
|
|
473
|
+
.inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
|
|
474
|
+
|
|
475
|
+
.checkout-layout{
|
|
476
|
+
display:flex;min-height:calc(100vh - 42px);
|
|
477
|
+
}
|
|
478
|
+
.checkout-summary{
|
|
479
|
+
flex:1;background:#020;padding:48px 40px 48px 10%;
|
|
480
|
+
display:flex;flex-direction:column;justify-content:center;
|
|
481
|
+
border-right:1px solid #0a3300;
|
|
482
|
+
}
|
|
483
|
+
.checkout-form-side{
|
|
484
|
+
flex:1;background:#000;padding:48px 10% 48px 40px;
|
|
485
|
+
display:flex;flex-direction:column;justify-content:center;
|
|
486
|
+
}
|
|
487
|
+
.checkout-merchant{
|
|
488
|
+
display:flex;align-items:center;gap:10px;margin-bottom:6px;
|
|
489
|
+
}
|
|
490
|
+
.checkout-merchant-name{
|
|
491
|
+
font-family:'Geist Pixel',monospace;
|
|
492
|
+
font-size:.9375rem;font-weight:600;color:#33ff00;
|
|
493
|
+
}
|
|
494
|
+
.checkout-test-badge{
|
|
495
|
+
font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
|
|
496
|
+
background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
|
|
497
|
+
}
|
|
498
|
+
.checkout-total{
|
|
499
|
+
font-family:'Geist Pixel',monospace;
|
|
500
|
+
font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
|
|
501
|
+
}
|
|
502
|
+
.checkout-line-item{
|
|
503
|
+
display:flex;align-items:center;gap:14px;padding:14px 0;
|
|
504
|
+
border-bottom:1px solid #0a3300;
|
|
505
|
+
}
|
|
506
|
+
.checkout-line-item:first-child{border-top:1px solid #0a3300;}
|
|
507
|
+
.checkout-item-icon{
|
|
508
|
+
width:42px;height:42px;border-radius:6px;background:#0a3300;
|
|
509
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
510
|
+
font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
|
|
511
|
+
}
|
|
512
|
+
.checkout-item-details{flex:1;min-width:0;}
|
|
513
|
+
.checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
|
|
514
|
+
.checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
515
|
+
.checkout-item-price{
|
|
516
|
+
font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
|
|
517
|
+
}
|
|
518
|
+
.checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
|
|
519
|
+
.checkout-totals{margin-top:20px;}
|
|
520
|
+
.checkout-totals-row{
|
|
521
|
+
display:flex;justify-content:space-between;padding:6px 0;
|
|
522
|
+
font-size:.8125rem;color:#1a8c00;
|
|
523
|
+
}
|
|
524
|
+
.checkout-totals-row.total{
|
|
525
|
+
border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
|
|
526
|
+
font-size:.9375rem;font-weight:600;color:#33ff00;
|
|
527
|
+
}
|
|
528
|
+
.checkout-form-section{margin-bottom:24px;}
|
|
529
|
+
.checkout-form-label{
|
|
530
|
+
font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
|
|
531
|
+
}
|
|
532
|
+
.checkout-input{
|
|
533
|
+
width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
|
|
534
|
+
background:#020;color:#33ff00;font:inherit;font-size:.875rem;
|
|
535
|
+
transition:border-color .15s;outline:none;
|
|
536
|
+
}
|
|
537
|
+
.checkout-input:focus{border-color:#33ff00;}
|
|
538
|
+
.checkout-input::placeholder{color:#116600;}
|
|
539
|
+
.checkout-card-box{
|
|
540
|
+
border:1px solid #0a3300;border-radius:6px;padding:14px;
|
|
541
|
+
background:#020;
|
|
542
|
+
}
|
|
543
|
+
.checkout-card-row{
|
|
544
|
+
display:flex;gap:12px;margin-top:10px;
|
|
545
|
+
}
|
|
546
|
+
.checkout-card-row .checkout-input{flex:1;}
|
|
547
|
+
.checkout-sim-note{
|
|
548
|
+
font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
|
|
549
|
+
font-style:italic;
|
|
550
|
+
}
|
|
551
|
+
.checkout-pay-btn{
|
|
552
|
+
width:100%;padding:14px;border:none;border-radius:8px;
|
|
553
|
+
background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
|
|
554
|
+
cursor:pointer;transition:background .15s;
|
|
555
|
+
font-family:'Geist Pixel',monospace;
|
|
556
|
+
}
|
|
557
|
+
.checkout-pay-btn:hover{background:#44ff22;}
|
|
558
|
+
.checkout-cancel{
|
|
559
|
+
text-align:center;margin-top:14px;
|
|
560
|
+
}
|
|
561
|
+
.checkout-cancel a{
|
|
562
|
+
color:#1a8c00;text-decoration:none;font-size:.8125rem;
|
|
563
|
+
transition:color .15s;
|
|
564
|
+
}
|
|
565
|
+
.checkout-cancel a:hover{color:#33ff00;}
|
|
566
|
+
@media(max-width:768px){
|
|
567
|
+
.checkout-layout{flex-direction:column;}
|
|
568
|
+
.checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
|
|
569
|
+
.checkout-form-side{padding:32px 20px;}
|
|
570
|
+
}
|
|
441
571
|
`;
|
|
442
572
|
var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
|
|
443
573
|
function emuBar(service) {
|
|
@@ -457,6 +587,7 @@ function head(title) {
|
|
|
457
587
|
<head>
|
|
458
588
|
<meta charset="utf-8"/>
|
|
459
589
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
590
|
+
<link rel="icon" href="/_emulate/favicon.ico"/>
|
|
460
591
|
<title>${escapeHtml(title)} | emulate</title>
|
|
461
592
|
<style>${CSS}</style>
|
|
462
593
|
</head>`;
|
|
@@ -938,7 +1069,9 @@ function projectsRoutes({ app, store, baseUrl }) {
|
|
|
938
1069
|
key,
|
|
939
1070
|
value: typeof ev.value === "string" ? ev.value : String(ev.value ?? ""),
|
|
940
1071
|
type: ev.type === "system" || ev.type === "encrypted" || ev.type === "plain" || ev.type === "secret" || ev.type === "sensitive" ? ev.type : "encrypted",
|
|
941
|
-
target: Array.isArray(ev.target) ? ev.target.filter(
|
|
1072
|
+
target: Array.isArray(ev.target) ? ev.target.filter(
|
|
1073
|
+
(t) => t === "production" || t === "preview" || t === "development"
|
|
1074
|
+
) : ["production", "preview", "development"],
|
|
942
1075
|
gitBranch: typeof ev.gitBranch === "string" ? ev.gitBranch : null,
|
|
943
1076
|
customEnvironmentIds: Array.isArray(ev.customEnvironmentIds) ? ev.customEnvironmentIds : [],
|
|
944
1077
|
comment: typeof ev.comment === "string" ? ev.comment : null,
|
|
@@ -1927,11 +2060,17 @@ function parseEnvRow(body) {
|
|
|
1927
2060
|
}
|
|
1928
2061
|
const type = parseType(body.type);
|
|
1929
2062
|
if (type === "invalid") {
|
|
1930
|
-
return {
|
|
2063
|
+
return {
|
|
2064
|
+
row: {},
|
|
2065
|
+
error: "Invalid value: type must be one of system, encrypted, plain, secret, sensitive"
|
|
2066
|
+
};
|
|
1931
2067
|
}
|
|
1932
2068
|
const target = parseTarget(body.target);
|
|
1933
2069
|
if (target === "invalid") {
|
|
1934
|
-
return {
|
|
2070
|
+
return {
|
|
2071
|
+
row: {},
|
|
2072
|
+
error: "Invalid value: target must be a non-empty array of production, preview, development"
|
|
2073
|
+
};
|
|
1935
2074
|
}
|
|
1936
2075
|
const customEnvironmentIds = parseCustomEnvironmentIds(body.customEnvironmentIds);
|
|
1937
2076
|
if (customEnvironmentIds === "invalid") {
|
|
@@ -2039,9 +2178,7 @@ function envRoutes({ app, store }) {
|
|
|
2039
2178
|
}
|
|
2040
2179
|
const { row } = parsed;
|
|
2041
2180
|
const existingDb = findEnvByKeyAndTargetsOverlap(vs, project.uid, row.key, row.target);
|
|
2042
|
-
const existingPending = pending.find(
|
|
2043
|
-
(e) => e.key === row.key && targetsOverlap(e.target, row.target)
|
|
2044
|
-
);
|
|
2181
|
+
const existingPending = pending.find((e) => e.key === row.key && targetsOverlap(e.target, row.target));
|
|
2045
2182
|
if (upsert) {
|
|
2046
2183
|
const toUpdate = existingDb ?? existingPending;
|
|
2047
2184
|
if (toUpdate) {
|
|
@@ -2140,14 +2277,24 @@ function envRoutes({ app, store }) {
|
|
|
2140
2277
|
if ("type" in body) {
|
|
2141
2278
|
const t = parseType(body.type);
|
|
2142
2279
|
if (t === "invalid") {
|
|
2143
|
-
return vercelErr5(
|
|
2280
|
+
return vercelErr5(
|
|
2281
|
+
c,
|
|
2282
|
+
400,
|
|
2283
|
+
"bad_request",
|
|
2284
|
+
"Invalid value: type must be one of system, encrypted, plain, secret, sensitive"
|
|
2285
|
+
);
|
|
2144
2286
|
}
|
|
2145
2287
|
patch.type = t;
|
|
2146
2288
|
}
|
|
2147
2289
|
if ("target" in body) {
|
|
2148
2290
|
const t = parseTarget(body.target);
|
|
2149
2291
|
if (t === "invalid") {
|
|
2150
|
-
return vercelErr5(
|
|
2292
|
+
return vercelErr5(
|
|
2293
|
+
c,
|
|
2294
|
+
400,
|
|
2295
|
+
"bad_request",
|
|
2296
|
+
"Invalid value: target must be a non-empty array of production, preview, development"
|
|
2297
|
+
);
|
|
2151
2298
|
}
|
|
2152
2299
|
patch.target = t;
|
|
2153
2300
|
}
|
|
@@ -2245,11 +2392,23 @@ function oauthRoutes({ app, store, tokenMap }) {
|
|
|
2245
2392
|
if (integrationsConfigured) {
|
|
2246
2393
|
const integration = vs.integrations.findOneBy("client_id", client_id);
|
|
2247
2394
|
if (!integration) {
|
|
2248
|
-
return c.html(
|
|
2395
|
+
return c.html(
|
|
2396
|
+
renderErrorPage("Application not found", `The client_id '${client_id}' is not registered.`, SERVICE_LABEL),
|
|
2397
|
+
400
|
|
2398
|
+
);
|
|
2249
2399
|
}
|
|
2250
2400
|
if (redirect_uri && !matchesRedirectUri(redirect_uri, integration.redirect_uris)) {
|
|
2251
|
-
console.warn(
|
|
2252
|
-
|
|
2401
|
+
console.warn(
|
|
2402
|
+
`[OAuth] redirect_uri mismatch: got "${redirect_uri}", registered: ${JSON.stringify(integration.redirect_uris)}`
|
|
2403
|
+
);
|
|
2404
|
+
return c.html(
|
|
2405
|
+
renderErrorPage(
|
|
2406
|
+
"Redirect URI mismatch",
|
|
2407
|
+
"The redirect_uri is not registered for this application.",
|
|
2408
|
+
SERVICE_LABEL
|
|
2409
|
+
),
|
|
2410
|
+
400
|
|
2411
|
+
);
|
|
2253
2412
|
}
|
|
2254
2413
|
integrationName = integration.name;
|
|
2255
2414
|
}
|
|
@@ -2297,7 +2456,10 @@ function oauthRoutes({ app, store, tokenMap }) {
|
|
|
2297
2456
|
codeChallengeMethod: code_challenge_method || null,
|
|
2298
2457
|
created_at: Date.now()
|
|
2299
2458
|
});
|
|
2300
|
-
debug(
|
|
2459
|
+
debug(
|
|
2460
|
+
"vercel.oauth",
|
|
2461
|
+
`[Vercel callback] generated code: ${code.slice(0, 8)}... for username=${username}, challenge=${code_challenge ? "present" : "none"}, pendingCodes size: ${pendingCodes.size}`
|
|
2462
|
+
);
|
|
2301
2463
|
const url = new URL(redirect_uri);
|
|
2302
2464
|
url.searchParams.set("code", code);
|
|
2303
2465
|
if (state !== "") url.searchParams.set("state", state);
|
|
@@ -2309,7 +2471,10 @@ function oauthRoutes({ app, store, tokenMap }) {
|
|
|
2309
2471
|
const pendingCodes = getPendingCodes(store);
|
|
2310
2472
|
debug("vercel.oauth", `[Vercel token] Content-Type: ${contentType}`);
|
|
2311
2473
|
debug("vercel.oauth", `[Vercel token] pendingCodes size: ${pendingCodes.size}`);
|
|
2312
|
-
debug(
|
|
2474
|
+
debug(
|
|
2475
|
+
"vercel.oauth",
|
|
2476
|
+
`[Vercel token] pendingCodes keys: ${[...pendingCodes.keys()].map((k) => k.slice(0, 8) + "...").join(", ")}`
|
|
2477
|
+
);
|
|
2313
2478
|
const rawText = await c.req.text();
|
|
2314
2479
|
debug("vercel.oauth", `[Vercel token] raw body: ${rawText.slice(0, 500)}`);
|
|
2315
2480
|
let body;
|
|
@@ -2331,73 +2496,70 @@ function oauthRoutes({ app, store, tokenMap }) {
|
|
|
2331
2496
|
debug("vercel.oauth", `[Vercel token] code: ${code.slice(0, 8)}... (len=${code.length})`);
|
|
2332
2497
|
debug("vercel.oauth", `[Vercel token] client_id: ${bodyClientId}`);
|
|
2333
2498
|
debug("vercel.oauth", `[Vercel token] client_secret: ${bodyClientSecret.slice(0, 4)}****`);
|
|
2334
|
-
debug(
|
|
2499
|
+
debug(
|
|
2500
|
+
"vercel.oauth",
|
|
2501
|
+
`[Vercel token] code_verifier: ${code_verifier ? code_verifier.slice(0, 8) + "..." : "undefined"}`
|
|
2502
|
+
);
|
|
2335
2503
|
const integrationsConfigured = vs.integrations.all().length > 0;
|
|
2336
2504
|
if (integrationsConfigured) {
|
|
2337
2505
|
const integration = vs.integrations.findOneBy("client_id", bodyClientId);
|
|
2338
2506
|
if (!integration) {
|
|
2339
2507
|
debug("vercel.oauth", `[Vercel token] REJECTED: client_id not found`);
|
|
2340
|
-
return c.json(
|
|
2508
|
+
return c.json(
|
|
2509
|
+
{ error: "invalid_client", error_description: "The client_id and/or client_secret passed are incorrect." },
|
|
2510
|
+
401
|
|
2511
|
+
);
|
|
2341
2512
|
}
|
|
2342
2513
|
if (!constantTimeSecretEqual(bodyClientSecret, integration.client_secret)) {
|
|
2343
2514
|
debug("vercel.oauth", `[Vercel token] REJECTED: client_secret mismatch`);
|
|
2344
|
-
return c.json(
|
|
2515
|
+
return c.json(
|
|
2516
|
+
{ error: "invalid_client", error_description: "The client_id and/or client_secret passed are incorrect." },
|
|
2517
|
+
401
|
|
2518
|
+
);
|
|
2345
2519
|
}
|
|
2346
2520
|
debug("vercel.oauth", `[Vercel token] client credentials OK (${integration.name})`);
|
|
2347
2521
|
}
|
|
2348
2522
|
const pending = pendingCodes.get(code);
|
|
2349
2523
|
if (!pending) {
|
|
2350
2524
|
debug("vercel.oauth", `[Vercel token] REJECTED: code not found in pendingCodes`);
|
|
2351
|
-
return c.json(
|
|
2352
|
-
{ error: "invalid_grant", error_description: "The code passed is incorrect or expired." },
|
|
2353
|
-
400
|
|
2354
|
-
);
|
|
2525
|
+
return c.json({ error: "invalid_grant", error_description: "The code passed is incorrect or expired." }, 400);
|
|
2355
2526
|
}
|
|
2356
2527
|
if (isPendingCodeExpired(pending)) {
|
|
2357
2528
|
debug("vercel.oauth", `[Vercel token] REJECTED: code expired`);
|
|
2358
2529
|
pendingCodes.delete(code);
|
|
2359
|
-
return c.json(
|
|
2360
|
-
{ error: "invalid_grant", error_description: "The code passed is incorrect or expired." },
|
|
2361
|
-
400
|
|
2362
|
-
);
|
|
2530
|
+
return c.json({ error: "invalid_grant", error_description: "The code passed is incorrect or expired." }, 400);
|
|
2363
2531
|
}
|
|
2364
2532
|
debug("vercel.oauth", `[Vercel token] code valid, username=${pending.username}, scope=${pending.scope}`);
|
|
2365
2533
|
if (redirect_uri && pending.redirectUri && redirect_uri !== pending.redirectUri) {
|
|
2366
|
-
debug(
|
|
2534
|
+
debug(
|
|
2535
|
+
"vercel.oauth",
|
|
2536
|
+
`[Vercel token] REJECTED: redirect_uri mismatch (got "${redirect_uri}", expected "${pending.redirectUri}")`
|
|
2537
|
+
);
|
|
2367
2538
|
pendingCodes.delete(code);
|
|
2368
2539
|
return c.json(
|
|
2369
|
-
{
|
|
2540
|
+
{
|
|
2541
|
+
error: "invalid_grant",
|
|
2542
|
+
error_description: "The redirect_uri does not match the one used during authorization."
|
|
2543
|
+
},
|
|
2370
2544
|
400
|
|
2371
2545
|
);
|
|
2372
2546
|
}
|
|
2373
2547
|
if (pending.codeChallenge != null) {
|
|
2374
2548
|
if (code_verifier === void 0) {
|
|
2375
|
-
return c.json(
|
|
2376
|
-
{ error: "invalid_grant", error_description: "PKCE verification failed." },
|
|
2377
|
-
400
|
|
2378
|
-
);
|
|
2549
|
+
return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
|
|
2379
2550
|
}
|
|
2380
2551
|
const method = (pending.codeChallengeMethod ?? "plain").toLowerCase();
|
|
2381
2552
|
if (method === "s256") {
|
|
2382
2553
|
const expected = createHash("sha256").update(code_verifier).digest("base64url");
|
|
2383
2554
|
if (expected !== pending.codeChallenge) {
|
|
2384
|
-
return c.json(
|
|
2385
|
-
{ error: "invalid_grant", error_description: "PKCE verification failed." },
|
|
2386
|
-
400
|
|
2387
|
-
);
|
|
2555
|
+
return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
|
|
2388
2556
|
}
|
|
2389
2557
|
} else if (method === "plain") {
|
|
2390
2558
|
if (code_verifier !== pending.codeChallenge) {
|
|
2391
|
-
return c.json(
|
|
2392
|
-
{ error: "invalid_grant", error_description: "PKCE verification failed." },
|
|
2393
|
-
400
|
|
2394
|
-
);
|
|
2559
|
+
return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
|
|
2395
2560
|
}
|
|
2396
2561
|
} else {
|
|
2397
|
-
return c.json(
|
|
2398
|
-
{ error: "invalid_grant", error_description: "PKCE verification failed." },
|
|
2399
|
-
400
|
|
2400
|
-
);
|
|
2562
|
+
return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
|
|
2401
2563
|
}
|
|
2402
2564
|
}
|
|
2403
2565
|
debug("vercel.oauth", `[Vercel token] PKCE OK (challenge=${pending.codeChallenge ? "present" : "none"})`);
|
|
@@ -2415,7 +2577,10 @@ function oauthRoutes({ app, store, tokenMap }) {
|
|
|
2415
2577
|
if (tokenMap) {
|
|
2416
2578
|
tokenMap.set(token, { login: user.username, id: user.id, scopes });
|
|
2417
2579
|
}
|
|
2418
|
-
debug(
|
|
2580
|
+
debug(
|
|
2581
|
+
"vercel.oauth",
|
|
2582
|
+
`[Vercel token] SUCCESS: issued token for ${user.username} (scopes: ${scopes.join(",") || "none"})`
|
|
2583
|
+
);
|
|
2419
2584
|
return c.json({
|
|
2420
2585
|
access_token: token,
|
|
2421
2586
|
token_type: "Bearer",
|