emulate 0.2.0 → 0.3.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/LICENSE +202 -0
- package/README.md +66 -5
- package/dist/api.d.ts +3 -13
- package/dist/api.js +596 -17220
- package/dist/api.js.map +1 -1
- package/dist/chunk-D6EKRYGP.js +1615 -0
- package/dist/chunk-D6EKRYGP.js.map +1 -0
- package/dist/chunk-TEPNEZ63.js +2143 -0
- package/dist/chunk-TEPNEZ63.js.map +1 -0
- package/dist/dist-BKXG6HVH.js +3641 -0
- package/dist/dist-BKXG6HVH.js.map +1 -0
- package/dist/dist-DSSB3LYT.js +788 -0
- package/dist/dist-DSSB3LYT.js.map +1 -0
- package/dist/dist-JYDZIVC6.js +2684 -0
- package/dist/dist-JYDZIVC6.js.map +1 -0
- package/dist/dist-O4KFIBVU.js +626 -0
- package/dist/dist-O4KFIBVU.js.map +1 -0
- package/dist/dist-OCDKIMRJ.js +10586 -0
- package/dist/dist-OCDKIMRJ.js.map +1 -0
- package/dist/dist-UZSUUE3Y.js +1287 -0
- package/dist/dist-UZSUUE3Y.js.map +1 -0
- package/dist/dist-VVXVP5EZ.js +1199 -0
- package/dist/dist-VVXVP5EZ.js.map +1 -0
- package/dist/index.js +612 -17370
- package/dist/index.js.map +1 -1
- package/package.json +17 -14
|
@@ -0,0 +1,2684 @@
|
|
|
1
|
+
import "./chunk-TEPNEZ63.js";
|
|
2
|
+
|
|
3
|
+
// ../@emulators/vercel/dist/index.js
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
import { timingSafeEqual } from "crypto";
|
|
9
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
10
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
11
|
+
function getVercelStore(store) {
|
|
12
|
+
return {
|
|
13
|
+
users: store.collection("vercel.users", ["uid", "username"]),
|
|
14
|
+
teams: store.collection("vercel.teams", ["uid", "slug"]),
|
|
15
|
+
teamMembers: store.collection("vercel.team_members", ["teamId", "userId"]),
|
|
16
|
+
projects: store.collection("vercel.projects", ["uid", "name", "accountId"]),
|
|
17
|
+
deployments: store.collection("vercel.deployments", ["uid", "projectId", "url"]),
|
|
18
|
+
deploymentAliases: store.collection("vercel.deployment_aliases", ["deploymentId", "projectId"]),
|
|
19
|
+
builds: store.collection("vercel.builds", ["deploymentId"]),
|
|
20
|
+
deploymentEvents: store.collection("vercel.deployment_events", ["deploymentId"]),
|
|
21
|
+
files: store.collection("vercel.files", ["digest"]),
|
|
22
|
+
deploymentFiles: store.collection("vercel.deployment_files", ["deploymentId"]),
|
|
23
|
+
domains: store.collection("vercel.domains", ["projectId", "name"]),
|
|
24
|
+
envVars: store.collection("vercel.env_vars", ["projectId", "uid"]),
|
|
25
|
+
protectionBypasses: store.collection("vercel.protection_bypasses", ["projectId"]),
|
|
26
|
+
apiKeys: store.collection("vercel.api_keys", ["uid", "teamId", "userId"]),
|
|
27
|
+
integrations: store.collection("vercel.integrations", ["client_id"])
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function generateUid(prefix = "") {
|
|
31
|
+
const id = randomBytes(12).toString("base64url").slice(0, 20);
|
|
32
|
+
return prefix ? `${prefix}_${id}` : id;
|
|
33
|
+
}
|
|
34
|
+
function generateSecret() {
|
|
35
|
+
return randomBytes(32).toString("base64url");
|
|
36
|
+
}
|
|
37
|
+
function nowMs() {
|
|
38
|
+
return Date.now();
|
|
39
|
+
}
|
|
40
|
+
function resolveTeamScope(c, vs) {
|
|
41
|
+
const teamId = c.req.query("teamId");
|
|
42
|
+
const slug = c.req.query("slug");
|
|
43
|
+
if (teamId) {
|
|
44
|
+
const team = vs.teams.findOneBy("uid", teamId);
|
|
45
|
+
if (!team) return null;
|
|
46
|
+
return { accountId: team.uid, team };
|
|
47
|
+
}
|
|
48
|
+
if (slug) {
|
|
49
|
+
const team = vs.teams.findOneBy("slug", slug);
|
|
50
|
+
if (!team) return null;
|
|
51
|
+
return { accountId: team.uid, team };
|
|
52
|
+
}
|
|
53
|
+
const authUser = c.get("authUser");
|
|
54
|
+
if (!authUser) return null;
|
|
55
|
+
const user = vs.users.findOneBy("username", authUser.login);
|
|
56
|
+
if (!user) return null;
|
|
57
|
+
return { accountId: user.uid, team: null };
|
|
58
|
+
}
|
|
59
|
+
function lookupProject(vs, idOrName, accountId) {
|
|
60
|
+
let project = vs.projects.findOneBy("uid", idOrName);
|
|
61
|
+
if (project && project.accountId === accountId) return project;
|
|
62
|
+
const byName = vs.projects.findBy("name", idOrName);
|
|
63
|
+
return byName.find((p) => p.accountId === accountId);
|
|
64
|
+
}
|
|
65
|
+
function parseCursorPagination(c) {
|
|
66
|
+
return {
|
|
67
|
+
limit: Math.min(100, Math.max(1, parseInt(c.req.query("limit") ?? "20", 10) || 20)),
|
|
68
|
+
since: c.req.query("since") ? parseInt(c.req.query("since"), 10) : void 0,
|
|
69
|
+
until: c.req.query("until") ? parseInt(c.req.query("until"), 10) : void 0,
|
|
70
|
+
from: c.req.query("from") ? parseInt(c.req.query("from"), 10) : void 0
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function applyCursorPagination(items, pagination) {
|
|
74
|
+
let filtered = [...items].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
75
|
+
if (pagination.since !== void 0) {
|
|
76
|
+
filtered = filtered.filter((i) => new Date(i.created_at).getTime() > pagination.since);
|
|
77
|
+
}
|
|
78
|
+
if (pagination.until !== void 0) {
|
|
79
|
+
filtered = filtered.filter((i) => new Date(i.created_at).getTime() <= pagination.until);
|
|
80
|
+
}
|
|
81
|
+
const total = filtered.length;
|
|
82
|
+
const limited = filtered.slice(0, pagination.limit);
|
|
83
|
+
const hasNext = total > pagination.limit;
|
|
84
|
+
return {
|
|
85
|
+
items: limited,
|
|
86
|
+
pagination: {
|
|
87
|
+
count: limited.length,
|
|
88
|
+
next: hasNext && limited.length > 0 ? new Date(limited[limited.length - 1].created_at).getTime() : null,
|
|
89
|
+
prev: limited.length > 0 ? new Date(limited[0].created_at).getTime() : null
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function formatUser(user) {
|
|
94
|
+
return {
|
|
95
|
+
id: user.uid,
|
|
96
|
+
email: user.email,
|
|
97
|
+
name: user.name,
|
|
98
|
+
username: user.username,
|
|
99
|
+
avatar: user.avatar,
|
|
100
|
+
defaultTeamId: user.defaultTeamId,
|
|
101
|
+
version: user.version,
|
|
102
|
+
createdAt: new Date(user.created_at).getTime(),
|
|
103
|
+
softBlock: user.softBlock,
|
|
104
|
+
billing: user.billing,
|
|
105
|
+
resourceConfig: user.resourceConfig,
|
|
106
|
+
stagingPrefix: user.stagingPrefix
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function formatTeam(team) {
|
|
110
|
+
return {
|
|
111
|
+
id: team.uid,
|
|
112
|
+
slug: team.slug,
|
|
113
|
+
name: team.name,
|
|
114
|
+
avatar: team.avatar,
|
|
115
|
+
description: team.description,
|
|
116
|
+
creatorId: team.creatorId,
|
|
117
|
+
createdAt: new Date(team.created_at).getTime(),
|
|
118
|
+
updatedAt: new Date(team.updated_at).getTime(),
|
|
119
|
+
membership: team.membership,
|
|
120
|
+
billing: team.billing,
|
|
121
|
+
resourceConfig: team.resourceConfig,
|
|
122
|
+
stagingPrefix: team.stagingPrefix
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function formatProject(project, baseUrl) {
|
|
126
|
+
return {
|
|
127
|
+
accountId: project.accountId,
|
|
128
|
+
autoAssignCustomDomains: project.autoAssignCustomDomains,
|
|
129
|
+
autoAssignCustomDomainsUpdatedBy: project.autoAssignCustomDomainsUpdatedBy,
|
|
130
|
+
buildCommand: project.buildCommand,
|
|
131
|
+
createdAt: new Date(project.created_at).getTime(),
|
|
132
|
+
devCommand: project.devCommand,
|
|
133
|
+
directoryListing: false,
|
|
134
|
+
framework: project.framework,
|
|
135
|
+
gitForkProtection: project.gitForkProtection,
|
|
136
|
+
gitComments: project.gitComments,
|
|
137
|
+
id: project.uid,
|
|
138
|
+
installCommand: project.installCommand,
|
|
139
|
+
name: project.name,
|
|
140
|
+
nodeVersion: project.nodeVersion,
|
|
141
|
+
outputDirectory: project.outputDirectory,
|
|
142
|
+
publicSource: project.publicSource,
|
|
143
|
+
rootDirectory: project.rootDirectory,
|
|
144
|
+
commandForIgnoringBuildStep: project.commandForIgnoringBuildStep,
|
|
145
|
+
serverlessFunctionRegion: project.serverlessFunctionRegion,
|
|
146
|
+
sourceFilesOutsideRootDirectory: project.sourceFilesOutsideRootDirectory,
|
|
147
|
+
updatedAt: new Date(project.updated_at).getTime(),
|
|
148
|
+
live: project.live,
|
|
149
|
+
link: project.link,
|
|
150
|
+
latestDeployments: project.latestDeployments,
|
|
151
|
+
targets: project.targets,
|
|
152
|
+
protectionBypass: project.protectionBypass,
|
|
153
|
+
passwordProtection: project.passwordProtection,
|
|
154
|
+
ssoProtection: project.ssoProtection,
|
|
155
|
+
trustedIps: project.trustedIps,
|
|
156
|
+
connectConfigurationId: project.connectConfigurationId,
|
|
157
|
+
webAnalytics: project.webAnalytics,
|
|
158
|
+
speedInsights: project.speedInsights,
|
|
159
|
+
oidcTokenConfig: project.oidcTokenConfig,
|
|
160
|
+
tier: project.tier
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function formatDeployment(dep, vs, baseUrl) {
|
|
164
|
+
const project = vs.projects.findOneBy("uid", dep.projectId);
|
|
165
|
+
const creator = vs.users.findOneBy("uid", dep.creatorId);
|
|
166
|
+
const aliases = vs.deploymentAliases.findBy("deploymentId", dep.uid);
|
|
167
|
+
return {
|
|
168
|
+
uid: dep.uid,
|
|
169
|
+
id: dep.uid,
|
|
170
|
+
name: dep.name,
|
|
171
|
+
url: dep.url,
|
|
172
|
+
created: new Date(dep.created_at).getTime(),
|
|
173
|
+
createdAt: new Date(dep.created_at).getTime(),
|
|
174
|
+
source: dep.source,
|
|
175
|
+
state: dep.state,
|
|
176
|
+
readyState: dep.readyState,
|
|
177
|
+
readySubstate: dep.readySubstate,
|
|
178
|
+
type: "LAMBDAS",
|
|
179
|
+
creator: creator ? { uid: creator.uid, email: creator.email, username: creator.username } : null,
|
|
180
|
+
inspectorUrl: dep.inspectorUrl,
|
|
181
|
+
meta: dep.meta,
|
|
182
|
+
target: dep.target,
|
|
183
|
+
aliasAssigned: dep.aliasAssigned,
|
|
184
|
+
aliasError: dep.aliasError,
|
|
185
|
+
buildingAt: dep.buildingAt,
|
|
186
|
+
readyAt: dep.readyAt,
|
|
187
|
+
bootedAt: dep.bootedAt,
|
|
188
|
+
canceledAt: dep.canceledAt,
|
|
189
|
+
errorCode: dep.errorCode,
|
|
190
|
+
errorMessage: dep.errorMessage,
|
|
191
|
+
regions: dep.regions,
|
|
192
|
+
functions: dep.functions,
|
|
193
|
+
routes: dep.routes,
|
|
194
|
+
plan: dep.plan,
|
|
195
|
+
projectId: dep.projectId,
|
|
196
|
+
gitSource: dep.gitSource,
|
|
197
|
+
alias: aliases.map((a) => a.alias)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function formatDeploymentBrief(dep, vs) {
|
|
201
|
+
const creator = vs.users.findOneBy("uid", dep.creatorId);
|
|
202
|
+
return {
|
|
203
|
+
uid: dep.uid,
|
|
204
|
+
name: dep.name,
|
|
205
|
+
url: dep.url,
|
|
206
|
+
created: new Date(dep.created_at).getTime(),
|
|
207
|
+
state: dep.state,
|
|
208
|
+
readyState: dep.readyState,
|
|
209
|
+
type: "LAMBDAS",
|
|
210
|
+
creator: creator ? { uid: creator.uid, email: creator.email, username: creator.username } : null,
|
|
211
|
+
meta: dep.meta,
|
|
212
|
+
target: dep.target,
|
|
213
|
+
aliasAssigned: dep.aliasAssigned,
|
|
214
|
+
projectId: dep.projectId
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function formatDomain(domain) {
|
|
218
|
+
return {
|
|
219
|
+
name: domain.name,
|
|
220
|
+
apexName: domain.apexName,
|
|
221
|
+
projectId: domain.projectId,
|
|
222
|
+
redirect: domain.redirect,
|
|
223
|
+
redirectStatusCode: domain.redirectStatusCode,
|
|
224
|
+
gitBranch: domain.gitBranch,
|
|
225
|
+
customEnvironmentId: domain.customEnvironmentId,
|
|
226
|
+
updatedAt: new Date(domain.updated_at).getTime(),
|
|
227
|
+
createdAt: new Date(domain.created_at).getTime(),
|
|
228
|
+
verified: domain.verified,
|
|
229
|
+
verification: domain.verified ? [] : domain.verification
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function formatEnvVar(env, decrypt = false) {
|
|
233
|
+
return {
|
|
234
|
+
type: env.type,
|
|
235
|
+
id: env.uid,
|
|
236
|
+
key: env.key,
|
|
237
|
+
value: decrypt || env.type === "plain" ? env.value : "",
|
|
238
|
+
target: env.target,
|
|
239
|
+
gitBranch: env.gitBranch,
|
|
240
|
+
customEnvironmentIds: env.customEnvironmentIds,
|
|
241
|
+
configurationId: null,
|
|
242
|
+
createdAt: new Date(env.created_at).getTime(),
|
|
243
|
+
updatedAt: new Date(env.updated_at).getTime(),
|
|
244
|
+
createdBy: null,
|
|
245
|
+
updatedBy: null,
|
|
246
|
+
comment: env.comment ?? ""
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function createErrorHandler(documentationUrl) {
|
|
250
|
+
return async (c, next) => {
|
|
251
|
+
if (documentationUrl) {
|
|
252
|
+
c.set("docsUrl", documentationUrl);
|
|
253
|
+
}
|
|
254
|
+
await next();
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
var errorHandler = createErrorHandler();
|
|
258
|
+
var ApiError = class extends Error {
|
|
259
|
+
constructor(status, message, errors) {
|
|
260
|
+
super(message);
|
|
261
|
+
this.status = status;
|
|
262
|
+
this.errors = errors;
|
|
263
|
+
this.name = "ApiError";
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
async function parseJsonBody(c) {
|
|
267
|
+
try {
|
|
268
|
+
const body = await c.req.json();
|
|
269
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
270
|
+
return body;
|
|
271
|
+
}
|
|
272
|
+
return {};
|
|
273
|
+
} catch {
|
|
274
|
+
throw new ApiError(400, "Problems parsing JSON");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
|
|
278
|
+
function debug(label, ...args) {
|
|
279
|
+
if (isDebug) {
|
|
280
|
+
console.log(`[${label}]`, ...args);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
284
|
+
var FONTS = {
|
|
285
|
+
"geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
|
|
286
|
+
"GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
|
|
287
|
+
};
|
|
288
|
+
function escapeHtml(s) {
|
|
289
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
290
|
+
}
|
|
291
|
+
function escapeAttr(s) {
|
|
292
|
+
return escapeHtml(s).replace(/'/g, "'");
|
|
293
|
+
}
|
|
294
|
+
var CSS = `
|
|
295
|
+
@font-face{
|
|
296
|
+
font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
|
|
297
|
+
src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
|
|
298
|
+
}
|
|
299
|
+
@font-face{
|
|
300
|
+
font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
|
|
301
|
+
src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
|
|
302
|
+
}
|
|
303
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
304
|
+
body{
|
|
305
|
+
font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
|
|
306
|
+
background:#000;color:#33ff00;min-height:100vh;
|
|
307
|
+
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
|
308
|
+
}
|
|
309
|
+
.emu-bar{
|
|
310
|
+
border-bottom:1px solid #0a3300;padding:10px 20px;
|
|
311
|
+
display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
|
|
312
|
+
}
|
|
313
|
+
.emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
|
|
314
|
+
.emu-bar-links{margin-left:auto;display:flex;gap:16px;}
|
|
315
|
+
.emu-bar-links a{
|
|
316
|
+
color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
|
|
317
|
+
}
|
|
318
|
+
.emu-bar-links a:hover{color:#33ff00;}
|
|
319
|
+
.emu-bar-links a .full{display:inline;}
|
|
320
|
+
.emu-bar-links a .short{display:none;}
|
|
321
|
+
@media(max-width:600px){
|
|
322
|
+
.emu-bar-links a .full{display:none;}
|
|
323
|
+
.emu-bar-links a .short{display:inline;}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.content{
|
|
327
|
+
display:flex;align-items:center;justify-content:center;
|
|
328
|
+
min-height:calc(100vh - 42px);padding:24px 16px;
|
|
329
|
+
}
|
|
330
|
+
.content-inner{width:100%;max-width:420px;}
|
|
331
|
+
.card-title{
|
|
332
|
+
font-family:'Geist Pixel',monospace;
|
|
333
|
+
font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
|
|
334
|
+
}
|
|
335
|
+
.card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
|
|
336
|
+
.powered-by{
|
|
337
|
+
position:fixed;bottom:0;left:0;right:0;
|
|
338
|
+
text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
|
|
339
|
+
font-family:'Geist Pixel',monospace;
|
|
340
|
+
}
|
|
341
|
+
.powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
|
|
342
|
+
.powered-by a:hover{color:#33ff00;}
|
|
343
|
+
|
|
344
|
+
.error-title{
|
|
345
|
+
font-family:'Geist Pixel',monospace;
|
|
346
|
+
color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
|
|
347
|
+
}
|
|
348
|
+
.error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
|
|
349
|
+
.error-card{text-align:center;}
|
|
350
|
+
|
|
351
|
+
.user-form{margin-bottom:8px;}
|
|
352
|
+
.user-form:last-of-type{margin-bottom:0;}
|
|
353
|
+
.user-btn{
|
|
354
|
+
width:100%;display:flex;align-items:center;gap:12px;
|
|
355
|
+
padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
|
|
356
|
+
background:#000;color:inherit;cursor:pointer;text-align:left;
|
|
357
|
+
font:inherit;transition:border-color .15s;
|
|
358
|
+
}
|
|
359
|
+
.user-btn:hover{border-color:#33ff00;}
|
|
360
|
+
.avatar{
|
|
361
|
+
width:36px;height:36px;border-radius:50%;
|
|
362
|
+
background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
|
|
363
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
364
|
+
font-family:'Geist Pixel',monospace;
|
|
365
|
+
}
|
|
366
|
+
.user-text{min-width:0;}
|
|
367
|
+
.user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
|
|
368
|
+
.user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
|
|
369
|
+
.user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
|
|
370
|
+
|
|
371
|
+
.settings-layout{
|
|
372
|
+
max-width:920px;margin:0 auto;padding:28px 20px;
|
|
373
|
+
display:flex;gap:28px;
|
|
374
|
+
}
|
|
375
|
+
.settings-sidebar{width:200px;flex-shrink:0;}
|
|
376
|
+
.settings-sidebar a{
|
|
377
|
+
display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
|
|
378
|
+
text-decoration:none;font-size:.8125rem;transition:color .15s;
|
|
379
|
+
}
|
|
380
|
+
.settings-sidebar a:hover{color:#33ff00;}
|
|
381
|
+
.settings-sidebar a.active{color:#33ff00;font-weight:600;}
|
|
382
|
+
.settings-main{flex:1;min-width:0;}
|
|
383
|
+
|
|
384
|
+
.s-card{
|
|
385
|
+
padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
|
|
386
|
+
}
|
|
387
|
+
.s-card:last-child{border-bottom:none;}
|
|
388
|
+
.s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
|
|
389
|
+
.s-icon{
|
|
390
|
+
width:42px;height:42px;border-radius:8px;
|
|
391
|
+
background:#0a3300;display:flex;align-items:center;justify-content:center;
|
|
392
|
+
font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
393
|
+
font-family:'Geist Pixel',monospace;
|
|
394
|
+
}
|
|
395
|
+
.s-title{
|
|
396
|
+
font-family:'Geist Pixel',monospace;
|
|
397
|
+
font-size:1.25rem;font-weight:600;color:#33ff00;
|
|
398
|
+
}
|
|
399
|
+
.s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
400
|
+
.section-heading{
|
|
401
|
+
font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
|
|
402
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
403
|
+
}
|
|
404
|
+
.perm-list{list-style:none;}
|
|
405
|
+
.perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
|
|
406
|
+
.check{color:#33ff00;}
|
|
407
|
+
.org-row{
|
|
408
|
+
display:flex;align-items:center;gap:8px;padding:7px 0;
|
|
409
|
+
border-bottom:1px solid #0a3300;font-size:.8125rem;
|
|
410
|
+
}
|
|
411
|
+
.org-row:last-child{border-bottom:none;}
|
|
412
|
+
.org-icon{
|
|
413
|
+
width:22px;height:22px;border-radius:4px;background:#0a3300;
|
|
414
|
+
display:flex;align-items:center;justify-content:center;
|
|
415
|
+
font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
416
|
+
font-family:'Geist Pixel',monospace;
|
|
417
|
+
}
|
|
418
|
+
.org-name{font-weight:600;color:#33ff00;}
|
|
419
|
+
.badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
|
|
420
|
+
.badge-granted{background:#0a3300;color:#33ff00;}
|
|
421
|
+
.badge-denied{background:#1a0a0a;color:#ff4444;}
|
|
422
|
+
.badge-requested{background:#0a3300;color:#1a8c00;}
|
|
423
|
+
.btn-revoke{
|
|
424
|
+
display:inline-block;padding:5px 14px;border-radius:6px;
|
|
425
|
+
border:1px solid #0a3300;background:transparent;color:#ff4444;
|
|
426
|
+
font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
|
|
427
|
+
}
|
|
428
|
+
.btn-revoke:hover{border-color:#ff4444;}
|
|
429
|
+
.info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
|
|
430
|
+
.app-link{
|
|
431
|
+
display:flex;align-items:center;gap:12px;padding:12px;
|
|
432
|
+
border:1px solid #0a3300;border-radius:8px;background:#000;
|
|
433
|
+
text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
|
|
434
|
+
}
|
|
435
|
+
.app-link:hover{border-color:#33ff00;}
|
|
436
|
+
.app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
|
|
437
|
+
.app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
|
|
438
|
+
.empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
|
|
439
|
+
`;
|
|
440
|
+
var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
|
|
441
|
+
function emuBar(service) {
|
|
442
|
+
const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
|
|
443
|
+
return `<div class="emu-bar">
|
|
444
|
+
<span class="emu-bar-title">${title}</span>
|
|
445
|
+
<nav class="emu-bar-links">
|
|
446
|
+
<a href="https://github.com/vercel-labs/emulate/issues" target="_blank" rel="noopener"><span class="full">Report Issue</span><span class="short">Report</span></a>
|
|
447
|
+
<a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
|
|
448
|
+
<a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
|
|
449
|
+
</nav>
|
|
450
|
+
</div>`;
|
|
451
|
+
}
|
|
452
|
+
function head(title) {
|
|
453
|
+
return `<!DOCTYPE html>
|
|
454
|
+
<html lang="en">
|
|
455
|
+
<head>
|
|
456
|
+
<meta charset="utf-8"/>
|
|
457
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
458
|
+
<title>${escapeHtml(title)} | emulate</title>
|
|
459
|
+
<style>${CSS}</style>
|
|
460
|
+
</head>`;
|
|
461
|
+
}
|
|
462
|
+
function renderCardPage(title, subtitle, body, service) {
|
|
463
|
+
return `${head(title)}
|
|
464
|
+
<body>
|
|
465
|
+
${emuBar(service)}
|
|
466
|
+
<div class="content">
|
|
467
|
+
<div class="content-inner">
|
|
468
|
+
<div class="card-title">${escapeHtml(title)}</div>
|
|
469
|
+
<div class="card-subtitle">${subtitle}</div>
|
|
470
|
+
${body}
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
${POWERED_BY}
|
|
474
|
+
</body></html>`;
|
|
475
|
+
}
|
|
476
|
+
function renderErrorPage(title, message, service) {
|
|
477
|
+
return `${head(title)}
|
|
478
|
+
<body>
|
|
479
|
+
${emuBar(service)}
|
|
480
|
+
<div class="content">
|
|
481
|
+
<div class="content-inner error-card">
|
|
482
|
+
<div class="error-title">${escapeHtml(title)}</div>
|
|
483
|
+
<div class="error-msg">${escapeHtml(message)}</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
${POWERED_BY}
|
|
487
|
+
</body></html>`;
|
|
488
|
+
}
|
|
489
|
+
function renderUserButton(opts) {
|
|
490
|
+
const hiddens = Object.entries(opts.hiddenFields).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("");
|
|
491
|
+
const nameLine = opts.name ? `<div class="user-meta">${escapeHtml(opts.name)}</div>` : "";
|
|
492
|
+
const emailLine = opts.email ? `<div class="user-email">${escapeHtml(opts.email)}</div>` : "";
|
|
493
|
+
return `<form class="user-form" method="post" action="${escapeAttr(opts.formAction)}">
|
|
494
|
+
${hiddens}
|
|
495
|
+
<button type="submit" class="user-btn">
|
|
496
|
+
<span class="avatar">${escapeHtml(opts.letter)}</span>
|
|
497
|
+
<span class="user-text">
|
|
498
|
+
<span class="user-login">${escapeHtml(opts.login)}</span>
|
|
499
|
+
${nameLine}${emailLine}
|
|
500
|
+
</span>
|
|
501
|
+
</button>
|
|
502
|
+
</form>`;
|
|
503
|
+
}
|
|
504
|
+
function normalizeUri(uri) {
|
|
505
|
+
try {
|
|
506
|
+
const u = new URL(uri);
|
|
507
|
+
return `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
|
|
508
|
+
} catch {
|
|
509
|
+
return uri.replace(/\/+$/, "").split("?")[0];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function matchesRedirectUri(incoming, registered) {
|
|
513
|
+
const normalized = normalizeUri(incoming);
|
|
514
|
+
return registered.some((r) => normalizeUri(r) === normalized);
|
|
515
|
+
}
|
|
516
|
+
function constantTimeSecretEqual(a, b) {
|
|
517
|
+
const bufA = Buffer.from(a, "utf-8");
|
|
518
|
+
const bufB = Buffer.from(b, "utf-8");
|
|
519
|
+
if (bufA.length !== bufB.length) return false;
|
|
520
|
+
return timingSafeEqual(bufA, bufB);
|
|
521
|
+
}
|
|
522
|
+
function bodyStr(v) {
|
|
523
|
+
if (typeof v === "string") return v;
|
|
524
|
+
if (Array.isArray(v) && typeof v[0] === "string") return v[0];
|
|
525
|
+
return "";
|
|
526
|
+
}
|
|
527
|
+
function vercelErr(c, status, code, message) {
|
|
528
|
+
return c.json({ error: { code, message } }, status);
|
|
529
|
+
}
|
|
530
|
+
function resolveTeamByIdOrSlug(vs, teamIdOrSlug) {
|
|
531
|
+
return vs.teams.findOneBy("uid", teamIdOrSlug) ?? vs.teams.findOneBy("slug", teamIdOrSlug);
|
|
532
|
+
}
|
|
533
|
+
function getTeamMember(vs, teamUid, userUid) {
|
|
534
|
+
return vs.teamMembers.findBy("teamId", teamUid).find((m) => m.userId === userUid);
|
|
535
|
+
}
|
|
536
|
+
function formatTeamForViewer(team, member) {
|
|
537
|
+
return {
|
|
538
|
+
...formatTeam(team),
|
|
539
|
+
membership: member ? { confirmed: member.confirmed, role: member.role } : { confirmed: false, role: "VIEWER" }
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function formatMemberRow(vs, m) {
|
|
543
|
+
const user = vs.users.findOneBy("uid", m.userId);
|
|
544
|
+
return {
|
|
545
|
+
id: String(m.id),
|
|
546
|
+
role: m.role,
|
|
547
|
+
confirmed: m.confirmed,
|
|
548
|
+
joinedFrom: m.joinedFrom,
|
|
549
|
+
user: user ? formatUser(user) : null
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
var TEAM_ROLES = ["OWNER", "MEMBER", "DEVELOPER", "VIEWER"];
|
|
553
|
+
function parseRole(value, fallback) {
|
|
554
|
+
if (value === void 0 || value === null) return fallback;
|
|
555
|
+
if (typeof value !== "string") return null;
|
|
556
|
+
return TEAM_ROLES.includes(value) ? value : null;
|
|
557
|
+
}
|
|
558
|
+
function defaultTeamBilling() {
|
|
559
|
+
return { plan: "hobby", period: null, trial: null, cancelation: null, addons: null };
|
|
560
|
+
}
|
|
561
|
+
function defaultTeamResourceConfig() {
|
|
562
|
+
return { nodeType: "standard", concurrentBuilds: 1 };
|
|
563
|
+
}
|
|
564
|
+
function userRoutes({ app, store }) {
|
|
565
|
+
const vs = getVercelStore(store);
|
|
566
|
+
app.get("/registration", (c) => c.json({ registration: false }));
|
|
567
|
+
app.get("/v2/user", (c) => {
|
|
568
|
+
const auth = c.get("authUser");
|
|
569
|
+
if (!auth) {
|
|
570
|
+
return vercelErr(c, 401, "not_authenticated", "Authentication required");
|
|
571
|
+
}
|
|
572
|
+
const user = vs.users.findOneBy("username", auth.login);
|
|
573
|
+
if (!user) {
|
|
574
|
+
return vercelErr(c, 403, "forbidden", "User not found");
|
|
575
|
+
}
|
|
576
|
+
return c.json({ user: formatUser(user) });
|
|
577
|
+
});
|
|
578
|
+
app.patch("/v2/user", async (c) => {
|
|
579
|
+
const auth = c.get("authUser");
|
|
580
|
+
if (!auth) {
|
|
581
|
+
return vercelErr(c, 401, "not_authenticated", "Authentication required");
|
|
582
|
+
}
|
|
583
|
+
const existing = vs.users.findOneBy("username", auth.login);
|
|
584
|
+
if (!existing) {
|
|
585
|
+
return vercelErr(c, 403, "forbidden", "User not found");
|
|
586
|
+
}
|
|
587
|
+
const body = await parseJsonBody(c);
|
|
588
|
+
const patch = {};
|
|
589
|
+
if ("name" in body) {
|
|
590
|
+
if (body.name === null) patch.name = null;
|
|
591
|
+
else if (typeof body.name === "string") patch.name = body.name;
|
|
592
|
+
}
|
|
593
|
+
if ("email" in body && typeof body.email === "string") {
|
|
594
|
+
patch.email = body.email;
|
|
595
|
+
}
|
|
596
|
+
const updated = vs.users.update(existing.id, patch);
|
|
597
|
+
if (!updated) {
|
|
598
|
+
return vercelErr(c, 500, "internal_error", "Failed to update user");
|
|
599
|
+
}
|
|
600
|
+
return c.json({ user: formatUser(updated) });
|
|
601
|
+
});
|
|
602
|
+
app.get("/v2/teams", (c) => {
|
|
603
|
+
const auth = c.get("authUser");
|
|
604
|
+
if (!auth) {
|
|
605
|
+
return vercelErr(c, 401, "not_authenticated", "Authentication required");
|
|
606
|
+
}
|
|
607
|
+
const user = vs.users.findOneBy("username", auth.login);
|
|
608
|
+
if (!user) {
|
|
609
|
+
return vercelErr(c, 403, "forbidden", "User not found");
|
|
610
|
+
}
|
|
611
|
+
const pagination = parseCursorPagination(c);
|
|
612
|
+
const memberships = vs.teamMembers.findBy("userId", user.uid);
|
|
613
|
+
let teams = memberships.map((m) => vs.teams.findOneBy("uid", m.teamId)).filter((t) => Boolean(t));
|
|
614
|
+
if (c.req.query("teamId") || c.req.query("slug")) {
|
|
615
|
+
const scope = resolveTeamScope(c, vs);
|
|
616
|
+
const scopedTeam = scope?.team;
|
|
617
|
+
if (!scopedTeam) {
|
|
618
|
+
return vercelErr(c, 404, "not_found", "Team not found");
|
|
619
|
+
}
|
|
620
|
+
teams = teams.filter((t) => t.uid === scopedTeam.uid);
|
|
621
|
+
}
|
|
622
|
+
const { items, pagination: pageMeta } = applyCursorPagination(teams, pagination);
|
|
623
|
+
const formatted = items.map((team) => {
|
|
624
|
+
const member = getTeamMember(vs, team.uid, user.uid);
|
|
625
|
+
return formatTeamForViewer(team, member);
|
|
626
|
+
});
|
|
627
|
+
return c.json({
|
|
628
|
+
teams: formatted,
|
|
629
|
+
pagination: pageMeta
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
app.get("/v2/teams/:teamId", (c) => {
|
|
633
|
+
const auth = c.get("authUser");
|
|
634
|
+
if (!auth) {
|
|
635
|
+
return vercelErr(c, 401, "not_authenticated", "Authentication required");
|
|
636
|
+
}
|
|
637
|
+
const user = vs.users.findOneBy("username", auth.login);
|
|
638
|
+
if (!user) {
|
|
639
|
+
return vercelErr(c, 403, "forbidden", "User not found");
|
|
640
|
+
}
|
|
641
|
+
const team = resolveTeamByIdOrSlug(vs, c.req.param("teamId"));
|
|
642
|
+
if (!team) {
|
|
643
|
+
return vercelErr(c, 404, "not_found", "Team not found");
|
|
644
|
+
}
|
|
645
|
+
const member = getTeamMember(vs, team.uid, user.uid);
|
|
646
|
+
return c.json({ team: formatTeamForViewer(team, member) });
|
|
647
|
+
});
|
|
648
|
+
app.post("/v2/teams", async (c) => {
|
|
649
|
+
const auth = c.get("authUser");
|
|
650
|
+
if (!auth) {
|
|
651
|
+
return vercelErr(c, 401, "not_authenticated", "Authentication required");
|
|
652
|
+
}
|
|
653
|
+
const creator = vs.users.findOneBy("username", auth.login);
|
|
654
|
+
if (!creator) {
|
|
655
|
+
return vercelErr(c, 403, "forbidden", "User not found");
|
|
656
|
+
}
|
|
657
|
+
const body = await parseJsonBody(c);
|
|
658
|
+
const slug = typeof body.slug === "string" ? body.slug.trim() : "";
|
|
659
|
+
if (!slug) {
|
|
660
|
+
return vercelErr(c, 400, "bad_request", "Missing required field: slug");
|
|
661
|
+
}
|
|
662
|
+
if (vs.teams.findOneBy("slug", slug)) {
|
|
663
|
+
return vercelErr(c, 409, "team_slug_already_exists", "A team with this slug already exists");
|
|
664
|
+
}
|
|
665
|
+
const name = typeof body.name === "string" && body.name.trim() ? body.name.trim() : slug;
|
|
666
|
+
const team = vs.teams.insert({
|
|
667
|
+
uid: generateUid("team"),
|
|
668
|
+
slug,
|
|
669
|
+
name,
|
|
670
|
+
avatar: null,
|
|
671
|
+
description: null,
|
|
672
|
+
creatorId: creator.uid,
|
|
673
|
+
membership: { confirmed: true, role: "OWNER" },
|
|
674
|
+
billing: defaultTeamBilling(),
|
|
675
|
+
resourceConfig: defaultTeamResourceConfig(),
|
|
676
|
+
stagingPrefix: ""
|
|
677
|
+
});
|
|
678
|
+
vs.teamMembers.insert({
|
|
679
|
+
teamId: team.uid,
|
|
680
|
+
userId: creator.uid,
|
|
681
|
+
role: "OWNER",
|
|
682
|
+
confirmed: true,
|
|
683
|
+
joinedFrom: "cli"
|
|
684
|
+
});
|
|
685
|
+
const member = getTeamMember(vs, team.uid, creator.uid);
|
|
686
|
+
return c.json({ team: formatTeamForViewer(team, member) });
|
|
687
|
+
});
|
|
688
|
+
app.patch("/v2/teams/:teamId", async (c) => {
|
|
689
|
+
const auth = c.get("authUser");
|
|
690
|
+
if (!auth) {
|
|
691
|
+
return vercelErr(c, 401, "not_authenticated", "Authentication required");
|
|
692
|
+
}
|
|
693
|
+
const user = vs.users.findOneBy("username", auth.login);
|
|
694
|
+
if (!user) {
|
|
695
|
+
return vercelErr(c, 403, "forbidden", "User not found");
|
|
696
|
+
}
|
|
697
|
+
const team = resolveTeamByIdOrSlug(vs, c.req.param("teamId"));
|
|
698
|
+
if (!team) {
|
|
699
|
+
return vercelErr(c, 404, "not_found", "Team not found");
|
|
700
|
+
}
|
|
701
|
+
const member = getTeamMember(vs, team.uid, user.uid);
|
|
702
|
+
if (!member || member.role !== "OWNER") {
|
|
703
|
+
return vercelErr(c, 403, "forbidden", "Insufficient permissions to update this team");
|
|
704
|
+
}
|
|
705
|
+
const body = await parseJsonBody(c);
|
|
706
|
+
const patch = {};
|
|
707
|
+
if ("name" in body && typeof body.name === "string") patch.name = body.name;
|
|
708
|
+
if ("description" in body) {
|
|
709
|
+
if (body.description === null) patch.description = null;
|
|
710
|
+
else if (typeof body.description === "string") patch.description = body.description;
|
|
711
|
+
}
|
|
712
|
+
if ("slug" in body && typeof body.slug === "string") {
|
|
713
|
+
const nextSlug = body.slug.trim();
|
|
714
|
+
if (nextSlug && nextSlug !== team.slug) {
|
|
715
|
+
const taken = vs.teams.findOneBy("slug", nextSlug);
|
|
716
|
+
if (taken && taken.id !== team.id) {
|
|
717
|
+
return vercelErr(c, 409, "team_slug_already_exists", "A team with this slug already exists");
|
|
718
|
+
}
|
|
719
|
+
patch.slug = nextSlug;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const updated = vs.teams.update(team.id, patch);
|
|
723
|
+
if (!updated) {
|
|
724
|
+
return vercelErr(c, 500, "internal_error", "Failed to update team");
|
|
725
|
+
}
|
|
726
|
+
const viewer = getTeamMember(vs, updated.uid, user.uid);
|
|
727
|
+
return c.json({ team: formatTeamForViewer(updated, viewer) });
|
|
728
|
+
});
|
|
729
|
+
app.get("/v2/teams/:teamId/members", (c) => {
|
|
730
|
+
const auth = c.get("authUser");
|
|
731
|
+
if (!auth) {
|
|
732
|
+
return vercelErr(c, 401, "not_authenticated", "Authentication required");
|
|
733
|
+
}
|
|
734
|
+
const user = vs.users.findOneBy("username", auth.login);
|
|
735
|
+
if (!user) {
|
|
736
|
+
return vercelErr(c, 403, "forbidden", "User not found");
|
|
737
|
+
}
|
|
738
|
+
const team = resolveTeamByIdOrSlug(vs, c.req.param("teamId"));
|
|
739
|
+
if (!team) {
|
|
740
|
+
return vercelErr(c, 404, "not_found", "Team not found");
|
|
741
|
+
}
|
|
742
|
+
if (!getTeamMember(vs, team.uid, user.uid)) {
|
|
743
|
+
return vercelErr(c, 403, "forbidden", "Not a member of this team");
|
|
744
|
+
}
|
|
745
|
+
const pagination = parseCursorPagination(c);
|
|
746
|
+
const members = vs.teamMembers.findBy("teamId", team.uid);
|
|
747
|
+
const { items, pagination: pageMeta } = applyCursorPagination(members, pagination);
|
|
748
|
+
return c.json({
|
|
749
|
+
members: items.map((m) => formatMemberRow(vs, m)),
|
|
750
|
+
pagination: pageMeta
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
app.post("/v2/teams/:teamId/members", async (c) => {
|
|
754
|
+
const auth = c.get("authUser");
|
|
755
|
+
if (!auth) {
|
|
756
|
+
return vercelErr(c, 401, "not_authenticated", "Authentication required");
|
|
757
|
+
}
|
|
758
|
+
const actor = vs.users.findOneBy("username", auth.login);
|
|
759
|
+
if (!actor) {
|
|
760
|
+
return vercelErr(c, 403, "forbidden", "User not found");
|
|
761
|
+
}
|
|
762
|
+
const team = resolveTeamByIdOrSlug(vs, c.req.param("teamId"));
|
|
763
|
+
if (!team) {
|
|
764
|
+
return vercelErr(c, 404, "not_found", "Team not found");
|
|
765
|
+
}
|
|
766
|
+
const actorMember = getTeamMember(vs, team.uid, actor.uid);
|
|
767
|
+
if (!actorMember || actorMember.role !== "OWNER") {
|
|
768
|
+
return vercelErr(c, 403, "forbidden", "Insufficient permissions to add members");
|
|
769
|
+
}
|
|
770
|
+
const body = await parseJsonBody(c);
|
|
771
|
+
const email = typeof body.email === "string" ? body.email.trim() : void 0;
|
|
772
|
+
const uid = typeof body.uid === "string" ? body.uid.trim() : void 0;
|
|
773
|
+
let target;
|
|
774
|
+
if (uid) {
|
|
775
|
+
target = vs.users.findOneBy("uid", uid);
|
|
776
|
+
} else if (email) {
|
|
777
|
+
target = vs.users.findOneBy("email", email);
|
|
778
|
+
} else {
|
|
779
|
+
return vercelErr(c, 400, "bad_request", "Provide uid or email");
|
|
780
|
+
}
|
|
781
|
+
if (!target) {
|
|
782
|
+
return vercelErr(c, 404, "not_found", "User not found");
|
|
783
|
+
}
|
|
784
|
+
const role = parseRole(body.role, "MEMBER");
|
|
785
|
+
if (role === null) {
|
|
786
|
+
return vercelErr(c, 400, "bad_request", "Invalid role");
|
|
787
|
+
}
|
|
788
|
+
if (getTeamMember(vs, team.uid, target.uid)) {
|
|
789
|
+
return vercelErr(c, 409, "member_already_exists", "User is already a member of this team");
|
|
790
|
+
}
|
|
791
|
+
const row = vs.teamMembers.insert({
|
|
792
|
+
teamId: team.uid,
|
|
793
|
+
userId: target.uid,
|
|
794
|
+
role,
|
|
795
|
+
confirmed: true,
|
|
796
|
+
joinedFrom: email ? "email" : "invite"
|
|
797
|
+
});
|
|
798
|
+
return c.json({ member: formatMemberRow(vs, row) });
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
function vercelErr2(c, status, code, message) {
|
|
802
|
+
return c.json({ error: { code, message } }, status);
|
|
803
|
+
}
|
|
804
|
+
function parseGitLink(body) {
|
|
805
|
+
const gr = body.gitRepository;
|
|
806
|
+
if (!gr || typeof gr !== "object") return null;
|
|
807
|
+
const g = gr;
|
|
808
|
+
const repo = typeof g.repo === "string" ? g.repo : "";
|
|
809
|
+
if (!repo) return null;
|
|
810
|
+
const t = nowMs();
|
|
811
|
+
return {
|
|
812
|
+
type: typeof g.type === "string" ? g.type : "github",
|
|
813
|
+
repo,
|
|
814
|
+
repoId: typeof g.repoId === "number" ? g.repoId : 0,
|
|
815
|
+
org: typeof g.org === "string" ? g.org : "",
|
|
816
|
+
gitCredentialId: typeof g.gitCredentialId === "string" ? g.gitCredentialId : "",
|
|
817
|
+
productionBranch: typeof g.productionBranch === "string" ? g.productionBranch : "main",
|
|
818
|
+
createdAt: t,
|
|
819
|
+
updatedAt: t,
|
|
820
|
+
deployHooks: []
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function deleteProjectCascade(vs, project) {
|
|
824
|
+
const projectUid = project.uid;
|
|
825
|
+
const deps = vs.deployments.findBy("projectId", projectUid);
|
|
826
|
+
for (const dep of deps) {
|
|
827
|
+
for (const b of vs.builds.findBy("deploymentId", dep.uid)) {
|
|
828
|
+
vs.builds.delete(b.id);
|
|
829
|
+
}
|
|
830
|
+
for (const e of vs.deploymentEvents.findBy("deploymentId", dep.uid)) {
|
|
831
|
+
vs.deploymentEvents.delete(e.id);
|
|
832
|
+
}
|
|
833
|
+
for (const f of vs.deploymentFiles.findBy("deploymentId", dep.uid)) {
|
|
834
|
+
vs.deploymentFiles.delete(f.id);
|
|
835
|
+
}
|
|
836
|
+
for (const a of vs.deploymentAliases.findBy("deploymentId", dep.uid)) {
|
|
837
|
+
vs.deploymentAliases.delete(a.id);
|
|
838
|
+
}
|
|
839
|
+
vs.deployments.delete(dep.id);
|
|
840
|
+
}
|
|
841
|
+
for (const d of vs.domains.findBy("projectId", projectUid)) {
|
|
842
|
+
vs.domains.delete(d.id);
|
|
843
|
+
}
|
|
844
|
+
for (const ev of vs.envVars.findBy("projectId", projectUid)) {
|
|
845
|
+
vs.envVars.delete(ev.id);
|
|
846
|
+
}
|
|
847
|
+
for (const pb of vs.protectionBypasses.findBy("projectId", projectUid)) {
|
|
848
|
+
vs.protectionBypasses.delete(pb.id);
|
|
849
|
+
}
|
|
850
|
+
vs.projects.delete(project.id);
|
|
851
|
+
}
|
|
852
|
+
function protectionMetaForRow(row) {
|
|
853
|
+
return {
|
|
854
|
+
createdAt: new Date(row.created_at).getTime(),
|
|
855
|
+
createdBy: row.createdBy,
|
|
856
|
+
scope: row.scope
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function syncProtectionRecordFromCollection(vs, project) {
|
|
860
|
+
const rows = vs.protectionBypasses.findBy("projectId", project.uid);
|
|
861
|
+
const record = {};
|
|
862
|
+
for (const row of rows) {
|
|
863
|
+
record[row.secret] = protectionMetaForRow(row);
|
|
864
|
+
}
|
|
865
|
+
const updated = vs.projects.update(project.id, { protectionBypass: record });
|
|
866
|
+
return updated ?? { ...project, protectionBypass: record };
|
|
867
|
+
}
|
|
868
|
+
function projectsRoutes({ app, store, baseUrl }) {
|
|
869
|
+
const vs = getVercelStore(store);
|
|
870
|
+
app.post("/v11/projects", async (c) => {
|
|
871
|
+
const auth = c.get("authUser");
|
|
872
|
+
if (!auth) {
|
|
873
|
+
return vercelErr2(c, 401, "not_authenticated", "Authentication required");
|
|
874
|
+
}
|
|
875
|
+
const scope = resolveTeamScope(c, vs);
|
|
876
|
+
if (!scope) {
|
|
877
|
+
return vercelErr2(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
878
|
+
}
|
|
879
|
+
const body = await parseJsonBody(c);
|
|
880
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
881
|
+
if (!name) {
|
|
882
|
+
return vercelErr2(c, 400, "bad_request", "Missing required field: name");
|
|
883
|
+
}
|
|
884
|
+
const existing = vs.projects.findBy("name", name).filter((p) => p.accountId === scope.accountId);
|
|
885
|
+
if (existing.length > 0) {
|
|
886
|
+
return vercelErr2(c, 409, "project_already_exists", "A project with this name already exists");
|
|
887
|
+
}
|
|
888
|
+
const link = parseGitLink(body);
|
|
889
|
+
const project = vs.projects.insert({
|
|
890
|
+
uid: generateUid("prj"),
|
|
891
|
+
name,
|
|
892
|
+
accountId: scope.accountId,
|
|
893
|
+
framework: typeof body.framework === "string" ? body.framework : null,
|
|
894
|
+
buildCommand: typeof body.buildCommand === "string" ? body.buildCommand : null,
|
|
895
|
+
devCommand: typeof body.devCommand === "string" ? body.devCommand : null,
|
|
896
|
+
installCommand: typeof body.installCommand === "string" ? body.installCommand : null,
|
|
897
|
+
outputDirectory: typeof body.outputDirectory === "string" ? body.outputDirectory : null,
|
|
898
|
+
rootDirectory: typeof body.rootDirectory === "string" ? body.rootDirectory : null,
|
|
899
|
+
commandForIgnoringBuildStep: null,
|
|
900
|
+
nodeVersion: typeof body.nodeVersion === "string" ? body.nodeVersion : "20.x",
|
|
901
|
+
serverlessFunctionRegion: typeof body.serverlessFunctionRegion === "string" ? body.serverlessFunctionRegion : null,
|
|
902
|
+
publicSource: typeof body.publicSource === "boolean" ? body.publicSource : false,
|
|
903
|
+
autoAssignCustomDomains: true,
|
|
904
|
+
autoAssignCustomDomainsUpdatedBy: null,
|
|
905
|
+
gitForkProtection: true,
|
|
906
|
+
sourceFilesOutsideRootDirectory: false,
|
|
907
|
+
live: true,
|
|
908
|
+
link,
|
|
909
|
+
latestDeployments: [],
|
|
910
|
+
targets: {},
|
|
911
|
+
protectionBypass: {},
|
|
912
|
+
passwordProtection: null,
|
|
913
|
+
ssoProtection: null,
|
|
914
|
+
trustedIps: null,
|
|
915
|
+
connectConfigurationId: null,
|
|
916
|
+
gitComments: { onPullRequest: true, onCommit: false },
|
|
917
|
+
webAnalytics: null,
|
|
918
|
+
speedInsights: null,
|
|
919
|
+
oidcTokenConfig: null,
|
|
920
|
+
tier: "hobby"
|
|
921
|
+
});
|
|
922
|
+
const envIn = body.environmentVariables;
|
|
923
|
+
if (Array.isArray(envIn)) {
|
|
924
|
+
for (const raw of envIn) {
|
|
925
|
+
if (!raw || typeof raw !== "object") continue;
|
|
926
|
+
const ev = raw;
|
|
927
|
+
const key = typeof ev.key === "string" ? ev.key : "";
|
|
928
|
+
if (!key) continue;
|
|
929
|
+
vs.envVars.insert({
|
|
930
|
+
uid: generateUid("env"),
|
|
931
|
+
projectId: project.uid,
|
|
932
|
+
key,
|
|
933
|
+
value: typeof ev.value === "string" ? ev.value : String(ev.value ?? ""),
|
|
934
|
+
type: ev.type === "system" || ev.type === "encrypted" || ev.type === "plain" || ev.type === "secret" || ev.type === "sensitive" ? ev.type : "encrypted",
|
|
935
|
+
target: Array.isArray(ev.target) ? ev.target.filter((t) => t === "production" || t === "preview" || t === "development") : ["production", "preview", "development"],
|
|
936
|
+
gitBranch: typeof ev.gitBranch === "string" ? ev.gitBranch : null,
|
|
937
|
+
customEnvironmentIds: Array.isArray(ev.customEnvironmentIds) ? ev.customEnvironmentIds : [],
|
|
938
|
+
comment: typeof ev.comment === "string" ? ev.comment : null,
|
|
939
|
+
decrypted: false
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return c.json(formatProject(project, baseUrl));
|
|
944
|
+
});
|
|
945
|
+
app.get("/v10/projects", (c) => {
|
|
946
|
+
const scope = resolveTeamScope(c, vs);
|
|
947
|
+
if (!scope) {
|
|
948
|
+
return vercelErr2(c, 401, "not_authenticated", "Authentication required");
|
|
949
|
+
}
|
|
950
|
+
const pagination = parseCursorPagination(c);
|
|
951
|
+
const search = (c.req.query("search") ?? "").trim().toLowerCase();
|
|
952
|
+
let list = vs.projects.all().filter((p) => p.accountId === scope.accountId);
|
|
953
|
+
if (search) {
|
|
954
|
+
list = list.filter((p) => p.name.toLowerCase().includes(search));
|
|
955
|
+
}
|
|
956
|
+
const { items, pagination: pageMeta } = applyCursorPagination(list, pagination);
|
|
957
|
+
return c.json({
|
|
958
|
+
projects: items.map((p) => formatProject(p, baseUrl)),
|
|
959
|
+
pagination: pageMeta
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
app.get("/v9/projects/:idOrName", (c) => {
|
|
963
|
+
const scope = resolveTeamScope(c, vs);
|
|
964
|
+
if (!scope) {
|
|
965
|
+
return vercelErr2(c, 401, "not_authenticated", "Authentication required");
|
|
966
|
+
}
|
|
967
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
968
|
+
if (!project) {
|
|
969
|
+
return vercelErr2(c, 404, "not_found", "Project not found");
|
|
970
|
+
}
|
|
971
|
+
const envs = vs.envVars.findBy("projectId", project.uid);
|
|
972
|
+
return c.json({
|
|
973
|
+
...formatProject(project, baseUrl),
|
|
974
|
+
env: envs.map((e) => formatEnvVar(e))
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
app.patch("/v9/projects/:idOrName", async (c) => {
|
|
978
|
+
const auth = c.get("authUser");
|
|
979
|
+
if (!auth) {
|
|
980
|
+
return vercelErr2(c, 401, "not_authenticated", "Authentication required");
|
|
981
|
+
}
|
|
982
|
+
const scope = resolveTeamScope(c, vs);
|
|
983
|
+
if (!scope) {
|
|
984
|
+
return vercelErr2(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
985
|
+
}
|
|
986
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
987
|
+
if (!project) {
|
|
988
|
+
return vercelErr2(c, 404, "not_found", "Project not found");
|
|
989
|
+
}
|
|
990
|
+
const body = await parseJsonBody(c);
|
|
991
|
+
const patch = {};
|
|
992
|
+
if ("name" in body && typeof body.name === "string") patch.name = body.name.trim();
|
|
993
|
+
if ("buildCommand" in body) {
|
|
994
|
+
patch.buildCommand = body.buildCommand === null ? null : typeof body.buildCommand === "string" ? body.buildCommand : project.buildCommand;
|
|
995
|
+
}
|
|
996
|
+
if ("devCommand" in body) {
|
|
997
|
+
patch.devCommand = body.devCommand === null ? null : typeof body.devCommand === "string" ? body.devCommand : project.devCommand;
|
|
998
|
+
}
|
|
999
|
+
if ("installCommand" in body) {
|
|
1000
|
+
patch.installCommand = body.installCommand === null ? null : typeof body.installCommand === "string" ? body.installCommand : project.installCommand;
|
|
1001
|
+
}
|
|
1002
|
+
if ("outputDirectory" in body) {
|
|
1003
|
+
patch.outputDirectory = body.outputDirectory === null ? null : typeof body.outputDirectory === "string" ? body.outputDirectory : project.outputDirectory;
|
|
1004
|
+
}
|
|
1005
|
+
if ("framework" in body) {
|
|
1006
|
+
patch.framework = body.framework === null ? null : typeof body.framework === "string" ? body.framework : project.framework;
|
|
1007
|
+
}
|
|
1008
|
+
if ("rootDirectory" in body) {
|
|
1009
|
+
patch.rootDirectory = body.rootDirectory === null ? null : typeof body.rootDirectory === "string" ? body.rootDirectory : project.rootDirectory;
|
|
1010
|
+
}
|
|
1011
|
+
if ("gitForkProtection" in body && typeof body.gitForkProtection === "boolean") {
|
|
1012
|
+
patch.gitForkProtection = body.gitForkProtection;
|
|
1013
|
+
}
|
|
1014
|
+
if ("publicSource" in body && typeof body.publicSource === "boolean") {
|
|
1015
|
+
patch.publicSource = body.publicSource;
|
|
1016
|
+
}
|
|
1017
|
+
if ("nodeVersion" in body && typeof body.nodeVersion === "string") {
|
|
1018
|
+
patch.nodeVersion = body.nodeVersion;
|
|
1019
|
+
}
|
|
1020
|
+
if ("serverlessFunctionRegion" in body) {
|
|
1021
|
+
patch.serverlessFunctionRegion = body.serverlessFunctionRegion === null ? null : typeof body.serverlessFunctionRegion === "string" ? body.serverlessFunctionRegion : project.serverlessFunctionRegion;
|
|
1022
|
+
}
|
|
1023
|
+
if ("autoAssignCustomDomains" in body && typeof body.autoAssignCustomDomains === "boolean") {
|
|
1024
|
+
patch.autoAssignCustomDomains = body.autoAssignCustomDomains;
|
|
1025
|
+
}
|
|
1026
|
+
if ("commandForIgnoringBuildStep" in body) {
|
|
1027
|
+
patch.commandForIgnoringBuildStep = body.commandForIgnoringBuildStep === null ? null : typeof body.commandForIgnoringBuildStep === "string" ? body.commandForIgnoringBuildStep : project.commandForIgnoringBuildStep;
|
|
1028
|
+
}
|
|
1029
|
+
const updated = vs.projects.update(project.id, patch);
|
|
1030
|
+
if (!updated) {
|
|
1031
|
+
return vercelErr2(c, 500, "internal_error", "Failed to update project");
|
|
1032
|
+
}
|
|
1033
|
+
return c.json(formatProject(updated, baseUrl));
|
|
1034
|
+
});
|
|
1035
|
+
app.delete("/v9/projects/:idOrName", (c) => {
|
|
1036
|
+
const auth = c.get("authUser");
|
|
1037
|
+
if (!auth) {
|
|
1038
|
+
return vercelErr2(c, 401, "not_authenticated", "Authentication required");
|
|
1039
|
+
}
|
|
1040
|
+
const scope = resolveTeamScope(c, vs);
|
|
1041
|
+
if (!scope) {
|
|
1042
|
+
return vercelErr2(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
1043
|
+
}
|
|
1044
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
1045
|
+
if (!project) {
|
|
1046
|
+
return vercelErr2(c, 404, "not_found", "Project not found");
|
|
1047
|
+
}
|
|
1048
|
+
deleteProjectCascade(vs, project);
|
|
1049
|
+
return c.body(null, 204);
|
|
1050
|
+
});
|
|
1051
|
+
app.get("/v1/projects/:projectId/promote/aliases", (c) => {
|
|
1052
|
+
const scope = resolveTeamScope(c, vs);
|
|
1053
|
+
if (!scope) {
|
|
1054
|
+
return vercelErr2(c, 401, "not_authenticated", "Authentication required");
|
|
1055
|
+
}
|
|
1056
|
+
const project = lookupProject(vs, c.req.param("projectId"), scope.accountId);
|
|
1057
|
+
if (!project) {
|
|
1058
|
+
return vercelErr2(c, 404, "not_found", "Project not found");
|
|
1059
|
+
}
|
|
1060
|
+
const deployments = vs.deployments.findBy("projectId", project.uid);
|
|
1061
|
+
const production = deployments.filter((d) => d.target === "production").sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
|
|
1062
|
+
if (!production) {
|
|
1063
|
+
return c.json({
|
|
1064
|
+
status: "PENDING",
|
|
1065
|
+
alias: []
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
const aliases = vs.deploymentAliases.findBy("deploymentId", production.uid).map((a) => a.alias);
|
|
1069
|
+
const status = production.readySubstate === "PROMOTED" || production.readyState === "READY" ? "PROMOTED" : "PENDING";
|
|
1070
|
+
return c.json({
|
|
1071
|
+
status,
|
|
1072
|
+
alias: aliases
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
app.patch("/v1/projects/:idOrName/protection-bypass", async (c) => {
|
|
1076
|
+
const auth = c.get("authUser");
|
|
1077
|
+
if (!auth) {
|
|
1078
|
+
return vercelErr2(c, 401, "not_authenticated", "Authentication required");
|
|
1079
|
+
}
|
|
1080
|
+
const scope = resolveTeamScope(c, vs);
|
|
1081
|
+
if (!scope) {
|
|
1082
|
+
return vercelErr2(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
1083
|
+
}
|
|
1084
|
+
let project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
1085
|
+
if (!project) {
|
|
1086
|
+
return vercelErr2(c, 404, "not_found", "Project not found");
|
|
1087
|
+
}
|
|
1088
|
+
const user = vs.users.findOneBy("username", auth.login);
|
|
1089
|
+
const createdBy = user?.uid ?? auth.login;
|
|
1090
|
+
const body = await parseJsonBody(c);
|
|
1091
|
+
if (body.generate && typeof body.generate === "object" && body.generate !== null) {
|
|
1092
|
+
const g = body.generate;
|
|
1093
|
+
const secret = generateSecret();
|
|
1094
|
+
vs.protectionBypasses.insert({
|
|
1095
|
+
projectId: project.uid,
|
|
1096
|
+
secret,
|
|
1097
|
+
note: typeof g.note === "string" ? g.note : null,
|
|
1098
|
+
scope: typeof g.scope === "string" ? g.scope : "deployment",
|
|
1099
|
+
createdBy
|
|
1100
|
+
});
|
|
1101
|
+
project = syncProtectionRecordFromCollection(vs, project);
|
|
1102
|
+
}
|
|
1103
|
+
if (Array.isArray(body.revoke)) {
|
|
1104
|
+
for (const secret of body.revoke) {
|
|
1105
|
+
if (typeof secret !== "string") continue;
|
|
1106
|
+
const row = vs.protectionBypasses.findBy("projectId", project.uid).find((r) => r.secret === secret);
|
|
1107
|
+
if (row) {
|
|
1108
|
+
vs.protectionBypasses.delete(row.id);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
project = syncProtectionRecordFromCollection(vs, project);
|
|
1112
|
+
}
|
|
1113
|
+
if (Array.isArray(body.regenerate)) {
|
|
1114
|
+
for (const oldSecret of body.regenerate) {
|
|
1115
|
+
if (typeof oldSecret !== "string") continue;
|
|
1116
|
+
const row = vs.protectionBypasses.findBy("projectId", project.uid).find((r) => r.secret === oldSecret);
|
|
1117
|
+
if (!row) continue;
|
|
1118
|
+
const note = row.note;
|
|
1119
|
+
const scopeVal = row.scope;
|
|
1120
|
+
vs.protectionBypasses.delete(row.id);
|
|
1121
|
+
vs.protectionBypasses.insert({
|
|
1122
|
+
projectId: project.uid,
|
|
1123
|
+
secret: generateSecret(),
|
|
1124
|
+
note,
|
|
1125
|
+
scope: scopeVal,
|
|
1126
|
+
createdBy
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
project = syncProtectionRecordFromCollection(vs, project);
|
|
1130
|
+
}
|
|
1131
|
+
const fresh = vs.projects.get(project.id) ?? project;
|
|
1132
|
+
return c.json({ protectionBypass: fresh.protectionBypass });
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
function vercelErr3(c, status, code, message) {
|
|
1136
|
+
return c.json({ error: { code, message } }, status);
|
|
1137
|
+
}
|
|
1138
|
+
function normalizeUrlParam(raw) {
|
|
1139
|
+
const s = raw.trim();
|
|
1140
|
+
if (s.startsWith("http://") || s.startsWith("https://")) {
|
|
1141
|
+
try {
|
|
1142
|
+
return new URL(s).hostname;
|
|
1143
|
+
} catch {
|
|
1144
|
+
return s;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return s;
|
|
1148
|
+
}
|
|
1149
|
+
function primaryHostFromBaseUrl(baseUrl) {
|
|
1150
|
+
try {
|
|
1151
|
+
const u = new URL(baseUrl);
|
|
1152
|
+
if (u.hostname && u.hostname !== "localhost" && u.hostname !== "127.0.0.1") {
|
|
1153
|
+
return u.hostname;
|
|
1154
|
+
}
|
|
1155
|
+
} catch {
|
|
1156
|
+
}
|
|
1157
|
+
return "vercel.app";
|
|
1158
|
+
}
|
|
1159
|
+
function deploymentHostname(name, uid, baseUrl) {
|
|
1160
|
+
const slug = `${name}-${uid.slice(4, 12)}`;
|
|
1161
|
+
return `${slug}.${primaryHostFromBaseUrl(baseUrl)}`;
|
|
1162
|
+
}
|
|
1163
|
+
function productionProjectAlias(projectName, baseUrl) {
|
|
1164
|
+
return `${projectName}.${primaryHostFromBaseUrl(baseUrl)}`;
|
|
1165
|
+
}
|
|
1166
|
+
function findDeploymentByIdOrUrl(vs, idOrUrl) {
|
|
1167
|
+
const raw = idOrUrl.trim();
|
|
1168
|
+
const byUid = vs.deployments.findOneBy("uid", raw);
|
|
1169
|
+
if (byUid) return byUid;
|
|
1170
|
+
const host = normalizeUrlParam(raw);
|
|
1171
|
+
return vs.deployments.findOneBy("url", host) ?? vs.deployments.findOneBy("url", raw);
|
|
1172
|
+
}
|
|
1173
|
+
function assertDeploymentAccess(vs, dep, accountId) {
|
|
1174
|
+
const project = vs.projects.findOneBy("uid", dep.projectId);
|
|
1175
|
+
return !!project && project.accountId === accountId;
|
|
1176
|
+
}
|
|
1177
|
+
function defaultProjectPayload(name, accountId) {
|
|
1178
|
+
return {
|
|
1179
|
+
uid: generateUid("prj"),
|
|
1180
|
+
name,
|
|
1181
|
+
accountId,
|
|
1182
|
+
framework: null,
|
|
1183
|
+
buildCommand: null,
|
|
1184
|
+
devCommand: null,
|
|
1185
|
+
installCommand: null,
|
|
1186
|
+
outputDirectory: null,
|
|
1187
|
+
rootDirectory: null,
|
|
1188
|
+
commandForIgnoringBuildStep: null,
|
|
1189
|
+
nodeVersion: "20.x",
|
|
1190
|
+
serverlessFunctionRegion: null,
|
|
1191
|
+
publicSource: false,
|
|
1192
|
+
autoAssignCustomDomains: true,
|
|
1193
|
+
autoAssignCustomDomainsUpdatedBy: null,
|
|
1194
|
+
gitForkProtection: true,
|
|
1195
|
+
sourceFilesOutsideRootDirectory: false,
|
|
1196
|
+
live: true,
|
|
1197
|
+
link: null,
|
|
1198
|
+
latestDeployments: [],
|
|
1199
|
+
targets: {},
|
|
1200
|
+
protectionBypass: {},
|
|
1201
|
+
passwordProtection: null,
|
|
1202
|
+
ssoProtection: null,
|
|
1203
|
+
trustedIps: null,
|
|
1204
|
+
connectConfigurationId: null,
|
|
1205
|
+
gitComments: { onPullRequest: true, onCommit: false },
|
|
1206
|
+
webAnalytics: null,
|
|
1207
|
+
speedInsights: null,
|
|
1208
|
+
oidcTokenConfig: null,
|
|
1209
|
+
tier: "hobby"
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
function resolveOrCreateProject(vs, accountId, name, projectField) {
|
|
1213
|
+
if (typeof projectField === "string" && projectField.trim()) {
|
|
1214
|
+
const byId = lookupProject(vs, projectField.trim(), accountId);
|
|
1215
|
+
if (byId) return byId;
|
|
1216
|
+
}
|
|
1217
|
+
const existing = vs.projects.findBy("name", name).find((p) => p.accountId === accountId);
|
|
1218
|
+
if (existing) return existing;
|
|
1219
|
+
return vs.projects.insert(defaultProjectPayload(name, accountId));
|
|
1220
|
+
}
|
|
1221
|
+
function targetKey(target) {
|
|
1222
|
+
if (target === "production") return "production";
|
|
1223
|
+
if (target === "staging") return "staging";
|
|
1224
|
+
return "preview";
|
|
1225
|
+
}
|
|
1226
|
+
function upsertProjectDeploymentRefs(vs, projectId, dep) {
|
|
1227
|
+
const project = vs.projects.get(projectId);
|
|
1228
|
+
if (!project) return;
|
|
1229
|
+
const createdAt = new Date(dep.created_at).getTime();
|
|
1230
|
+
const entry = { id: dep.uid, url: dep.url, state: dep.state, createdAt };
|
|
1231
|
+
const latest = [{ ...entry }, ...project.latestDeployments.filter((d) => d.id !== dep.uid)];
|
|
1232
|
+
const targets = { ...project.targets };
|
|
1233
|
+
targets[targetKey(dep.target)] = { ...entry };
|
|
1234
|
+
vs.projects.update(project.id, { latestDeployments: latest, targets });
|
|
1235
|
+
}
|
|
1236
|
+
function parseGitSource(raw) {
|
|
1237
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1238
|
+
const g = raw;
|
|
1239
|
+
return {
|
|
1240
|
+
type: typeof g.type === "string" ? g.type : "github",
|
|
1241
|
+
ref: typeof g.ref === "string" ? g.ref : "",
|
|
1242
|
+
sha: typeof g.sha === "string" ? g.sha : "",
|
|
1243
|
+
repoId: typeof g.repoId === "string" ? g.repoId : typeof g.repoId === "number" ? String(g.repoId) : "",
|
|
1244
|
+
org: typeof g.org === "string" ? g.org : "",
|
|
1245
|
+
repo: typeof g.repo === "string" ? g.repo : "",
|
|
1246
|
+
message: typeof g.message === "string" ? g.message : "",
|
|
1247
|
+
authorName: typeof g.authorName === "string" ? g.authorName : "",
|
|
1248
|
+
commitAuthorName: typeof g.commitAuthorName === "string" ? g.commitAuthorName : ""
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function buildFileTreeFromRows(rows, genUid) {
|
|
1252
|
+
if (rows.length === 0) {
|
|
1253
|
+
return [
|
|
1254
|
+
{
|
|
1255
|
+
uid: genUid(),
|
|
1256
|
+
name: "/",
|
|
1257
|
+
type: "directory",
|
|
1258
|
+
mode: 16877,
|
|
1259
|
+
size: 0,
|
|
1260
|
+
contentType: null,
|
|
1261
|
+
children: []
|
|
1262
|
+
}
|
|
1263
|
+
];
|
|
1264
|
+
}
|
|
1265
|
+
const root = {
|
|
1266
|
+
uid: genUid(),
|
|
1267
|
+
name: "/",
|
|
1268
|
+
type: "directory",
|
|
1269
|
+
mode: 16877,
|
|
1270
|
+
size: 0,
|
|
1271
|
+
contentType: null,
|
|
1272
|
+
children: []
|
|
1273
|
+
};
|
|
1274
|
+
for (const row of rows) {
|
|
1275
|
+
if (row.type !== "file") continue;
|
|
1276
|
+
const parts = row.name.split("/").filter(Boolean);
|
|
1277
|
+
if (parts.length === 0) continue;
|
|
1278
|
+
const fileName = parts.pop();
|
|
1279
|
+
let current = root;
|
|
1280
|
+
for (const part of parts) {
|
|
1281
|
+
let dir = current.children.find((c) => c.name === part && c.type === "directory");
|
|
1282
|
+
if (!dir) {
|
|
1283
|
+
dir = {
|
|
1284
|
+
uid: genUid(),
|
|
1285
|
+
name: part,
|
|
1286
|
+
type: "directory",
|
|
1287
|
+
mode: 16877,
|
|
1288
|
+
size: 0,
|
|
1289
|
+
contentType: null,
|
|
1290
|
+
children: []
|
|
1291
|
+
};
|
|
1292
|
+
current.children.push(dir);
|
|
1293
|
+
}
|
|
1294
|
+
current = dir;
|
|
1295
|
+
}
|
|
1296
|
+
current.children.push({
|
|
1297
|
+
uid: row.uid,
|
|
1298
|
+
name: fileName,
|
|
1299
|
+
type: "file",
|
|
1300
|
+
mode: row.mode,
|
|
1301
|
+
size: row.size,
|
|
1302
|
+
contentType: row.contentType,
|
|
1303
|
+
children: []
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
return [root];
|
|
1307
|
+
}
|
|
1308
|
+
function deleteDeploymentCascade(vs, dep) {
|
|
1309
|
+
const uid = dep.uid;
|
|
1310
|
+
for (const b of vs.builds.findBy("deploymentId", uid)) {
|
|
1311
|
+
vs.builds.delete(b.id);
|
|
1312
|
+
}
|
|
1313
|
+
for (const e of vs.deploymentEvents.findBy("deploymentId", uid)) {
|
|
1314
|
+
vs.deploymentEvents.delete(e.id);
|
|
1315
|
+
}
|
|
1316
|
+
for (const f of vs.deploymentFiles.findBy("deploymentId", uid)) {
|
|
1317
|
+
vs.deploymentFiles.delete(f.id);
|
|
1318
|
+
}
|
|
1319
|
+
for (const a of vs.deploymentAliases.findBy("deploymentId", uid)) {
|
|
1320
|
+
vs.deploymentAliases.delete(a.id);
|
|
1321
|
+
}
|
|
1322
|
+
vs.deployments.delete(dep.id);
|
|
1323
|
+
const project = vs.projects.findOneBy("uid", dep.projectId);
|
|
1324
|
+
if (project) {
|
|
1325
|
+
const latestDeployments = project.latestDeployments.filter((d) => d.id !== uid);
|
|
1326
|
+
const targets = { ...project.targets };
|
|
1327
|
+
for (const k of Object.keys(targets)) {
|
|
1328
|
+
if (targets[k]?.id === uid) {
|
|
1329
|
+
delete targets[k];
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
vs.projects.update(project.id, { latestDeployments, targets });
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function deploymentsRoutes({ app, store, baseUrl }) {
|
|
1336
|
+
const vs = getVercelStore(store);
|
|
1337
|
+
app.patch("/v12/deployments/:id/cancel", async (c) => {
|
|
1338
|
+
const auth = c.get("authUser");
|
|
1339
|
+
if (!auth) {
|
|
1340
|
+
return vercelErr3(c, 401, "not_authenticated", "Authentication required");
|
|
1341
|
+
}
|
|
1342
|
+
const scope = resolveTeamScope(c, vs);
|
|
1343
|
+
if (!scope) {
|
|
1344
|
+
return vercelErr3(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
1345
|
+
}
|
|
1346
|
+
const dep = vs.deployments.findOneBy("uid", c.req.param("id"));
|
|
1347
|
+
if (!dep || !assertDeploymentAccess(vs, dep, scope.accountId)) {
|
|
1348
|
+
return vercelErr3(c, 404, "not_found", "Deployment not found");
|
|
1349
|
+
}
|
|
1350
|
+
if (dep.readyState !== "QUEUED" && dep.readyState !== "BUILDING") {
|
|
1351
|
+
return vercelErr3(c, 400, "bad_request", "Deployment cannot be canceled in its current state");
|
|
1352
|
+
}
|
|
1353
|
+
const t = nowMs();
|
|
1354
|
+
const updated = vs.deployments.update(dep.id, {
|
|
1355
|
+
readyState: "CANCELED",
|
|
1356
|
+
state: "CANCELED",
|
|
1357
|
+
canceledAt: t
|
|
1358
|
+
}) ?? dep;
|
|
1359
|
+
vs.deploymentEvents.insert({
|
|
1360
|
+
deploymentId: updated.uid,
|
|
1361
|
+
type: "canceled",
|
|
1362
|
+
payload: { text: "Deployment canceled" },
|
|
1363
|
+
date: t,
|
|
1364
|
+
serial: String(t)
|
|
1365
|
+
});
|
|
1366
|
+
return c.json(formatDeployment(updated, vs, baseUrl));
|
|
1367
|
+
});
|
|
1368
|
+
app.get("/v2/deployments/:id/aliases", (c) => {
|
|
1369
|
+
const scope = resolveTeamScope(c, vs);
|
|
1370
|
+
if (!scope) {
|
|
1371
|
+
return vercelErr3(c, 401, "not_authenticated", "Authentication required");
|
|
1372
|
+
}
|
|
1373
|
+
const dep = vs.deployments.findOneBy("uid", c.req.param("id"));
|
|
1374
|
+
if (!dep || !assertDeploymentAccess(vs, dep, scope.accountId)) {
|
|
1375
|
+
return vercelErr3(c, 404, "not_found", "Deployment not found");
|
|
1376
|
+
}
|
|
1377
|
+
const aliases = vs.deploymentAliases.findBy("deploymentId", dep.uid);
|
|
1378
|
+
return c.json({
|
|
1379
|
+
aliases: aliases.map((a) => ({
|
|
1380
|
+
uid: a.uid,
|
|
1381
|
+
alias: a.alias,
|
|
1382
|
+
deploymentId: a.deploymentId,
|
|
1383
|
+
projectId: a.projectId
|
|
1384
|
+
}))
|
|
1385
|
+
});
|
|
1386
|
+
});
|
|
1387
|
+
app.get("/v3/deployments/:idOrUrl/events", (c) => {
|
|
1388
|
+
const scope = resolveTeamScope(c, vs);
|
|
1389
|
+
if (!scope) {
|
|
1390
|
+
return vercelErr3(c, 401, "not_authenticated", "Authentication required");
|
|
1391
|
+
}
|
|
1392
|
+
const dep = findDeploymentByIdOrUrl(vs, c.req.param("idOrUrl"));
|
|
1393
|
+
if (!dep || !assertDeploymentAccess(vs, dep, scope.accountId)) {
|
|
1394
|
+
return vercelErr3(c, 404, "not_found", "Deployment not found");
|
|
1395
|
+
}
|
|
1396
|
+
void c.req.query("follow");
|
|
1397
|
+
const direction = (c.req.query("direction") ?? "backward").toLowerCase();
|
|
1398
|
+
const limit = Math.min(100, Math.max(1, parseInt(c.req.query("limit") ?? "20", 10) || 20));
|
|
1399
|
+
let list = [...vs.deploymentEvents.findBy("deploymentId", dep.uid)];
|
|
1400
|
+
list.sort((a, b) => a.date - b.date);
|
|
1401
|
+
if (direction === "backward") {
|
|
1402
|
+
list.reverse();
|
|
1403
|
+
}
|
|
1404
|
+
list = list.slice(0, limit);
|
|
1405
|
+
return c.json(
|
|
1406
|
+
list.map((e) => ({
|
|
1407
|
+
type: e.type,
|
|
1408
|
+
payload: e.payload,
|
|
1409
|
+
date: e.date,
|
|
1410
|
+
serial: e.serial
|
|
1411
|
+
}))
|
|
1412
|
+
);
|
|
1413
|
+
});
|
|
1414
|
+
app.get("/v6/deployments/:id/files", (c) => {
|
|
1415
|
+
const scope = resolveTeamScope(c, vs);
|
|
1416
|
+
if (!scope) {
|
|
1417
|
+
return vercelErr3(c, 401, "not_authenticated", "Authentication required");
|
|
1418
|
+
}
|
|
1419
|
+
const dep = vs.deployments.findOneBy("uid", c.req.param("id"));
|
|
1420
|
+
if (!dep || !assertDeploymentAccess(vs, dep, scope.accountId)) {
|
|
1421
|
+
return vercelErr3(c, 404, "not_found", "Deployment not found");
|
|
1422
|
+
}
|
|
1423
|
+
const rows = vs.deploymentFiles.findBy("deploymentId", dep.uid);
|
|
1424
|
+
const tree = buildFileTreeFromRows(rows, () => generateUid("file"));
|
|
1425
|
+
return c.json({ files: tree });
|
|
1426
|
+
});
|
|
1427
|
+
app.post("/v13/deployments", async (c) => {
|
|
1428
|
+
const auth = c.get("authUser");
|
|
1429
|
+
if (!auth) {
|
|
1430
|
+
return vercelErr3(c, 401, "not_authenticated", "Authentication required");
|
|
1431
|
+
}
|
|
1432
|
+
const scope = resolveTeamScope(c, vs);
|
|
1433
|
+
if (!scope) {
|
|
1434
|
+
return vercelErr3(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
1435
|
+
}
|
|
1436
|
+
const body = await parseJsonBody(c);
|
|
1437
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
1438
|
+
if (!name) {
|
|
1439
|
+
return vercelErr3(c, 400, "bad_request", "Missing required field: name");
|
|
1440
|
+
}
|
|
1441
|
+
const user = vs.users.findOneBy("username", auth.login);
|
|
1442
|
+
if (!user) {
|
|
1443
|
+
return vercelErr3(c, 400, "bad_request", "User not found in Vercel store");
|
|
1444
|
+
}
|
|
1445
|
+
const project = resolveOrCreateProject(vs, scope.accountId, name, body.project);
|
|
1446
|
+
const uid = generateUid("dpl");
|
|
1447
|
+
const url = deploymentHostname(name, uid, baseUrl);
|
|
1448
|
+
const inspectorUrl = `${baseUrl.replace(/\/$/, "")}/deployments/${uid}`;
|
|
1449
|
+
const targetRaw = body.target;
|
|
1450
|
+
const target = targetRaw === "production" || targetRaw === "preview" || targetRaw === "staging" ? targetRaw : "preview";
|
|
1451
|
+
const meta = {};
|
|
1452
|
+
if (body.meta && typeof body.meta === "object" && body.meta !== null) {
|
|
1453
|
+
for (const [k, v] of Object.entries(body.meta)) {
|
|
1454
|
+
if (typeof v === "string") meta[k] = v;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const regions = Array.isArray(body.regions) && body.regions.every((r) => typeof r === "string") ? body.regions : ["iad1"];
|
|
1458
|
+
const t = nowMs();
|
|
1459
|
+
const gitSource = parseGitSource(body.gitSource);
|
|
1460
|
+
const source = gitSource ? "git" : "cli";
|
|
1461
|
+
const dep = vs.deployments.insert({
|
|
1462
|
+
uid,
|
|
1463
|
+
name,
|
|
1464
|
+
url,
|
|
1465
|
+
projectId: project.uid,
|
|
1466
|
+
source,
|
|
1467
|
+
target,
|
|
1468
|
+
readyState: "READY",
|
|
1469
|
+
readySubstate: null,
|
|
1470
|
+
state: "READY",
|
|
1471
|
+
creatorId: user.uid,
|
|
1472
|
+
inspectorUrl,
|
|
1473
|
+
meta,
|
|
1474
|
+
gitSource,
|
|
1475
|
+
buildingAt: t,
|
|
1476
|
+
readyAt: t,
|
|
1477
|
+
canceledAt: null,
|
|
1478
|
+
errorCode: null,
|
|
1479
|
+
errorMessage: null,
|
|
1480
|
+
regions,
|
|
1481
|
+
functions: null,
|
|
1482
|
+
routes: null,
|
|
1483
|
+
plan: "hobby",
|
|
1484
|
+
aliasAssigned: true,
|
|
1485
|
+
aliasError: null,
|
|
1486
|
+
bootedAt: t
|
|
1487
|
+
});
|
|
1488
|
+
vs.deploymentAliases.insert({
|
|
1489
|
+
uid: generateUid("als"),
|
|
1490
|
+
alias: url,
|
|
1491
|
+
deploymentId: dep.uid,
|
|
1492
|
+
projectId: project.uid
|
|
1493
|
+
});
|
|
1494
|
+
if (target === "production") {
|
|
1495
|
+
vs.deploymentAliases.insert({
|
|
1496
|
+
uid: generateUid("als"),
|
|
1497
|
+
alias: productionProjectAlias(project.name, baseUrl),
|
|
1498
|
+
deploymentId: dep.uid,
|
|
1499
|
+
projectId: project.uid
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
upsertProjectDeploymentRefs(vs, project.id, dep);
|
|
1503
|
+
vs.builds.insert({
|
|
1504
|
+
uid: generateUid("bld"),
|
|
1505
|
+
deploymentId: dep.uid,
|
|
1506
|
+
entrypoint: "api/index.ts",
|
|
1507
|
+
readyState: "READY",
|
|
1508
|
+
output: [],
|
|
1509
|
+
readyStateAt: t,
|
|
1510
|
+
fingerprint: generateUid("fgp")
|
|
1511
|
+
});
|
|
1512
|
+
let serial = 0;
|
|
1513
|
+
const pushEvent = (type, text) => {
|
|
1514
|
+
serial += 1;
|
|
1515
|
+
vs.deploymentEvents.insert({
|
|
1516
|
+
deploymentId: dep.uid,
|
|
1517
|
+
type,
|
|
1518
|
+
payload: { text },
|
|
1519
|
+
date: t,
|
|
1520
|
+
serial: String(serial)
|
|
1521
|
+
});
|
|
1522
|
+
};
|
|
1523
|
+
pushEvent("created", "Deployment created");
|
|
1524
|
+
pushEvent("building", "Building");
|
|
1525
|
+
pushEvent("ready", "Deployment ready");
|
|
1526
|
+
const filesIn = body.files;
|
|
1527
|
+
if (Array.isArray(filesIn)) {
|
|
1528
|
+
for (const raw of filesIn) {
|
|
1529
|
+
if (!raw || typeof raw !== "object") continue;
|
|
1530
|
+
const f = raw;
|
|
1531
|
+
const filePath = typeof f.file === "string" ? f.file : "";
|
|
1532
|
+
const sha = typeof f.sha === "string" ? f.sha : "";
|
|
1533
|
+
const size = typeof f.size === "number" ? f.size : 0;
|
|
1534
|
+
if (!filePath || !sha) continue;
|
|
1535
|
+
if (!vs.files.findOneBy("digest", sha)) {
|
|
1536
|
+
vs.files.insert({
|
|
1537
|
+
digest: sha,
|
|
1538
|
+
size,
|
|
1539
|
+
contentType: "application/octet-stream"
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
vs.deploymentFiles.insert({
|
|
1543
|
+
deploymentId: dep.uid,
|
|
1544
|
+
name: filePath,
|
|
1545
|
+
type: "file",
|
|
1546
|
+
uid: generateUid("f"),
|
|
1547
|
+
children: [],
|
|
1548
|
+
contentType: "application/octet-stream",
|
|
1549
|
+
mode: 420,
|
|
1550
|
+
size
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return c.json(formatDeployment(dep, vs, baseUrl));
|
|
1555
|
+
});
|
|
1556
|
+
app.get("/v6/deployments", (c) => {
|
|
1557
|
+
const scope = resolveTeamScope(c, vs);
|
|
1558
|
+
if (!scope) {
|
|
1559
|
+
return vercelErr3(c, 401, "not_authenticated", "Authentication required");
|
|
1560
|
+
}
|
|
1561
|
+
const appName = (c.req.query("app") ?? "").trim();
|
|
1562
|
+
const projectIdFilter = (c.req.query("projectId") ?? "").trim();
|
|
1563
|
+
const targetFilter = c.req.query("target");
|
|
1564
|
+
const stateFilter = c.req.query("state");
|
|
1565
|
+
let list = vs.deployments.all().filter((d) => {
|
|
1566
|
+
const proj = vs.projects.findOneBy("uid", d.projectId);
|
|
1567
|
+
return proj && proj.accountId === scope.accountId;
|
|
1568
|
+
});
|
|
1569
|
+
if (appName) {
|
|
1570
|
+
list = list.filter((d) => {
|
|
1571
|
+
const proj = vs.projects.findOneBy("uid", d.projectId);
|
|
1572
|
+
return proj?.name === appName;
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
if (projectIdFilter) {
|
|
1576
|
+
list = list.filter((d) => d.projectId === projectIdFilter);
|
|
1577
|
+
}
|
|
1578
|
+
if (targetFilter === "production" || targetFilter === "preview" || targetFilter === "staging") {
|
|
1579
|
+
list = list.filter((d) => d.target === targetFilter);
|
|
1580
|
+
}
|
|
1581
|
+
if (stateFilter) {
|
|
1582
|
+
list = list.filter((d) => d.state === stateFilter || d.readyState === stateFilter);
|
|
1583
|
+
}
|
|
1584
|
+
const pagination = parseCursorPagination(c);
|
|
1585
|
+
const { items, pagination: pageMeta } = applyCursorPagination(list, pagination);
|
|
1586
|
+
return c.json({
|
|
1587
|
+
deployments: items.map((d) => formatDeploymentBrief(d, vs)),
|
|
1588
|
+
pagination: pageMeta
|
|
1589
|
+
});
|
|
1590
|
+
});
|
|
1591
|
+
app.delete("/v13/deployments/:id", (c) => {
|
|
1592
|
+
const auth = c.get("authUser");
|
|
1593
|
+
if (!auth) {
|
|
1594
|
+
return vercelErr3(c, 401, "not_authenticated", "Authentication required");
|
|
1595
|
+
}
|
|
1596
|
+
const scope = resolveTeamScope(c, vs);
|
|
1597
|
+
if (!scope) {
|
|
1598
|
+
return vercelErr3(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
1599
|
+
}
|
|
1600
|
+
const dep = vs.deployments.findOneBy("uid", c.req.param("id"));
|
|
1601
|
+
if (!dep || !assertDeploymentAccess(vs, dep, scope.accountId)) {
|
|
1602
|
+
return vercelErr3(c, 404, "not_found", "Deployment not found");
|
|
1603
|
+
}
|
|
1604
|
+
const uid = dep.uid;
|
|
1605
|
+
deleteDeploymentCascade(vs, dep);
|
|
1606
|
+
return c.json({ uid, state: "DELETED" });
|
|
1607
|
+
});
|
|
1608
|
+
app.get("/v13/deployments/:idOrUrl", (c) => {
|
|
1609
|
+
const scope = resolveTeamScope(c, vs);
|
|
1610
|
+
if (!scope) {
|
|
1611
|
+
return vercelErr3(c, 401, "not_authenticated", "Authentication required");
|
|
1612
|
+
}
|
|
1613
|
+
const dep = findDeploymentByIdOrUrl(vs, c.req.param("idOrUrl"));
|
|
1614
|
+
if (!dep || !assertDeploymentAccess(vs, dep, scope.accountId)) {
|
|
1615
|
+
return vercelErr3(c, 404, "not_found", "Deployment not found");
|
|
1616
|
+
}
|
|
1617
|
+
return c.json(formatDeployment(dep, vs, baseUrl));
|
|
1618
|
+
});
|
|
1619
|
+
app.post("/v2/files", async (c) => {
|
|
1620
|
+
const auth = c.get("authUser");
|
|
1621
|
+
if (!auth) {
|
|
1622
|
+
return vercelErr3(c, 401, "not_authenticated", "Authentication required");
|
|
1623
|
+
}
|
|
1624
|
+
const digest = c.req.header("x-vercel-digest") ?? "";
|
|
1625
|
+
if (!digest) {
|
|
1626
|
+
return vercelErr3(c, 400, "bad_request", "Missing x-vercel-digest header");
|
|
1627
|
+
}
|
|
1628
|
+
const lenRaw = c.req.header("Content-Length");
|
|
1629
|
+
const size = lenRaw ? parseInt(lenRaw, 10) : 0;
|
|
1630
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
1631
|
+
return vercelErr3(c, 400, "bad_request", "Invalid Content-Length");
|
|
1632
|
+
}
|
|
1633
|
+
await c.req.arrayBuffer();
|
|
1634
|
+
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
|
|
1635
|
+
if (!vs.files.findOneBy("digest", digest)) {
|
|
1636
|
+
vs.files.insert({
|
|
1637
|
+
digest,
|
|
1638
|
+
size,
|
|
1639
|
+
contentType
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
return c.json([]);
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
function vercelErr4(c, status, code, message) {
|
|
1646
|
+
return c.json({ error: { code, message } }, status);
|
|
1647
|
+
}
|
|
1648
|
+
function extractApexName(domain) {
|
|
1649
|
+
const parts = domain.toLowerCase().split(".").filter((p) => p.length > 0);
|
|
1650
|
+
if (parts.length === 0) return domain;
|
|
1651
|
+
if (parts.length === 1) return parts[0];
|
|
1652
|
+
return parts.slice(-2).join(".");
|
|
1653
|
+
}
|
|
1654
|
+
function isVercelAppDomain(domain) {
|
|
1655
|
+
const d = domain.toLowerCase();
|
|
1656
|
+
return d === "vercel.app" || d.endsWith(".vercel.app");
|
|
1657
|
+
}
|
|
1658
|
+
function normalizeDomainName(raw) {
|
|
1659
|
+
return raw.trim().toLowerCase();
|
|
1660
|
+
}
|
|
1661
|
+
function parseRedirectStatusCode(raw) {
|
|
1662
|
+
if (raw === void 0 || raw === null) return null;
|
|
1663
|
+
if (typeof raw !== "number" || !Number.isInteger(raw)) return "invalid";
|
|
1664
|
+
if (raw === 301 || raw === 302 || raw === 307 || raw === 308) return raw;
|
|
1665
|
+
return "invalid";
|
|
1666
|
+
}
|
|
1667
|
+
function findDomainInProject(vs, projectUid, domainName) {
|
|
1668
|
+
const normalized = normalizeDomainName(domainName);
|
|
1669
|
+
return vs.domains.findBy("projectId", projectUid).find((d) => d.name.toLowerCase() === normalized);
|
|
1670
|
+
}
|
|
1671
|
+
function decodeDomainParam(raw) {
|
|
1672
|
+
try {
|
|
1673
|
+
return decodeURIComponent(raw);
|
|
1674
|
+
} catch {
|
|
1675
|
+
return raw;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function domainsRoutes({ app, store }) {
|
|
1679
|
+
const vs = getVercelStore(store);
|
|
1680
|
+
app.post("/v10/projects/:idOrName/domains", async (c) => {
|
|
1681
|
+
const auth = c.get("authUser");
|
|
1682
|
+
if (!auth) {
|
|
1683
|
+
return vercelErr4(c, 401, "not_authenticated", "Authentication required");
|
|
1684
|
+
}
|
|
1685
|
+
const scope = resolveTeamScope(c, vs);
|
|
1686
|
+
if (!scope) {
|
|
1687
|
+
return vercelErr4(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
1688
|
+
}
|
|
1689
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
1690
|
+
if (!project) {
|
|
1691
|
+
return vercelErr4(c, 404, "not_found", "Project not found");
|
|
1692
|
+
}
|
|
1693
|
+
const body = await parseJsonBody(c);
|
|
1694
|
+
const nameRaw = typeof body.name === "string" ? body.name.trim() : "";
|
|
1695
|
+
if (!nameRaw) {
|
|
1696
|
+
return vercelErr4(c, 400, "bad_request", "Missing required field: name");
|
|
1697
|
+
}
|
|
1698
|
+
const name = normalizeDomainName(nameRaw);
|
|
1699
|
+
const apexName = extractApexName(name);
|
|
1700
|
+
if (findDomainInProject(vs, project.uid, name)) {
|
|
1701
|
+
return vercelErr4(c, 409, "domain_already_exists", "A domain with this name already exists on the project");
|
|
1702
|
+
}
|
|
1703
|
+
const redirect = body.redirect === null ? null : typeof body.redirect === "string" ? body.redirect.trim() || null : null;
|
|
1704
|
+
const redirectStatusCode = parseRedirectStatusCode(body.redirectStatusCode);
|
|
1705
|
+
if (redirectStatusCode === "invalid") {
|
|
1706
|
+
return vercelErr4(c, 400, "bad_request", "Invalid redirectStatusCode");
|
|
1707
|
+
}
|
|
1708
|
+
const gitBranch = body.gitBranch === null ? null : typeof body.gitBranch === "string" ? body.gitBranch : null;
|
|
1709
|
+
const customEnvironmentId = body.customEnvironmentId === null ? null : typeof body.customEnvironmentId === "string" ? body.customEnvironmentId : null;
|
|
1710
|
+
const uid = generateUid();
|
|
1711
|
+
const autoVerified = isVercelAppDomain(name);
|
|
1712
|
+
const verified = autoVerified;
|
|
1713
|
+
const verification = autoVerified ? [] : [
|
|
1714
|
+
{
|
|
1715
|
+
type: "TXT",
|
|
1716
|
+
domain: `_vercel.${apexName}`,
|
|
1717
|
+
value: `vc-domain-verify=${name},${uid}`,
|
|
1718
|
+
reason: "Add the TXT record above to verify domain ownership"
|
|
1719
|
+
}
|
|
1720
|
+
];
|
|
1721
|
+
const row = vs.domains.insert({
|
|
1722
|
+
uid,
|
|
1723
|
+
projectId: project.uid,
|
|
1724
|
+
name,
|
|
1725
|
+
apexName,
|
|
1726
|
+
redirect,
|
|
1727
|
+
redirectStatusCode,
|
|
1728
|
+
gitBranch,
|
|
1729
|
+
customEnvironmentId,
|
|
1730
|
+
verified,
|
|
1731
|
+
verification
|
|
1732
|
+
});
|
|
1733
|
+
return c.json(formatDomain(row));
|
|
1734
|
+
});
|
|
1735
|
+
app.get("/v9/projects/:idOrName/domains", (c) => {
|
|
1736
|
+
const scope = resolveTeamScope(c, vs);
|
|
1737
|
+
if (!scope) {
|
|
1738
|
+
return vercelErr4(c, 401, "not_authenticated", "Authentication required");
|
|
1739
|
+
}
|
|
1740
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
1741
|
+
if (!project) {
|
|
1742
|
+
return vercelErr4(c, 404, "not_found", "Project not found");
|
|
1743
|
+
}
|
|
1744
|
+
const pagination = parseCursorPagination(c);
|
|
1745
|
+
const list = vs.domains.findBy("projectId", project.uid);
|
|
1746
|
+
const { items, pagination: pageMeta } = applyCursorPagination(list, pagination);
|
|
1747
|
+
return c.json({
|
|
1748
|
+
domains: items.map((d) => formatDomain(d)),
|
|
1749
|
+
pagination: pageMeta
|
|
1750
|
+
});
|
|
1751
|
+
});
|
|
1752
|
+
app.post("/v9/projects/:idOrName/domains/:domain/verify", (c) => {
|
|
1753
|
+
const auth = c.get("authUser");
|
|
1754
|
+
if (!auth) {
|
|
1755
|
+
return vercelErr4(c, 401, "not_authenticated", "Authentication required");
|
|
1756
|
+
}
|
|
1757
|
+
const scope = resolveTeamScope(c, vs);
|
|
1758
|
+
if (!scope) {
|
|
1759
|
+
return vercelErr4(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
1760
|
+
}
|
|
1761
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
1762
|
+
if (!project) {
|
|
1763
|
+
return vercelErr4(c, 404, "not_found", "Project not found");
|
|
1764
|
+
}
|
|
1765
|
+
const domainName = decodeDomainParam(c.req.param("domain"));
|
|
1766
|
+
const existing = findDomainInProject(vs, project.uid, domainName);
|
|
1767
|
+
if (!existing) {
|
|
1768
|
+
return vercelErr4(c, 404, "not_found", "Domain not found");
|
|
1769
|
+
}
|
|
1770
|
+
const updated = vs.domains.update(existing.id, {
|
|
1771
|
+
verified: true,
|
|
1772
|
+
verification: []
|
|
1773
|
+
});
|
|
1774
|
+
if (!updated) {
|
|
1775
|
+
return vercelErr4(c, 500, "internal_error", "Failed to update domain");
|
|
1776
|
+
}
|
|
1777
|
+
return c.json(formatDomain(updated));
|
|
1778
|
+
});
|
|
1779
|
+
app.get("/v9/projects/:idOrName/domains/:domain", (c) => {
|
|
1780
|
+
const scope = resolveTeamScope(c, vs);
|
|
1781
|
+
if (!scope) {
|
|
1782
|
+
return vercelErr4(c, 401, "not_authenticated", "Authentication required");
|
|
1783
|
+
}
|
|
1784
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
1785
|
+
if (!project) {
|
|
1786
|
+
return vercelErr4(c, 404, "not_found", "Project not found");
|
|
1787
|
+
}
|
|
1788
|
+
const domainName = decodeDomainParam(c.req.param("domain"));
|
|
1789
|
+
const existing = findDomainInProject(vs, project.uid, domainName);
|
|
1790
|
+
if (!existing) {
|
|
1791
|
+
return vercelErr4(c, 404, "not_found", "Domain not found");
|
|
1792
|
+
}
|
|
1793
|
+
return c.json(formatDomain(existing));
|
|
1794
|
+
});
|
|
1795
|
+
app.patch("/v9/projects/:idOrName/domains/:domain", async (c) => {
|
|
1796
|
+
const auth = c.get("authUser");
|
|
1797
|
+
if (!auth) {
|
|
1798
|
+
return vercelErr4(c, 401, "not_authenticated", "Authentication required");
|
|
1799
|
+
}
|
|
1800
|
+
const scope = resolveTeamScope(c, vs);
|
|
1801
|
+
if (!scope) {
|
|
1802
|
+
return vercelErr4(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
1803
|
+
}
|
|
1804
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
1805
|
+
if (!project) {
|
|
1806
|
+
return vercelErr4(c, 404, "not_found", "Project not found");
|
|
1807
|
+
}
|
|
1808
|
+
const domainName = decodeDomainParam(c.req.param("domain"));
|
|
1809
|
+
const existing = findDomainInProject(vs, project.uid, domainName);
|
|
1810
|
+
if (!existing) {
|
|
1811
|
+
return vercelErr4(c, 404, "not_found", "Domain not found");
|
|
1812
|
+
}
|
|
1813
|
+
const body = await parseJsonBody(c);
|
|
1814
|
+
const patch = {};
|
|
1815
|
+
if ("gitBranch" in body) {
|
|
1816
|
+
patch.gitBranch = body.gitBranch === null ? null : typeof body.gitBranch === "string" ? body.gitBranch : existing.gitBranch;
|
|
1817
|
+
}
|
|
1818
|
+
if ("redirect" in body) {
|
|
1819
|
+
patch.redirect = body.redirect === null ? null : typeof body.redirect === "string" ? body.redirect.trim() || null : existing.redirect;
|
|
1820
|
+
}
|
|
1821
|
+
if ("redirectStatusCode" in body) {
|
|
1822
|
+
const code = parseRedirectStatusCode(body.redirectStatusCode);
|
|
1823
|
+
if (code === "invalid") {
|
|
1824
|
+
return vercelErr4(c, 400, "bad_request", "Invalid redirectStatusCode");
|
|
1825
|
+
}
|
|
1826
|
+
patch.redirectStatusCode = code;
|
|
1827
|
+
}
|
|
1828
|
+
if ("customEnvironmentId" in body) {
|
|
1829
|
+
patch.customEnvironmentId = body.customEnvironmentId === null ? null : typeof body.customEnvironmentId === "string" ? body.customEnvironmentId : existing.customEnvironmentId;
|
|
1830
|
+
}
|
|
1831
|
+
const updated = vs.domains.update(existing.id, patch);
|
|
1832
|
+
if (!updated) {
|
|
1833
|
+
return vercelErr4(c, 500, "internal_error", "Failed to update domain");
|
|
1834
|
+
}
|
|
1835
|
+
return c.json(formatDomain(updated));
|
|
1836
|
+
});
|
|
1837
|
+
app.delete("/v9/projects/:idOrName/domains/:domain", (c) => {
|
|
1838
|
+
const auth = c.get("authUser");
|
|
1839
|
+
if (!auth) {
|
|
1840
|
+
return vercelErr4(c, 401, "not_authenticated", "Authentication required");
|
|
1841
|
+
}
|
|
1842
|
+
const scope = resolveTeamScope(c, vs);
|
|
1843
|
+
if (!scope) {
|
|
1844
|
+
return vercelErr4(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
1845
|
+
}
|
|
1846
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
1847
|
+
if (!project) {
|
|
1848
|
+
return vercelErr4(c, 404, "not_found", "Project not found");
|
|
1849
|
+
}
|
|
1850
|
+
const domainName = decodeDomainParam(c.req.param("domain"));
|
|
1851
|
+
const existing = findDomainInProject(vs, project.uid, domainName);
|
|
1852
|
+
if (!existing) {
|
|
1853
|
+
return vercelErr4(c, 404, "not_found", "Domain not found");
|
|
1854
|
+
}
|
|
1855
|
+
vs.domains.delete(existing.id);
|
|
1856
|
+
return c.json({}, 200);
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
var ENV_TYPES = /* @__PURE__ */ new Set(["system", "encrypted", "plain", "secret", "sensitive"]);
|
|
1860
|
+
var TARGET_ENVS = ["production", "preview", "development"];
|
|
1861
|
+
function isTargetEnv(t) {
|
|
1862
|
+
return TARGET_ENVS.includes(t);
|
|
1863
|
+
}
|
|
1864
|
+
function vercelErr5(c, status, code, message) {
|
|
1865
|
+
return c.json({ error: { code, message } }, status);
|
|
1866
|
+
}
|
|
1867
|
+
function parseQueryBoolean(raw) {
|
|
1868
|
+
if (raw === void 0) return false;
|
|
1869
|
+
const v = raw.toLowerCase();
|
|
1870
|
+
return v === "true" || v === "1" || v === "yes";
|
|
1871
|
+
}
|
|
1872
|
+
function targetsOverlap(a, b) {
|
|
1873
|
+
const set = new Set(a);
|
|
1874
|
+
return b.some((t) => set.has(t));
|
|
1875
|
+
}
|
|
1876
|
+
function findEnvByKeyAndTargetsOverlap(vs, projectUid, key, targets, excludeId) {
|
|
1877
|
+
const list = vs.envVars.findBy("projectId", projectUid);
|
|
1878
|
+
return list.find(
|
|
1879
|
+
(e) => e.key === key && (excludeId === void 0 || e.id !== excludeId) && targetsOverlap(e.target, targets)
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
function parseTarget(raw) {
|
|
1883
|
+
if (!Array.isArray(raw) || raw.length === 0) return "invalid";
|
|
1884
|
+
const out = [];
|
|
1885
|
+
for (const t of raw) {
|
|
1886
|
+
if (typeof t !== "string" || !isTargetEnv(t)) return "invalid";
|
|
1887
|
+
out.push(t);
|
|
1888
|
+
}
|
|
1889
|
+
return out;
|
|
1890
|
+
}
|
|
1891
|
+
function parseType(raw) {
|
|
1892
|
+
if (typeof raw !== "string" || !ENV_TYPES.has(raw)) return "invalid";
|
|
1893
|
+
return raw;
|
|
1894
|
+
}
|
|
1895
|
+
function parseCustomEnvironmentIds(raw) {
|
|
1896
|
+
if (raw === void 0 || raw === null) return [];
|
|
1897
|
+
if (!Array.isArray(raw)) return "invalid";
|
|
1898
|
+
const ids = [];
|
|
1899
|
+
for (const x of raw) {
|
|
1900
|
+
if (typeof x !== "string") return "invalid";
|
|
1901
|
+
ids.push(x);
|
|
1902
|
+
}
|
|
1903
|
+
return ids;
|
|
1904
|
+
}
|
|
1905
|
+
function parseEnvRow(body) {
|
|
1906
|
+
const key = typeof body.key === "string" ? body.key : "";
|
|
1907
|
+
if (!key.trim()) {
|
|
1908
|
+
return { row: {}, error: "Missing required field: key" };
|
|
1909
|
+
}
|
|
1910
|
+
if (body.value === void 0) {
|
|
1911
|
+
return { row: {}, error: "Missing required field: value" };
|
|
1912
|
+
}
|
|
1913
|
+
if (typeof body.value !== "string") {
|
|
1914
|
+
return { row: {}, error: "Invalid value: value must be a string" };
|
|
1915
|
+
}
|
|
1916
|
+
const type = parseType(body.type);
|
|
1917
|
+
if (type === "invalid") {
|
|
1918
|
+
return { row: {}, error: "Invalid value: type must be one of system, encrypted, plain, secret, sensitive" };
|
|
1919
|
+
}
|
|
1920
|
+
const target = parseTarget(body.target);
|
|
1921
|
+
if (target === "invalid") {
|
|
1922
|
+
return { row: {}, error: "Invalid value: target must be a non-empty array of production, preview, development" };
|
|
1923
|
+
}
|
|
1924
|
+
const customEnvironmentIds = parseCustomEnvironmentIds(body.customEnvironmentIds);
|
|
1925
|
+
if (customEnvironmentIds === "invalid") {
|
|
1926
|
+
return { row: {}, error: "Invalid value: customEnvironmentIds must be an array of strings" };
|
|
1927
|
+
}
|
|
1928
|
+
let gitBranch;
|
|
1929
|
+
if (!("gitBranch" in body)) {
|
|
1930
|
+
gitBranch = null;
|
|
1931
|
+
} else if (body.gitBranch === null) {
|
|
1932
|
+
gitBranch = null;
|
|
1933
|
+
} else if (typeof body.gitBranch === "string") {
|
|
1934
|
+
gitBranch = body.gitBranch;
|
|
1935
|
+
} else {
|
|
1936
|
+
return { row: {}, error: "Invalid value: gitBranch must be a string or null" };
|
|
1937
|
+
}
|
|
1938
|
+
let comment;
|
|
1939
|
+
if (!("comment" in body)) {
|
|
1940
|
+
comment = null;
|
|
1941
|
+
} else if (body.comment === null) {
|
|
1942
|
+
comment = null;
|
|
1943
|
+
} else if (typeof body.comment === "string") {
|
|
1944
|
+
comment = body.comment;
|
|
1945
|
+
} else {
|
|
1946
|
+
return { row: {}, error: "Invalid value: comment must be a string or null" };
|
|
1947
|
+
}
|
|
1948
|
+
return {
|
|
1949
|
+
row: {
|
|
1950
|
+
key,
|
|
1951
|
+
value: body.value,
|
|
1952
|
+
type,
|
|
1953
|
+
target,
|
|
1954
|
+
gitBranch,
|
|
1955
|
+
customEnvironmentIds,
|
|
1956
|
+
comment,
|
|
1957
|
+
decrypted: false
|
|
1958
|
+
},
|
|
1959
|
+
error: null
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
function findEnvByUidInProject(vs, projectUid, uid) {
|
|
1963
|
+
const list = vs.envVars.findBy("projectId", projectUid);
|
|
1964
|
+
return list.find((e) => e.uid === uid);
|
|
1965
|
+
}
|
|
1966
|
+
function envRoutes({ app, store }) {
|
|
1967
|
+
const vs = getVercelStore(store);
|
|
1968
|
+
app.get("/v10/projects/:idOrName/env", (c) => {
|
|
1969
|
+
const scope = resolveTeamScope(c, vs);
|
|
1970
|
+
if (!scope) {
|
|
1971
|
+
return vercelErr5(c, 401, "not_authenticated", "Authentication required");
|
|
1972
|
+
}
|
|
1973
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
1974
|
+
if (!project) {
|
|
1975
|
+
return vercelErr5(c, 404, "not_found", "Project not found");
|
|
1976
|
+
}
|
|
1977
|
+
const decrypt = parseQueryBoolean(c.req.query("decrypt"));
|
|
1978
|
+
const gitBranchQ = c.req.query("gitBranch");
|
|
1979
|
+
const customEnvironmentId = c.req.query("customEnvironmentId");
|
|
1980
|
+
const customEnvironmentSlug = c.req.query("customEnvironmentSlug");
|
|
1981
|
+
let list = vs.envVars.findBy("projectId", project.uid);
|
|
1982
|
+
if (gitBranchQ !== void 0) {
|
|
1983
|
+
list = list.filter((e) => e.gitBranch === gitBranchQ);
|
|
1984
|
+
}
|
|
1985
|
+
if (customEnvironmentId !== void 0 && customEnvironmentId !== "") {
|
|
1986
|
+
list = list.filter((e) => e.customEnvironmentIds.includes(customEnvironmentId));
|
|
1987
|
+
}
|
|
1988
|
+
if (customEnvironmentSlug !== void 0 && customEnvironmentSlug !== "") {
|
|
1989
|
+
list = list.filter((e) => e.customEnvironmentIds.includes(customEnvironmentSlug));
|
|
1990
|
+
}
|
|
1991
|
+
const pagination = parseCursorPagination(c);
|
|
1992
|
+
const { items, pagination: pageMeta } = applyCursorPagination(list, pagination);
|
|
1993
|
+
return c.json({
|
|
1994
|
+
envs: items.map((i) => formatEnvVar(i, decrypt)),
|
|
1995
|
+
pagination: pageMeta
|
|
1996
|
+
});
|
|
1997
|
+
});
|
|
1998
|
+
app.post("/v10/projects/:idOrName/env", async (c) => {
|
|
1999
|
+
const auth = c.get("authUser");
|
|
2000
|
+
if (!auth) {
|
|
2001
|
+
return vercelErr5(c, 401, "not_authenticated", "Authentication required");
|
|
2002
|
+
}
|
|
2003
|
+
const scope = resolveTeamScope(c, vs);
|
|
2004
|
+
if (!scope) {
|
|
2005
|
+
return vercelErr5(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
2006
|
+
}
|
|
2007
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
2008
|
+
if (!project) {
|
|
2009
|
+
return vercelErr5(c, 404, "not_found", "Project not found");
|
|
2010
|
+
}
|
|
2011
|
+
const upsert = parseQueryBoolean(c.req.query("upsert"));
|
|
2012
|
+
const rawBody = await c.req.json().catch(() => null);
|
|
2013
|
+
let items = [];
|
|
2014
|
+
if (Array.isArray(rawBody)) {
|
|
2015
|
+
items = rawBody;
|
|
2016
|
+
} else if (rawBody && typeof rawBody === "object" && !Array.isArray(rawBody)) {
|
|
2017
|
+
items = [rawBody];
|
|
2018
|
+
} else {
|
|
2019
|
+
return vercelErr5(c, 400, "bad_request", "Invalid JSON body");
|
|
2020
|
+
}
|
|
2021
|
+
const created = [];
|
|
2022
|
+
const pending = [];
|
|
2023
|
+
for (const body of items) {
|
|
2024
|
+
const parsed = parseEnvRow(body);
|
|
2025
|
+
if (parsed.error) {
|
|
2026
|
+
return vercelErr5(c, 400, "bad_request", parsed.error);
|
|
2027
|
+
}
|
|
2028
|
+
const { row } = parsed;
|
|
2029
|
+
const existingDb = findEnvByKeyAndTargetsOverlap(vs, project.uid, row.key, row.target);
|
|
2030
|
+
const existingPending = pending.find(
|
|
2031
|
+
(e) => e.key === row.key && targetsOverlap(e.target, row.target)
|
|
2032
|
+
);
|
|
2033
|
+
if (upsert) {
|
|
2034
|
+
const toUpdate = existingDb ?? existingPending;
|
|
2035
|
+
if (toUpdate) {
|
|
2036
|
+
const updated = vs.envVars.update(toUpdate.id, {
|
|
2037
|
+
key: row.key,
|
|
2038
|
+
value: row.value,
|
|
2039
|
+
type: row.type,
|
|
2040
|
+
target: row.target,
|
|
2041
|
+
gitBranch: row.gitBranch,
|
|
2042
|
+
customEnvironmentIds: row.customEnvironmentIds,
|
|
2043
|
+
comment: row.comment
|
|
2044
|
+
});
|
|
2045
|
+
if (!updated) {
|
|
2046
|
+
return vercelErr5(c, 500, "internal_error", "Failed to update environment variable");
|
|
2047
|
+
}
|
|
2048
|
+
const idx = pending.findIndex((p) => p.id === updated.id);
|
|
2049
|
+
if (idx >= 0) pending[idx] = updated;
|
|
2050
|
+
else pending.push(updated);
|
|
2051
|
+
created.push(updated);
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
} else {
|
|
2055
|
+
if (existingDb || existingPending) {
|
|
2056
|
+
return vercelErr5(
|
|
2057
|
+
c,
|
|
2058
|
+
409,
|
|
2059
|
+
"env_already_exists",
|
|
2060
|
+
`An environment variable with key "${row.key}" and overlapping targets already exists`
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
const inserted = vs.envVars.insert({
|
|
2065
|
+
uid: generateUid("env"),
|
|
2066
|
+
projectId: project.uid,
|
|
2067
|
+
key: row.key,
|
|
2068
|
+
value: row.value,
|
|
2069
|
+
type: row.type,
|
|
2070
|
+
target: row.target,
|
|
2071
|
+
gitBranch: row.gitBranch,
|
|
2072
|
+
customEnvironmentIds: row.customEnvironmentIds,
|
|
2073
|
+
comment: row.comment,
|
|
2074
|
+
decrypted: row.decrypted
|
|
2075
|
+
});
|
|
2076
|
+
pending.push(inserted);
|
|
2077
|
+
created.push(inserted);
|
|
2078
|
+
}
|
|
2079
|
+
return c.json({ envs: created.map((e) => formatEnvVar(e, true)) });
|
|
2080
|
+
});
|
|
2081
|
+
app.get("/v10/projects/:idOrName/env/:id", (c) => {
|
|
2082
|
+
const scope = resolveTeamScope(c, vs);
|
|
2083
|
+
if (!scope) {
|
|
2084
|
+
return vercelErr5(c, 401, "not_authenticated", "Authentication required");
|
|
2085
|
+
}
|
|
2086
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
2087
|
+
if (!project) {
|
|
2088
|
+
return vercelErr5(c, 404, "not_found", "Project not found");
|
|
2089
|
+
}
|
|
2090
|
+
const env = findEnvByUidInProject(vs, project.uid, c.req.param("id"));
|
|
2091
|
+
if (!env) {
|
|
2092
|
+
return vercelErr5(c, 404, "not_found", "Environment variable not found");
|
|
2093
|
+
}
|
|
2094
|
+
const decrypt = parseQueryBoolean(c.req.query("decrypt"));
|
|
2095
|
+
return c.json(formatEnvVar(env, decrypt));
|
|
2096
|
+
});
|
|
2097
|
+
app.patch("/v9/projects/:idOrName/env/:id", async (c) => {
|
|
2098
|
+
const auth = c.get("authUser");
|
|
2099
|
+
if (!auth) {
|
|
2100
|
+
return vercelErr5(c, 401, "not_authenticated", "Authentication required");
|
|
2101
|
+
}
|
|
2102
|
+
const scope = resolveTeamScope(c, vs);
|
|
2103
|
+
if (!scope) {
|
|
2104
|
+
return vercelErr5(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
2105
|
+
}
|
|
2106
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
2107
|
+
if (!project) {
|
|
2108
|
+
return vercelErr5(c, 404, "not_found", "Project not found");
|
|
2109
|
+
}
|
|
2110
|
+
const existing = findEnvByUidInProject(vs, project.uid, c.req.param("id"));
|
|
2111
|
+
if (!existing) {
|
|
2112
|
+
return vercelErr5(c, 404, "not_found", "Environment variable not found");
|
|
2113
|
+
}
|
|
2114
|
+
const body = await parseJsonBody(c);
|
|
2115
|
+
const patch = {};
|
|
2116
|
+
if ("key" in body) {
|
|
2117
|
+
if (typeof body.key !== "string" || !body.key.trim()) {
|
|
2118
|
+
return vercelErr5(c, 400, "bad_request", "Invalid value: key must be a non-empty string");
|
|
2119
|
+
}
|
|
2120
|
+
patch.key = body.key;
|
|
2121
|
+
}
|
|
2122
|
+
if ("value" in body) {
|
|
2123
|
+
if (typeof body.value !== "string") {
|
|
2124
|
+
return vercelErr5(c, 400, "bad_request", "Invalid value: value must be a string");
|
|
2125
|
+
}
|
|
2126
|
+
patch.value = body.value;
|
|
2127
|
+
}
|
|
2128
|
+
if ("type" in body) {
|
|
2129
|
+
const t = parseType(body.type);
|
|
2130
|
+
if (t === "invalid") {
|
|
2131
|
+
return vercelErr5(c, 400, "bad_request", "Invalid value: type must be one of system, encrypted, plain, secret, sensitive");
|
|
2132
|
+
}
|
|
2133
|
+
patch.type = t;
|
|
2134
|
+
}
|
|
2135
|
+
if ("target" in body) {
|
|
2136
|
+
const t = parseTarget(body.target);
|
|
2137
|
+
if (t === "invalid") {
|
|
2138
|
+
return vercelErr5(c, 400, "bad_request", "Invalid value: target must be a non-empty array of production, preview, development");
|
|
2139
|
+
}
|
|
2140
|
+
patch.target = t;
|
|
2141
|
+
}
|
|
2142
|
+
if ("gitBranch" in body) {
|
|
2143
|
+
if (body.gitBranch === null) {
|
|
2144
|
+
patch.gitBranch = null;
|
|
2145
|
+
} else if (typeof body.gitBranch === "string") {
|
|
2146
|
+
patch.gitBranch = body.gitBranch;
|
|
2147
|
+
} else {
|
|
2148
|
+
return vercelErr5(c, 400, "bad_request", "Invalid value: gitBranch must be a string or null");
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
if ("customEnvironmentIds" in body) {
|
|
2152
|
+
const ids = parseCustomEnvironmentIds(body.customEnvironmentIds);
|
|
2153
|
+
if (ids === "invalid") {
|
|
2154
|
+
return vercelErr5(c, 400, "bad_request", "Invalid value: customEnvironmentIds must be an array of strings");
|
|
2155
|
+
}
|
|
2156
|
+
patch.customEnvironmentIds = ids;
|
|
2157
|
+
}
|
|
2158
|
+
if ("comment" in body) {
|
|
2159
|
+
if (body.comment === null) {
|
|
2160
|
+
patch.comment = null;
|
|
2161
|
+
} else if (typeof body.comment === "string") {
|
|
2162
|
+
patch.comment = body.comment;
|
|
2163
|
+
} else {
|
|
2164
|
+
return vercelErr5(c, 400, "bad_request", "Invalid value: comment must be a string or null");
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
const nextKey = patch.key ?? existing.key;
|
|
2168
|
+
const nextTarget = patch.target ?? existing.target;
|
|
2169
|
+
const conflict = findEnvByKeyAndTargetsOverlap(vs, project.uid, nextKey, nextTarget, existing.id);
|
|
2170
|
+
if (conflict) {
|
|
2171
|
+
return vercelErr5(
|
|
2172
|
+
c,
|
|
2173
|
+
409,
|
|
2174
|
+
"env_already_exists",
|
|
2175
|
+
`An environment variable with key "${nextKey}" and overlapping targets already exists`
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
const updated = vs.envVars.update(existing.id, patch);
|
|
2179
|
+
if (!updated) {
|
|
2180
|
+
return vercelErr5(c, 500, "internal_error", "Failed to update environment variable");
|
|
2181
|
+
}
|
|
2182
|
+
return c.json(formatEnvVar(updated, true));
|
|
2183
|
+
});
|
|
2184
|
+
app.delete("/v9/projects/:idOrName/env/:id", (c) => {
|
|
2185
|
+
const auth = c.get("authUser");
|
|
2186
|
+
if (!auth) {
|
|
2187
|
+
return vercelErr5(c, 401, "not_authenticated", "Authentication required");
|
|
2188
|
+
}
|
|
2189
|
+
const scope = resolveTeamScope(c, vs);
|
|
2190
|
+
if (!scope) {
|
|
2191
|
+
return vercelErr5(c, 400, "bad_request", "Could not resolve team or account scope");
|
|
2192
|
+
}
|
|
2193
|
+
const project = lookupProject(vs, c.req.param("idOrName"), scope.accountId);
|
|
2194
|
+
if (!project) {
|
|
2195
|
+
return vercelErr5(c, 404, "not_found", "Project not found");
|
|
2196
|
+
}
|
|
2197
|
+
const existing = findEnvByUidInProject(vs, project.uid, c.req.param("id"));
|
|
2198
|
+
if (!existing) {
|
|
2199
|
+
return vercelErr5(c, 404, "not_found", "Environment variable not found");
|
|
2200
|
+
}
|
|
2201
|
+
const snapshot = formatEnvVar(existing, true);
|
|
2202
|
+
vs.envVars.delete(existing.id);
|
|
2203
|
+
return c.json(snapshot, 200);
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
var PENDING_CODE_TTL_MS = 10 * 60 * 1e3;
|
|
2207
|
+
function getPendingCodes(store) {
|
|
2208
|
+
let map = store.getData("vercel.oauth.pendingCodes");
|
|
2209
|
+
if (!map) {
|
|
2210
|
+
map = /* @__PURE__ */ new Map();
|
|
2211
|
+
store.setData("vercel.oauth.pendingCodes", map);
|
|
2212
|
+
}
|
|
2213
|
+
return map;
|
|
2214
|
+
}
|
|
2215
|
+
function isPendingCodeExpired(p) {
|
|
2216
|
+
return Date.now() - p.created_at > PENDING_CODE_TTL_MS;
|
|
2217
|
+
}
|
|
2218
|
+
var SERVICE_LABEL = "Vercel";
|
|
2219
|
+
function oauthRoutes({ app, store, tokenMap }) {
|
|
2220
|
+
const vs = getVercelStore(store);
|
|
2221
|
+
app.get("/oauth/authorize", (c) => {
|
|
2222
|
+
const client_id = c.req.query("client_id") ?? "";
|
|
2223
|
+
const redirect_uri = c.req.query("redirect_uri") ?? "";
|
|
2224
|
+
const scope = c.req.query("scope") ?? "";
|
|
2225
|
+
const state = c.req.query("state") ?? "";
|
|
2226
|
+
const code_challenge = c.req.query("code_challenge") ?? "";
|
|
2227
|
+
const code_challenge_method = c.req.query("code_challenge_method") ?? "";
|
|
2228
|
+
const integrationsConfigured = vs.integrations.all().length > 0;
|
|
2229
|
+
let integrationName = "";
|
|
2230
|
+
if (integrationsConfigured) {
|
|
2231
|
+
const integration = vs.integrations.findOneBy("client_id", client_id);
|
|
2232
|
+
if (!integration) {
|
|
2233
|
+
return c.html(renderErrorPage("Application not found", `The client_id '${client_id}' is not registered.`, SERVICE_LABEL), 400);
|
|
2234
|
+
}
|
|
2235
|
+
if (redirect_uri && !matchesRedirectUri(redirect_uri, integration.redirect_uris)) {
|
|
2236
|
+
console.warn(`[OAuth] redirect_uri mismatch: got "${redirect_uri}", registered: ${JSON.stringify(integration.redirect_uris)}`);
|
|
2237
|
+
return c.html(renderErrorPage("Redirect URI mismatch", "The redirect_uri is not registered for this application.", SERVICE_LABEL), 400);
|
|
2238
|
+
}
|
|
2239
|
+
integrationName = integration.name;
|
|
2240
|
+
}
|
|
2241
|
+
const subtitleText = integrationName ? `Authorize <strong>${escapeHtml(integrationName)}</strong> to access your account.` : "Choose a seeded user to continue.";
|
|
2242
|
+
const users = vs.users.all();
|
|
2243
|
+
const userButtons = users.map((user) => {
|
|
2244
|
+
const u = formatUser(user);
|
|
2245
|
+
return renderUserButton({
|
|
2246
|
+
letter: (u.username[0] ?? "?").toUpperCase(),
|
|
2247
|
+
login: u.username,
|
|
2248
|
+
name: u.name ?? void 0,
|
|
2249
|
+
email: u.email,
|
|
2250
|
+
formAction: "/oauth/authorize/callback",
|
|
2251
|
+
hiddenFields: {
|
|
2252
|
+
username: u.username,
|
|
2253
|
+
redirect_uri,
|
|
2254
|
+
scope,
|
|
2255
|
+
state,
|
|
2256
|
+
client_id,
|
|
2257
|
+
code_challenge,
|
|
2258
|
+
code_challenge_method
|
|
2259
|
+
}
|
|
2260
|
+
});
|
|
2261
|
+
}).join("\n");
|
|
2262
|
+
const body = users.length === 0 ? '<p class="empty">No users in the emulator store.</p>' : userButtons;
|
|
2263
|
+
return c.html(renderCardPage("Sign in to Vercel", subtitleText, body, SERVICE_LABEL));
|
|
2264
|
+
});
|
|
2265
|
+
app.post("/oauth/authorize/callback", async (c) => {
|
|
2266
|
+
const body = await c.req.parseBody();
|
|
2267
|
+
const username = bodyStr(body.username);
|
|
2268
|
+
const redirect_uri = bodyStr(body.redirect_uri);
|
|
2269
|
+
const scope = bodyStr(body.scope);
|
|
2270
|
+
const state = bodyStr(body.state);
|
|
2271
|
+
const client_id = bodyStr(body.client_id);
|
|
2272
|
+
const code = randomBytes2(20).toString("hex");
|
|
2273
|
+
const code_challenge = bodyStr(body.code_challenge);
|
|
2274
|
+
const code_challenge_method = bodyStr(body.code_challenge_method);
|
|
2275
|
+
const pendingCodes = getPendingCodes(store);
|
|
2276
|
+
pendingCodes.set(code, {
|
|
2277
|
+
username,
|
|
2278
|
+
scope,
|
|
2279
|
+
redirectUri: redirect_uri,
|
|
2280
|
+
clientId: client_id,
|
|
2281
|
+
codeChallenge: code_challenge || null,
|
|
2282
|
+
codeChallengeMethod: code_challenge_method || null,
|
|
2283
|
+
created_at: Date.now()
|
|
2284
|
+
});
|
|
2285
|
+
debug("vercel.oauth", `[Vercel callback] generated code: ${code.slice(0, 8)}... for username=${username}, challenge=${code_challenge ? "present" : "none"}, pendingCodes size: ${pendingCodes.size}`);
|
|
2286
|
+
const url = new URL(redirect_uri);
|
|
2287
|
+
url.searchParams.set("code", code);
|
|
2288
|
+
if (state !== "") url.searchParams.set("state", state);
|
|
2289
|
+
debug("vercel.oauth", `[Vercel callback] redirecting to: ${url.toString().slice(0, 120)}...`);
|
|
2290
|
+
return c.redirect(url.toString(), 302);
|
|
2291
|
+
});
|
|
2292
|
+
app.post("/login/oauth/token", async (c) => {
|
|
2293
|
+
const contentType = c.req.header("Content-Type") ?? "";
|
|
2294
|
+
const pendingCodes = getPendingCodes(store);
|
|
2295
|
+
debug("vercel.oauth", `[Vercel token] Content-Type: ${contentType}`);
|
|
2296
|
+
debug("vercel.oauth", `[Vercel token] pendingCodes size: ${pendingCodes.size}`);
|
|
2297
|
+
debug("vercel.oauth", `[Vercel token] pendingCodes keys: ${[...pendingCodes.keys()].map((k) => k.slice(0, 8) + "...").join(", ")}`);
|
|
2298
|
+
const rawText = await c.req.text();
|
|
2299
|
+
debug("vercel.oauth", `[Vercel token] raw body: ${rawText.slice(0, 500)}`);
|
|
2300
|
+
let body;
|
|
2301
|
+
if (contentType.includes("application/json")) {
|
|
2302
|
+
try {
|
|
2303
|
+
body = JSON.parse(rawText);
|
|
2304
|
+
} catch {
|
|
2305
|
+
body = {};
|
|
2306
|
+
}
|
|
2307
|
+
} else {
|
|
2308
|
+
body = Object.fromEntries(new URLSearchParams(rawText));
|
|
2309
|
+
}
|
|
2310
|
+
debug("vercel.oauth", `[Vercel token] parsed keys: ${Object.keys(body).join(", ")}`);
|
|
2311
|
+
const code = typeof body.code === "string" ? body.code : "";
|
|
2312
|
+
const redirect_uri = typeof body.redirect_uri === "string" ? body.redirect_uri : "";
|
|
2313
|
+
const code_verifier = typeof body.code_verifier === "string" ? body.code_verifier : void 0;
|
|
2314
|
+
const bodyClientId = typeof body.client_id === "string" ? body.client_id : "";
|
|
2315
|
+
const bodyClientSecret = typeof body.client_secret === "string" ? body.client_secret : "";
|
|
2316
|
+
debug("vercel.oauth", `[Vercel token] code: ${code.slice(0, 8)}... (len=${code.length})`);
|
|
2317
|
+
debug("vercel.oauth", `[Vercel token] client_id: ${bodyClientId}`);
|
|
2318
|
+
debug("vercel.oauth", `[Vercel token] client_secret: ${bodyClientSecret.slice(0, 4)}****`);
|
|
2319
|
+
debug("vercel.oauth", `[Vercel token] code_verifier: ${code_verifier ? code_verifier.slice(0, 8) + "..." : "undefined"}`);
|
|
2320
|
+
const integrationsConfigured = vs.integrations.all().length > 0;
|
|
2321
|
+
if (integrationsConfigured) {
|
|
2322
|
+
const integration = vs.integrations.findOneBy("client_id", bodyClientId);
|
|
2323
|
+
if (!integration) {
|
|
2324
|
+
debug("vercel.oauth", `[Vercel token] REJECTED: client_id not found`);
|
|
2325
|
+
return c.json({ error: "invalid_client", error_description: "The client_id and/or client_secret passed are incorrect." }, 401);
|
|
2326
|
+
}
|
|
2327
|
+
if (!constantTimeSecretEqual(bodyClientSecret, integration.client_secret)) {
|
|
2328
|
+
debug("vercel.oauth", `[Vercel token] REJECTED: client_secret mismatch`);
|
|
2329
|
+
return c.json({ error: "invalid_client", error_description: "The client_id and/or client_secret passed are incorrect." }, 401);
|
|
2330
|
+
}
|
|
2331
|
+
debug("vercel.oauth", `[Vercel token] client credentials OK (${integration.name})`);
|
|
2332
|
+
}
|
|
2333
|
+
const pending = pendingCodes.get(code);
|
|
2334
|
+
if (!pending) {
|
|
2335
|
+
debug("vercel.oauth", `[Vercel token] REJECTED: code not found in pendingCodes`);
|
|
2336
|
+
return c.json(
|
|
2337
|
+
{ error: "invalid_grant", error_description: "The code passed is incorrect or expired." },
|
|
2338
|
+
400
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
if (isPendingCodeExpired(pending)) {
|
|
2342
|
+
debug("vercel.oauth", `[Vercel token] REJECTED: code expired`);
|
|
2343
|
+
pendingCodes.delete(code);
|
|
2344
|
+
return c.json(
|
|
2345
|
+
{ error: "invalid_grant", error_description: "The code passed is incorrect or expired." },
|
|
2346
|
+
400
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
debug("vercel.oauth", `[Vercel token] code valid, username=${pending.username}, scope=${pending.scope}`);
|
|
2350
|
+
if (redirect_uri && pending.redirectUri && redirect_uri !== pending.redirectUri) {
|
|
2351
|
+
debug("vercel.oauth", `[Vercel token] REJECTED: redirect_uri mismatch (got "${redirect_uri}", expected "${pending.redirectUri}")`);
|
|
2352
|
+
pendingCodes.delete(code);
|
|
2353
|
+
return c.json(
|
|
2354
|
+
{ error: "invalid_grant", error_description: "The redirect_uri does not match the one used during authorization." },
|
|
2355
|
+
400
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
if (pending.codeChallenge != null) {
|
|
2359
|
+
if (code_verifier === void 0) {
|
|
2360
|
+
return c.json(
|
|
2361
|
+
{ error: "invalid_grant", error_description: "PKCE verification failed." },
|
|
2362
|
+
400
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
const method = (pending.codeChallengeMethod ?? "plain").toLowerCase();
|
|
2366
|
+
if (method === "s256") {
|
|
2367
|
+
const expected = createHash("sha256").update(code_verifier).digest("base64url");
|
|
2368
|
+
if (expected !== pending.codeChallenge) {
|
|
2369
|
+
return c.json(
|
|
2370
|
+
{ error: "invalid_grant", error_description: "PKCE verification failed." },
|
|
2371
|
+
400
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
} else if (method === "plain") {
|
|
2375
|
+
if (code_verifier !== pending.codeChallenge) {
|
|
2376
|
+
return c.json(
|
|
2377
|
+
{ error: "invalid_grant", error_description: "PKCE verification failed." },
|
|
2378
|
+
400
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
} else {
|
|
2382
|
+
return c.json(
|
|
2383
|
+
{ error: "invalid_grant", error_description: "PKCE verification failed." },
|
|
2384
|
+
400
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
debug("vercel.oauth", `[Vercel token] PKCE OK (challenge=${pending.codeChallenge ? "present" : "none"})`);
|
|
2389
|
+
pendingCodes.delete(code);
|
|
2390
|
+
const user = vs.users.findOneBy("username", pending.username);
|
|
2391
|
+
if (!user) {
|
|
2392
|
+
debug("vercel.oauth", `[Vercel token] REJECTED: user "${pending.username}" not found`);
|
|
2393
|
+
return c.json(
|
|
2394
|
+
{ error: "invalid_grant", error_description: "The user associated with this code was not found." },
|
|
2395
|
+
400
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
const token = "vercel_" + randomBytes2(20).toString("base64url");
|
|
2399
|
+
const scopes = pending.scope ? pending.scope.split(/[,\s]+/).filter(Boolean) : [];
|
|
2400
|
+
if (tokenMap) {
|
|
2401
|
+
tokenMap.set(token, { login: user.username, id: user.id, scopes });
|
|
2402
|
+
}
|
|
2403
|
+
debug("vercel.oauth", `[Vercel token] SUCCESS: issued token for ${user.username} (scopes: ${scopes.join(",") || "none"})`);
|
|
2404
|
+
return c.json({
|
|
2405
|
+
access_token: token,
|
|
2406
|
+
token_type: "Bearer",
|
|
2407
|
+
scope: pending.scope || ""
|
|
2408
|
+
});
|
|
2409
|
+
});
|
|
2410
|
+
app.get("/login/oauth/userinfo", (c) => {
|
|
2411
|
+
const authUser = c.get("authUser");
|
|
2412
|
+
if (!authUser) {
|
|
2413
|
+
return c.json({ error: { code: "unauthorized", message: "Authentication required" } }, 401);
|
|
2414
|
+
}
|
|
2415
|
+
const user = vs.users.findOneBy("username", authUser.login);
|
|
2416
|
+
if (!user) {
|
|
2417
|
+
return c.json({ error: { code: "unauthorized", message: "Authentication required" } }, 401);
|
|
2418
|
+
}
|
|
2419
|
+
return c.json({
|
|
2420
|
+
sub: user.uid,
|
|
2421
|
+
email: user.email,
|
|
2422
|
+
name: user.name,
|
|
2423
|
+
preferred_username: user.username,
|
|
2424
|
+
email_verified: true,
|
|
2425
|
+
picture: user.avatar
|
|
2426
|
+
});
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
function vercelErr6(c, status, code, message) {
|
|
2430
|
+
return c.json({ error: { code, message } }, status);
|
|
2431
|
+
}
|
|
2432
|
+
function apiKeysRoutes({ app, store, tokenMap }) {
|
|
2433
|
+
const vs = getVercelStore(store);
|
|
2434
|
+
app.post("/v1/api-keys", async (c) => {
|
|
2435
|
+
const auth = c.get("authUser");
|
|
2436
|
+
if (!auth) {
|
|
2437
|
+
return vercelErr6(c, 401, "not_authenticated", "Authentication required");
|
|
2438
|
+
}
|
|
2439
|
+
const user = vs.users.findOneBy("username", auth.login);
|
|
2440
|
+
if (!user) {
|
|
2441
|
+
return vercelErr6(c, 403, "forbidden", "User not found");
|
|
2442
|
+
}
|
|
2443
|
+
const teamId = c.req.query("teamId") ?? null;
|
|
2444
|
+
const body = await parseJsonBody(c);
|
|
2445
|
+
const name = typeof body.name === "string" ? body.name : "API Key";
|
|
2446
|
+
const tokenString = `vercel_api_${randomBytes3(24).toString("base64url")}`;
|
|
2447
|
+
const uid = generateUid("ak");
|
|
2448
|
+
vs.apiKeys.insert({
|
|
2449
|
+
uid,
|
|
2450
|
+
name,
|
|
2451
|
+
teamId,
|
|
2452
|
+
userId: user.uid,
|
|
2453
|
+
tokenString
|
|
2454
|
+
});
|
|
2455
|
+
if (tokenMap) {
|
|
2456
|
+
tokenMap.set(tokenString, { login: user.username, id: user.id, scopes: [] });
|
|
2457
|
+
}
|
|
2458
|
+
return c.json({
|
|
2459
|
+
apiKeyString: tokenString,
|
|
2460
|
+
apiKey: {
|
|
2461
|
+
id: uid,
|
|
2462
|
+
name,
|
|
2463
|
+
teamId,
|
|
2464
|
+
createdAt: Date.now()
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
});
|
|
2468
|
+
app.get("/v1/api-keys", (c) => {
|
|
2469
|
+
const auth = c.get("authUser");
|
|
2470
|
+
if (!auth) {
|
|
2471
|
+
return vercelErr6(c, 401, "not_authenticated", "Authentication required");
|
|
2472
|
+
}
|
|
2473
|
+
const user = vs.users.findOneBy("username", auth.login);
|
|
2474
|
+
if (!user) {
|
|
2475
|
+
return vercelErr6(c, 403, "forbidden", "User not found");
|
|
2476
|
+
}
|
|
2477
|
+
const teamId = c.req.query("teamId") ?? null;
|
|
2478
|
+
const keys = vs.apiKeys.all().filter((k) => {
|
|
2479
|
+
if (k.userId !== user.uid) return false;
|
|
2480
|
+
if (teamId && k.teamId !== teamId) return false;
|
|
2481
|
+
return true;
|
|
2482
|
+
});
|
|
2483
|
+
return c.json({
|
|
2484
|
+
keys: keys.map((k) => ({
|
|
2485
|
+
id: k.uid,
|
|
2486
|
+
name: k.name,
|
|
2487
|
+
teamId: k.teamId,
|
|
2488
|
+
createdAt: k.created_at
|
|
2489
|
+
}))
|
|
2490
|
+
});
|
|
2491
|
+
});
|
|
2492
|
+
app.delete("/v1/api-keys/:keyId", (c) => {
|
|
2493
|
+
const auth = c.get("authUser");
|
|
2494
|
+
if (!auth) {
|
|
2495
|
+
return vercelErr6(c, 401, "not_authenticated", "Authentication required");
|
|
2496
|
+
}
|
|
2497
|
+
const user = vs.users.findOneBy("username", auth.login);
|
|
2498
|
+
if (!user) {
|
|
2499
|
+
return vercelErr6(c, 403, "forbidden", "User not found");
|
|
2500
|
+
}
|
|
2501
|
+
const keyId = c.req.param("keyId");
|
|
2502
|
+
const key = vs.apiKeys.findOneBy("uid", keyId);
|
|
2503
|
+
if (!key) {
|
|
2504
|
+
return vercelErr6(c, 404, "not_found", "API key not found");
|
|
2505
|
+
}
|
|
2506
|
+
if (key.userId !== user.uid) {
|
|
2507
|
+
return vercelErr6(c, 403, "forbidden", "Not authorized to delete this API key");
|
|
2508
|
+
}
|
|
2509
|
+
if (tokenMap) {
|
|
2510
|
+
tokenMap.delete(key.tokenString);
|
|
2511
|
+
}
|
|
2512
|
+
vs.apiKeys.delete(key.id);
|
|
2513
|
+
return c.json({});
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
function seedDefaults(store, _baseUrl) {
|
|
2517
|
+
const vs = getVercelStore(store);
|
|
2518
|
+
vs.users.insert({
|
|
2519
|
+
uid: generateUid("user"),
|
|
2520
|
+
email: "admin@localhost",
|
|
2521
|
+
username: "admin",
|
|
2522
|
+
name: "Admin",
|
|
2523
|
+
avatar: null,
|
|
2524
|
+
defaultTeamId: null,
|
|
2525
|
+
softBlock: null,
|
|
2526
|
+
billing: { plan: "hobby", period: null, trial: null, cancelation: null, addons: null },
|
|
2527
|
+
resourceConfig: { nodeType: "Edge Functions", concurrentBuilds: 1 },
|
|
2528
|
+
stagingPrefix: "staging",
|
|
2529
|
+
version: null
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
function seedFromConfig(store, baseUrl, config) {
|
|
2533
|
+
const vs = getVercelStore(store);
|
|
2534
|
+
if (config.users) {
|
|
2535
|
+
for (const u of config.users) {
|
|
2536
|
+
const existing = vs.users.findOneBy("username", u.username);
|
|
2537
|
+
if (existing) continue;
|
|
2538
|
+
vs.users.insert({
|
|
2539
|
+
uid: generateUid("user"),
|
|
2540
|
+
email: u.email ?? `${u.username}@localhost`,
|
|
2541
|
+
username: u.username,
|
|
2542
|
+
name: u.name ?? null,
|
|
2543
|
+
avatar: null,
|
|
2544
|
+
defaultTeamId: null,
|
|
2545
|
+
softBlock: null,
|
|
2546
|
+
billing: { plan: "hobby", period: null, trial: null, cancelation: null, addons: null },
|
|
2547
|
+
resourceConfig: { nodeType: "Edge Functions", concurrentBuilds: 1 },
|
|
2548
|
+
stagingPrefix: "staging",
|
|
2549
|
+
version: null
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
if (config.teams) {
|
|
2554
|
+
for (const t of config.teams) {
|
|
2555
|
+
const existing = vs.teams.findOneBy("slug", t.slug);
|
|
2556
|
+
if (existing) continue;
|
|
2557
|
+
const firstUser = vs.users.all()[0];
|
|
2558
|
+
const creatorId = firstUser?.uid ?? "unknown";
|
|
2559
|
+
const team = vs.teams.insert({
|
|
2560
|
+
uid: generateUid("team"),
|
|
2561
|
+
slug: t.slug,
|
|
2562
|
+
name: t.name ?? t.slug,
|
|
2563
|
+
avatar: null,
|
|
2564
|
+
description: t.description ?? null,
|
|
2565
|
+
creatorId,
|
|
2566
|
+
membership: { confirmed: true, role: "OWNER" },
|
|
2567
|
+
billing: { plan: "pro", period: null, trial: null, cancelation: null, addons: null },
|
|
2568
|
+
resourceConfig: { nodeType: "Edge Functions", concurrentBuilds: 1 },
|
|
2569
|
+
stagingPrefix: "staging"
|
|
2570
|
+
});
|
|
2571
|
+
for (const u of vs.users.all()) {
|
|
2572
|
+
const role = u.uid === creatorId ? "OWNER" : "MEMBER";
|
|
2573
|
+
vs.teamMembers.insert({
|
|
2574
|
+
teamId: team.uid,
|
|
2575
|
+
userId: u.uid,
|
|
2576
|
+
role,
|
|
2577
|
+
confirmed: true,
|
|
2578
|
+
joinedFrom: "seed"
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
if (config.projects) {
|
|
2584
|
+
for (const p of config.projects) {
|
|
2585
|
+
let accountId;
|
|
2586
|
+
if (p.team) {
|
|
2587
|
+
const team = vs.teams.findOneBy("slug", p.team);
|
|
2588
|
+
if (!team) continue;
|
|
2589
|
+
accountId = team.uid;
|
|
2590
|
+
} else {
|
|
2591
|
+
const user = vs.users.all()[0];
|
|
2592
|
+
if (!user) continue;
|
|
2593
|
+
accountId = user.uid;
|
|
2594
|
+
}
|
|
2595
|
+
const existingByName = vs.projects.findBy("name", p.name);
|
|
2596
|
+
if (existingByName.some((proj) => proj.accountId === accountId)) continue;
|
|
2597
|
+
const project = vs.projects.insert({
|
|
2598
|
+
uid: generateUid("prj"),
|
|
2599
|
+
name: p.name,
|
|
2600
|
+
accountId,
|
|
2601
|
+
framework: p.framework ?? null,
|
|
2602
|
+
buildCommand: p.buildCommand ?? null,
|
|
2603
|
+
devCommand: null,
|
|
2604
|
+
installCommand: null,
|
|
2605
|
+
outputDirectory: p.outputDirectory ?? null,
|
|
2606
|
+
rootDirectory: p.rootDirectory ?? null,
|
|
2607
|
+
commandForIgnoringBuildStep: null,
|
|
2608
|
+
nodeVersion: p.nodeVersion ?? "20.x",
|
|
2609
|
+
serverlessFunctionRegion: null,
|
|
2610
|
+
publicSource: false,
|
|
2611
|
+
autoAssignCustomDomains: true,
|
|
2612
|
+
autoAssignCustomDomainsUpdatedBy: null,
|
|
2613
|
+
gitForkProtection: true,
|
|
2614
|
+
sourceFilesOutsideRootDirectory: false,
|
|
2615
|
+
live: true,
|
|
2616
|
+
link: null,
|
|
2617
|
+
latestDeployments: [],
|
|
2618
|
+
targets: {},
|
|
2619
|
+
protectionBypass: {},
|
|
2620
|
+
passwordProtection: null,
|
|
2621
|
+
ssoProtection: null,
|
|
2622
|
+
trustedIps: null,
|
|
2623
|
+
connectConfigurationId: null,
|
|
2624
|
+
gitComments: { onPullRequest: true, onCommit: false },
|
|
2625
|
+
webAnalytics: null,
|
|
2626
|
+
speedInsights: null,
|
|
2627
|
+
oidcTokenConfig: null,
|
|
2628
|
+
tier: "hobby"
|
|
2629
|
+
});
|
|
2630
|
+
if (p.envVars) {
|
|
2631
|
+
for (const ev of p.envVars) {
|
|
2632
|
+
vs.envVars.insert({
|
|
2633
|
+
uid: generateUid("env"),
|
|
2634
|
+
projectId: project.uid,
|
|
2635
|
+
key: ev.key,
|
|
2636
|
+
value: ev.value,
|
|
2637
|
+
type: ev.type ?? "encrypted",
|
|
2638
|
+
target: ev.target ?? ["production", "preview", "development"],
|
|
2639
|
+
gitBranch: null,
|
|
2640
|
+
customEnvironmentIds: [],
|
|
2641
|
+
comment: null,
|
|
2642
|
+
decrypted: false
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
if (config.integrations) {
|
|
2649
|
+
for (const integ of config.integrations) {
|
|
2650
|
+
const existing = vs.integrations.findOneBy("client_id", integ.client_id);
|
|
2651
|
+
if (existing) continue;
|
|
2652
|
+
vs.integrations.insert({
|
|
2653
|
+
client_id: integ.client_id,
|
|
2654
|
+
client_secret: integ.client_secret,
|
|
2655
|
+
name: integ.name,
|
|
2656
|
+
redirect_uris: integ.redirect_uris
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
var vercelPlugin = {
|
|
2662
|
+
name: "vercel",
|
|
2663
|
+
register(app, store, webhooks, baseUrl, tokenMap) {
|
|
2664
|
+
const ctx = { app, store, webhooks, baseUrl, tokenMap };
|
|
2665
|
+
oauthRoutes(ctx);
|
|
2666
|
+
userRoutes(ctx);
|
|
2667
|
+
projectsRoutes(ctx);
|
|
2668
|
+
deploymentsRoutes(ctx);
|
|
2669
|
+
domainsRoutes(ctx);
|
|
2670
|
+
envRoutes(ctx);
|
|
2671
|
+
apiKeysRoutes(ctx);
|
|
2672
|
+
},
|
|
2673
|
+
seed(store, baseUrl) {
|
|
2674
|
+
seedDefaults(store, baseUrl);
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
var index_default = vercelPlugin;
|
|
2678
|
+
export {
|
|
2679
|
+
index_default as default,
|
|
2680
|
+
getVercelStore,
|
|
2681
|
+
seedFromConfig,
|
|
2682
|
+
vercelPlugin
|
|
2683
|
+
};
|
|
2684
|
+
//# sourceMappingURL=dist-JYDZIVC6.js.map
|