@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 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", ["deploymentId", "projectId"]),
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
- let project = vs.projects.findOneBy("uid", idOrName);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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((t) => t === "production" || t === "preview" || t === "development") : ["production", "preview", "development"],
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 { row: {}, error: "Invalid value: type must be one of system, encrypted, plain, secret, sensitive" };
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 { row: {}, error: "Invalid value: target must be a non-empty array of production, preview, development" };
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(c, 400, "bad_request", "Invalid value: type must be one of system, encrypted, plain, secret, sensitive");
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(c, 400, "bad_request", "Invalid value: target must be a non-empty array of production, preview, development");
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(renderErrorPage("Application not found", `The client_id '${client_id}' is not registered.`, SERVICE_LABEL), 400);
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(`[OAuth] redirect_uri mismatch: got "${redirect_uri}", registered: ${JSON.stringify(integration.redirect_uris)}`);
2252
- return c.html(renderErrorPage("Redirect URI mismatch", "The redirect_uri is not registered for this application.", SERVICE_LABEL), 400);
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("vercel.oauth", `[Vercel callback] generated code: ${code.slice(0, 8)}... for username=${username}, challenge=${code_challenge ? "present" : "none"}, pendingCodes size: ${pendingCodes.size}`);
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("vercel.oauth", `[Vercel token] pendingCodes keys: ${[...pendingCodes.keys()].map((k) => k.slice(0, 8) + "...").join(", ")}`);
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("vercel.oauth", `[Vercel token] code_verifier: ${code_verifier ? code_verifier.slice(0, 8) + "..." : "undefined"}`);
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({ error: "invalid_client", error_description: "The client_id and/or client_secret passed are incorrect." }, 401);
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({ error: "invalid_client", error_description: "The client_id and/or client_secret passed are incorrect." }, 401);
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("vercel.oauth", `[Vercel token] REJECTED: redirect_uri mismatch (got "${redirect_uri}", expected "${pending.redirectUri}")`);
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
- { error: "invalid_grant", error_description: "The redirect_uri does not match the one used during authorization." },
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("vercel.oauth", `[Vercel token] SUCCESS: issued token for ${user.username} (scopes: ${scopes.join(",") || "none"})`);
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",