lakebed 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/package.json +1 -1
- package/src/anonymous-server.js +883 -36
- package/src/anonymous.js +73 -3
- package/src/cli.js +106 -35
- package/src/client.js +15 -0
- package/src/version.js +1 -1
package/src/anonymous-server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import {
|
|
4
4
|
createClaimToken,
|
|
@@ -22,7 +22,48 @@ function dayWindowStart() {
|
|
|
22
22
|
return `${new Date().toISOString().slice(0, 10)}T00:00:00.000Z`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function
|
|
25
|
+
function serverEnvSecretFromEnv(env = process.env) {
|
|
26
|
+
return env.LAKEBED_SERVER_ENV_SECRET ?? env.LAKEBED_SESSION_SECRET ?? env.SPAN_SESSION_SECRET ?? "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function serverEnvEncryptionKey(secret) {
|
|
30
|
+
return createHash("sha256").update(String(secret)).digest();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function encryptServerEnvValue(value, secret) {
|
|
34
|
+
if (!secret) {
|
|
35
|
+
return `plain:${Buffer.from(value, "utf8").toString("base64")}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const iv = randomBytes(12);
|
|
39
|
+
const cipher = createCipheriv("aes-256-gcm", serverEnvEncryptionKey(secret), iv);
|
|
40
|
+
const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
41
|
+
const tag = cipher.getAuthTag();
|
|
42
|
+
return `v1:${iv.toString("base64url")}:${tag.toString("base64url")}:${ciphertext.toString("base64url")}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function decryptServerEnvValue(value, secret) {
|
|
46
|
+
const stored = String(value ?? "");
|
|
47
|
+
if (stored.startsWith("plain:")) {
|
|
48
|
+
return Buffer.from(stored.slice("plain:".length), "base64").toString("utf8");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!stored.startsWith("v1:")) {
|
|
52
|
+
return stored;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!secret) {
|
|
56
|
+
throw new Error("LAKEBED_SERVER_ENV_SECRET is required to decrypt hosted server env.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const [, ivBase64, tagBase64, ciphertextBase64] = stored.split(":");
|
|
60
|
+
const decipher = createDecipheriv("aes-256-gcm", serverEnvEncryptionKey(secret), Buffer.from(ivBase64, "base64url"));
|
|
61
|
+
decipher.setAuthTag(Buffer.from(tagBase64, "base64url"));
|
|
62
|
+
return Buffer.concat([decipher.update(Buffer.from(ciphertextBase64, "base64url")), decipher.final()]).toString("utf8");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function html(title, basePath, { clientBundleHash, shooBaseUrl } = {}) {
|
|
66
|
+
const clientScriptUrl = `${basePath}/client.js${clientBundleHash ? `?v=${encodeURIComponent(clientBundleHash)}` : ""}`;
|
|
26
67
|
return `<!doctype html>
|
|
27
68
|
<html lang="en">
|
|
28
69
|
<head>
|
|
@@ -34,7 +75,7 @@ function html(title, basePath, { shooBaseUrl } = {}) {
|
|
|
34
75
|
<div id="app"></div>
|
|
35
76
|
<script>window.__LAKEBED_BASE_PATH__ = ${JSON.stringify(basePath)};</script>
|
|
36
77
|
<script>window.__LAKEBED_AUTH__ = ${JSON.stringify({ shooBaseUrl })};</script>
|
|
37
|
-
<script type="module" src="${
|
|
78
|
+
<script type="module" src="${clientScriptUrl}"></script>
|
|
38
79
|
<script>
|
|
39
80
|
const tailwind = document.createElement("script");
|
|
40
81
|
tailwind.src = "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4";
|
|
@@ -214,6 +255,93 @@ function quotaLimitForBucket(bucket, deploy) {
|
|
|
214
255
|
return deploy.limits.requestsPerDay;
|
|
215
256
|
}
|
|
216
257
|
|
|
258
|
+
const USER_LIMIT_OVERRIDE_KEYS = ["requestsPerDay", "mutationsPerDay"];
|
|
259
|
+
|
|
260
|
+
function normalizeUserId(value) {
|
|
261
|
+
const userId = String(value ?? "").trim();
|
|
262
|
+
if (!userId) {
|
|
263
|
+
throw new Error("User id is required.");
|
|
264
|
+
}
|
|
265
|
+
if (userId.length > 256) {
|
|
266
|
+
throw new Error("User id must be 256 characters or fewer.");
|
|
267
|
+
}
|
|
268
|
+
return userId;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function normalizeLimitOverrideValue(value, key) {
|
|
272
|
+
const parsed = typeof value === "string" && value.trim() ? Number(value) : value;
|
|
273
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
274
|
+
throw new Error(`${key} must be a positive integer.`);
|
|
275
|
+
}
|
|
276
|
+
return parsed;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizeUserLimitOverrides(value = {}) {
|
|
280
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
281
|
+
throw new Error("limits must be a JSON object.");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const overrides = {};
|
|
285
|
+
for (const key of Object.keys(value)) {
|
|
286
|
+
if (!USER_LIMIT_OVERRIDE_KEYS.includes(key)) {
|
|
287
|
+
throw new Error(`Unsupported user limit override: ${key}.`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const key of USER_LIMIT_OVERRIDE_KEYS) {
|
|
292
|
+
const raw = value[key];
|
|
293
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
overrides[key] = normalizeLimitOverrideValue(raw, key);
|
|
297
|
+
}
|
|
298
|
+
return overrides;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function limitsWithOverrides(baseLimits, limitOverrides) {
|
|
302
|
+
return {
|
|
303
|
+
...baseLimits,
|
|
304
|
+
...normalizeUserLimitOverrides(limitOverrides ?? {})
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function userFromOwner(owner, current = {}) {
|
|
309
|
+
const id = normalizeUserId(owner?.id ?? current.id);
|
|
310
|
+
return {
|
|
311
|
+
avatarUrl: owner?.avatarUrl ?? current.avatarUrl ?? null,
|
|
312
|
+
createdAt: current.createdAt ?? now(),
|
|
313
|
+
displayName: owner?.displayName ?? owner?.login ?? current.displayName ?? id,
|
|
314
|
+
id,
|
|
315
|
+
limitOverrides: normalizeUserLimitOverrides(current.limitOverrides ?? {}),
|
|
316
|
+
login: owner?.login ?? current.login ?? null,
|
|
317
|
+
provider: owner?.provider ?? current.provider ?? null,
|
|
318
|
+
providerId: owner?.providerId ?? current.providerId ?? null,
|
|
319
|
+
updatedAt: now(),
|
|
320
|
+
url: owner?.url ?? current.url ?? null
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function adminUserSummary({ activeDeployCount = 0, deployCount = 0, usage = [], user }) {
|
|
325
|
+
const limitOverrides = normalizeUserLimitOverrides(user.limitOverrides ?? {});
|
|
326
|
+
return {
|
|
327
|
+
activeDeployCount,
|
|
328
|
+
avatarUrl: user.avatarUrl,
|
|
329
|
+
createdAt: user.createdAt,
|
|
330
|
+
defaultLimits: DEFAULT_ANONYMOUS_LIMITS,
|
|
331
|
+
deployCount,
|
|
332
|
+
displayName: user.displayName ?? user.login ?? user.id,
|
|
333
|
+
id: user.id,
|
|
334
|
+
limitOverrides,
|
|
335
|
+
limits: limitsWithOverrides(DEFAULT_ANONYMOUS_LIMITS, limitOverrides),
|
|
336
|
+
login: user.login,
|
|
337
|
+
provider: user.provider,
|
|
338
|
+
providerId: user.providerId,
|
|
339
|
+
updatedAt: user.updatedAt,
|
|
340
|
+
url: user.url,
|
|
341
|
+
...usageCounts(usage)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
217
345
|
const adminCookieName = "lakebed_admin";
|
|
218
346
|
const adminCookieMaxAgeSeconds = 60 * 60 * 24 * 7;
|
|
219
347
|
const developerCookieName = "lakebed_developer";
|
|
@@ -691,6 +819,10 @@ function adminHtml() {
|
|
|
691
819
|
overflow: hidden;
|
|
692
820
|
}
|
|
693
821
|
|
|
822
|
+
.panel + .panel {
|
|
823
|
+
margin-top: 18px;
|
|
824
|
+
}
|
|
825
|
+
|
|
694
826
|
.panel-head {
|
|
695
827
|
align-items: center;
|
|
696
828
|
border-bottom: 1px solid var(--line);
|
|
@@ -815,6 +947,58 @@ function adminHtml() {
|
|
|
815
947
|
opacity: 0.62;
|
|
816
948
|
}
|
|
817
949
|
|
|
950
|
+
.row-action.neutral {
|
|
951
|
+
border-color: var(--line-strong);
|
|
952
|
+
color: var(--text);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.row-action.primary {
|
|
956
|
+
border-color: rgba(154, 214, 107, 0.8);
|
|
957
|
+
color: var(--accent);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.user-name {
|
|
961
|
+
display: grid;
|
|
962
|
+
gap: 4px;
|
|
963
|
+
min-width: 220px;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.user-name strong {
|
|
967
|
+
font-size: 15px;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.limit-cell {
|
|
971
|
+
display: grid;
|
|
972
|
+
gap: 6px;
|
|
973
|
+
min-width: 150px;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.limit-cell input {
|
|
977
|
+
min-height: 34px;
|
|
978
|
+
padding: 0 10px;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.limit-form {
|
|
982
|
+
align-items: end;
|
|
983
|
+
display: grid;
|
|
984
|
+
gap: 8px;
|
|
985
|
+
grid-template-columns: minmax(180px, 1fr) 132px 132px auto;
|
|
986
|
+
width: min(100%, 720px);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.limit-form .field {
|
|
990
|
+
gap: 5px;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.limit-form input {
|
|
994
|
+
min-height: 36px;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.limit-actions {
|
|
998
|
+
display: flex;
|
|
999
|
+
gap: 8px;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
818
1002
|
th:nth-child(8),
|
|
819
1003
|
td:nth-child(8),
|
|
820
1004
|
th:nth-child(9),
|
|
@@ -898,6 +1082,11 @@ function adminHtml() {
|
|
|
898
1082
|
justify-content: flex-start;
|
|
899
1083
|
}
|
|
900
1084
|
|
|
1085
|
+
.limit-form {
|
|
1086
|
+
grid-template-columns: 1fr;
|
|
1087
|
+
width: 100%;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
901
1090
|
.metrics {
|
|
902
1091
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
903
1092
|
}
|
|
@@ -973,6 +1162,43 @@ function adminHtml() {
|
|
|
973
1162
|
</table>
|
|
974
1163
|
</div>
|
|
975
1164
|
</section>
|
|
1165
|
+
|
|
1166
|
+
<section class="panel">
|
|
1167
|
+
<div class="panel-head">
|
|
1168
|
+
<h2>User limit overrides</h2>
|
|
1169
|
+
<form class="limit-form" id="new-user-limit-form">
|
|
1170
|
+
<div class="field">
|
|
1171
|
+
<label for="new-user-id">User id</label>
|
|
1172
|
+
<input id="new-user-id" name="userId" autocomplete="off" placeholder="github:42" />
|
|
1173
|
+
</div>
|
|
1174
|
+
<div class="field">
|
|
1175
|
+
<label for="new-user-requests">Requests</label>
|
|
1176
|
+
<input id="new-user-requests" name="requestsPerDay" inputmode="numeric" min="1" step="1" type="number" />
|
|
1177
|
+
</div>
|
|
1178
|
+
<div class="field">
|
|
1179
|
+
<label for="new-user-mutations">Mutations</label>
|
|
1180
|
+
<input id="new-user-mutations" name="mutationsPerDay" inputmode="numeric" min="1" step="1" type="number" />
|
|
1181
|
+
</div>
|
|
1182
|
+
<button class="button secondary" type="submit">Save</button>
|
|
1183
|
+
</form>
|
|
1184
|
+
</div>
|
|
1185
|
+
<div class="table-wrap">
|
|
1186
|
+
<table>
|
|
1187
|
+
<thead>
|
|
1188
|
+
<tr>
|
|
1189
|
+
<th>User</th>
|
|
1190
|
+
<th>Deploys</th>
|
|
1191
|
+
<th>Requests today</th>
|
|
1192
|
+
<th>Mutations today</th>
|
|
1193
|
+
<th>Request limit</th>
|
|
1194
|
+
<th>Mutation limit</th>
|
|
1195
|
+
<th>Actions</th>
|
|
1196
|
+
</tr>
|
|
1197
|
+
</thead>
|
|
1198
|
+
<tbody id="user-rows"></tbody>
|
|
1199
|
+
</table>
|
|
1200
|
+
</div>
|
|
1201
|
+
</section>
|
|
976
1202
|
</section>
|
|
977
1203
|
</main>
|
|
978
1204
|
|
|
@@ -982,8 +1208,11 @@ function adminHtml() {
|
|
|
982
1208
|
const loginForm = document.getElementById("login-form");
|
|
983
1209
|
const loginError = document.getElementById("login-error");
|
|
984
1210
|
const rows = document.getElementById("deploy-rows");
|
|
1211
|
+
const userRows = document.getElementById("user-rows");
|
|
1212
|
+
const newUserLimitForm = document.getElementById("new-user-limit-form");
|
|
985
1213
|
const statusLine = document.getElementById("status-line");
|
|
986
1214
|
let terminatingDeployId = null;
|
|
1215
|
+
let savingUserId = null;
|
|
987
1216
|
|
|
988
1217
|
function show(view) {
|
|
989
1218
|
loginView.classList.toggle("hidden", view !== "login");
|
|
@@ -1019,6 +1248,19 @@ function adminHtml() {
|
|
|
1019
1248
|
document.getElementById(id).textContent = value;
|
|
1020
1249
|
}
|
|
1021
1250
|
|
|
1251
|
+
function parseLimitValue(value, name) {
|
|
1252
|
+
const trimmed = String(value || "").trim();
|
|
1253
|
+
if (!trimmed) {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const parsed = Number(trimmed);
|
|
1258
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
1259
|
+
throw new Error(name + " must be a positive integer.");
|
|
1260
|
+
}
|
|
1261
|
+
return parsed;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1022
1264
|
function textCell(value, className) {
|
|
1023
1265
|
const td = document.createElement("td");
|
|
1024
1266
|
td.textContent = value;
|
|
@@ -1072,6 +1314,80 @@ function adminHtml() {
|
|
|
1072
1314
|
return td;
|
|
1073
1315
|
}
|
|
1074
1316
|
|
|
1317
|
+
function userCell(user) {
|
|
1318
|
+
const td = document.createElement("td");
|
|
1319
|
+
const wrap = document.createElement("div");
|
|
1320
|
+
const name = document.createElement("strong");
|
|
1321
|
+
const id = document.createElement("small");
|
|
1322
|
+
wrap.className = "user-name";
|
|
1323
|
+
if (user.url) {
|
|
1324
|
+
const link = document.createElement("a");
|
|
1325
|
+
link.href = user.url;
|
|
1326
|
+
link.textContent = user.login || user.displayName || user.id;
|
|
1327
|
+
name.appendChild(link);
|
|
1328
|
+
} else {
|
|
1329
|
+
name.textContent = user.login || user.displayName || user.id;
|
|
1330
|
+
}
|
|
1331
|
+
id.className = "mono";
|
|
1332
|
+
id.textContent = user.id;
|
|
1333
|
+
wrap.appendChild(name);
|
|
1334
|
+
wrap.appendChild(id);
|
|
1335
|
+
td.appendChild(wrap);
|
|
1336
|
+
return td;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function limitInputCell(user, key, label) {
|
|
1340
|
+
const td = document.createElement("td");
|
|
1341
|
+
const wrap = document.createElement("div");
|
|
1342
|
+
const input = document.createElement("input");
|
|
1343
|
+
const effective = document.createElement("small");
|
|
1344
|
+
wrap.className = "limit-cell";
|
|
1345
|
+
input.inputMode = "numeric";
|
|
1346
|
+
input.min = "1";
|
|
1347
|
+
input.step = "1";
|
|
1348
|
+
input.type = "number";
|
|
1349
|
+
input.dataset.key = key;
|
|
1350
|
+
input.value = user.limitOverrides[key] ? String(user.limitOverrides[key]) : "";
|
|
1351
|
+
input.placeholder = String(user.defaultLimits[key]);
|
|
1352
|
+
input.setAttribute("aria-label", label + " override for " + user.id);
|
|
1353
|
+
effective.className = "mono";
|
|
1354
|
+
effective.textContent = "effective " + formatNumber(user.limits[key]);
|
|
1355
|
+
wrap.appendChild(input);
|
|
1356
|
+
wrap.appendChild(effective);
|
|
1357
|
+
td.appendChild(wrap);
|
|
1358
|
+
return td;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function userActionCell(user) {
|
|
1362
|
+
const td = document.createElement("td");
|
|
1363
|
+
const wrap = document.createElement("div");
|
|
1364
|
+
const save = document.createElement("button");
|
|
1365
|
+
const clear = document.createElement("button");
|
|
1366
|
+
wrap.className = "limit-actions";
|
|
1367
|
+
save.className = "row-action primary";
|
|
1368
|
+
save.type = "button";
|
|
1369
|
+
save.textContent = savingUserId === user.id ? "Saving" : "Save";
|
|
1370
|
+
save.disabled = savingUserId === user.id;
|
|
1371
|
+
save.addEventListener("click", () => {
|
|
1372
|
+
void saveUserLimits(user.id, save.closest("tr")).catch((error) => {
|
|
1373
|
+
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
1374
|
+
});
|
|
1375
|
+
});
|
|
1376
|
+
clear.className = "row-action neutral";
|
|
1377
|
+
clear.type = "button";
|
|
1378
|
+
clear.textContent = "Clear";
|
|
1379
|
+
clear.disabled = savingUserId === user.id;
|
|
1380
|
+
clear.addEventListener("click", () => {
|
|
1381
|
+
void saveUserLimits(user.id, null, { requestsPerDay: null, mutationsPerDay: null }).catch((error) => {
|
|
1382
|
+
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
wrap.appendChild(save);
|
|
1386
|
+
wrap.appendChild(clear);
|
|
1387
|
+
td.appendChild(wrap);
|
|
1388
|
+
return td;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1075
1391
|
function render(summary) {
|
|
1076
1392
|
setMetric("metric-deploys", formatNumber(summary.deployCount));
|
|
1077
1393
|
setMetric("metric-artifacts", formatBytes(summary.totals.artifactBytes));
|
|
@@ -1096,6 +1412,27 @@ function adminHtml() {
|
|
|
1096
1412
|
tr.appendChild(textCell(formatNumber(deploy.connections), "mono"));
|
|
1097
1413
|
rows.appendChild(tr);
|
|
1098
1414
|
}
|
|
1415
|
+
|
|
1416
|
+
userRows.replaceChildren();
|
|
1417
|
+
for (const user of summary.users || []) {
|
|
1418
|
+
const tr = document.createElement("tr");
|
|
1419
|
+
tr.appendChild(userCell(user));
|
|
1420
|
+
tr.appendChild(textCell(formatNumber(user.activeDeployCount) + " / " + formatNumber(user.deployCount), "mono"));
|
|
1421
|
+
tr.appendChild(textCell(formatNumber(user.requestsToday) + " / " + formatNumber(user.limits.requestsPerDay), "mono"));
|
|
1422
|
+
tr.appendChild(textCell(formatNumber(user.mutationsToday) + " / " + formatNumber(user.limits.mutationsPerDay), "mono"));
|
|
1423
|
+
tr.appendChild(limitInputCell(user, "requestsPerDay", "Request limit"));
|
|
1424
|
+
tr.appendChild(limitInputCell(user, "mutationsPerDay", "Mutation limit"));
|
|
1425
|
+
tr.appendChild(userActionCell(user));
|
|
1426
|
+
userRows.appendChild(tr);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
if (!summary.users?.length) {
|
|
1430
|
+
const tr = document.createElement("tr");
|
|
1431
|
+
const td = textCell("No claimed users or manual overrides yet.", "mono");
|
|
1432
|
+
td.colSpan = 7;
|
|
1433
|
+
tr.appendChild(td);
|
|
1434
|
+
userRows.appendChild(tr);
|
|
1435
|
+
}
|
|
1099
1436
|
}
|
|
1100
1437
|
|
|
1101
1438
|
async function loadSummary() {
|
|
@@ -1116,6 +1453,35 @@ function adminHtml() {
|
|
|
1116
1453
|
show("dashboard");
|
|
1117
1454
|
}
|
|
1118
1455
|
|
|
1456
|
+
async function saveUserLimits(userId, row, explicitLimits) {
|
|
1457
|
+
const limits = explicitLimits || {
|
|
1458
|
+
requestsPerDay: parseLimitValue(row.querySelector('input[data-key="requestsPerDay"]').value, "Request limit"),
|
|
1459
|
+
mutationsPerDay: parseLimitValue(row.querySelector('input[data-key="mutationsPerDay"]').value, "Mutation limit")
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
savingUserId = userId;
|
|
1463
|
+
statusLine.textContent = "Saving limits for " + userId;
|
|
1464
|
+
try {
|
|
1465
|
+
const response = await fetch("/admin/api/users/" + encodeURIComponent(userId) + "/limits", {
|
|
1466
|
+
body: JSON.stringify({ limits }),
|
|
1467
|
+
headers: { "Content-Type": "application/json" },
|
|
1468
|
+
method: "PUT"
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
if (response.status === 401) {
|
|
1472
|
+
show("login");
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (!response.ok) {
|
|
1476
|
+
statusLine.textContent = "Save failed";
|
|
1477
|
+
throw new Error(await response.text());
|
|
1478
|
+
}
|
|
1479
|
+
} finally {
|
|
1480
|
+
savingUserId = null;
|
|
1481
|
+
}
|
|
1482
|
+
await loadSummary();
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1119
1485
|
async function terminateDeploy(deploy) {
|
|
1120
1486
|
if (!confirm("Terminate " + (deploy.name || deploy.slug) + "?")) {
|
|
1121
1487
|
return;
|
|
@@ -1158,6 +1524,33 @@ function adminHtml() {
|
|
|
1158
1524
|
await loadSummary();
|
|
1159
1525
|
});
|
|
1160
1526
|
|
|
1527
|
+
newUserLimitForm.addEventListener("submit", async (event) => {
|
|
1528
|
+
event.preventDefault();
|
|
1529
|
+
const form = new FormData(newUserLimitForm);
|
|
1530
|
+
const userId = String(form.get("userId") || "").trim();
|
|
1531
|
+
if (!userId) {
|
|
1532
|
+
statusLine.textContent = "User id is required.";
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
try {
|
|
1537
|
+
const requestsPerDay = parseLimitValue(form.get("requestsPerDay"), "Request limit");
|
|
1538
|
+
const mutationsPerDay = parseLimitValue(form.get("mutationsPerDay"), "Mutation limit");
|
|
1539
|
+
if (!requestsPerDay && !mutationsPerDay) {
|
|
1540
|
+
statusLine.textContent = "Enter at least one custom limit.";
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
await saveUserLimits(userId, null, {
|
|
1545
|
+
requestsPerDay,
|
|
1546
|
+
mutationsPerDay
|
|
1547
|
+
});
|
|
1548
|
+
newUserLimitForm.reset();
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1161
1554
|
document.getElementById("refresh-button").addEventListener("click", () => {
|
|
1162
1555
|
void loadSummary();
|
|
1163
1556
|
});
|
|
@@ -1434,10 +1827,100 @@ export class MemoryAnonymousStore {
|
|
|
1434
1827
|
this.quotaEvents = new Map();
|
|
1435
1828
|
this.queues = new Map();
|
|
1436
1829
|
this.rows = new Map();
|
|
1830
|
+
this.serverEnv = new Map();
|
|
1831
|
+
this.users = new Map();
|
|
1437
1832
|
}
|
|
1438
1833
|
|
|
1439
1834
|
async initialize() {}
|
|
1440
1835
|
|
|
1836
|
+
async rememberUser(owner) {
|
|
1837
|
+
const id = normalizeUserId(owner?.id);
|
|
1838
|
+
const user = userFromOwner(owner, this.users.get(id));
|
|
1839
|
+
this.users.set(id, user);
|
|
1840
|
+
return user;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
async getUserLimitOverrides(userId) {
|
|
1844
|
+
return normalizeUserLimitOverrides(this.users.get(normalizeUserId(userId))?.limitOverrides ?? {});
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
async setUserLimitOverrides(userId, limitOverrides) {
|
|
1848
|
+
const id = normalizeUserId(userId);
|
|
1849
|
+
const current = this.users.get(id) ?? {
|
|
1850
|
+
createdAt: now(),
|
|
1851
|
+
displayName: id,
|
|
1852
|
+
id,
|
|
1853
|
+
limitOverrides: {}
|
|
1854
|
+
};
|
|
1855
|
+
const user = {
|
|
1856
|
+
...current,
|
|
1857
|
+
displayName: current.displayName ?? current.login ?? id,
|
|
1858
|
+
id,
|
|
1859
|
+
limitOverrides: normalizeUserLimitOverrides(limitOverrides),
|
|
1860
|
+
updatedAt: now()
|
|
1861
|
+
};
|
|
1862
|
+
this.users.set(id, user);
|
|
1863
|
+
return this.getAdminUser(id);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
async deployWithUserLimitOverrides(deploy) {
|
|
1867
|
+
if (!deploy?.ownerId) {
|
|
1868
|
+
return deploy;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
const limitOverrides = await this.getUserLimitOverrides(deploy.ownerId);
|
|
1872
|
+
if (Object.keys(limitOverrides).length === 0) {
|
|
1873
|
+
return deploy;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
return {
|
|
1877
|
+
...deploy,
|
|
1878
|
+
limits: limitsWithOverrides(deploy.limits, limitOverrides)
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
async getStoredDeployById(id) {
|
|
1883
|
+
return this.deploys.get(id) ?? null;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
async getAdminUser(userId) {
|
|
1887
|
+
const id = normalizeUserId(userId);
|
|
1888
|
+
const user = this.users.get(id);
|
|
1889
|
+
if (!user) {
|
|
1890
|
+
return null;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const deploys = Array.from(this.deploys.values()).filter((deploy) => deploy.ownerId === id);
|
|
1894
|
+
const usage = [];
|
|
1895
|
+
for (const deploy of deploys) {
|
|
1896
|
+
usage.push(...(await this.readUsage(deploy.id)));
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
return adminUserSummary({
|
|
1900
|
+
activeDeployCount: deploys.filter((deploy) => deploy.status === "active" && !isExpired(deploy)).length,
|
|
1901
|
+
deployCount: deploys.length,
|
|
1902
|
+
usage,
|
|
1903
|
+
user
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
async listAdminUsers() {
|
|
1908
|
+
for (const deploy of this.deploys.values()) {
|
|
1909
|
+
if (deploy.owner && !this.users.has(deploy.ownerId)) {
|
|
1910
|
+
await this.rememberUser(deploy.owner);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
const users = [];
|
|
1915
|
+
for (const user of this.users.values()) {
|
|
1916
|
+
users.push(await this.getAdminUser(user.id));
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
return users
|
|
1920
|
+
.filter(Boolean)
|
|
1921
|
+
.sort((left, right) => String(left.login ?? left.id).localeCompare(String(right.login ?? right.id)));
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1441
1924
|
storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt }) {
|
|
1442
1925
|
const currentArtifact = this.artifacts.get(artifactHash);
|
|
1443
1926
|
this.artifacts.set(artifactHash, {
|
|
@@ -1451,7 +1934,7 @@ export class MemoryAnonymousStore {
|
|
|
1451
1934
|
});
|
|
1452
1935
|
}
|
|
1453
1936
|
|
|
1454
|
-
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds }) {
|
|
1937
|
+
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds, serverEnv }) {
|
|
1455
1938
|
const deployId = createDeployId();
|
|
1456
1939
|
const normalizedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
|
|
1457
1940
|
let slug = createSlug();
|
|
@@ -1492,6 +1975,9 @@ export class MemoryAnonymousStore {
|
|
|
1492
1975
|
};
|
|
1493
1976
|
this.deploys.set(deployId, deploy);
|
|
1494
1977
|
this.deploysBySlug.set(slug, deployId);
|
|
1978
|
+
if (serverEnv !== undefined) {
|
|
1979
|
+
this.serverEnv.set(deployId, { ...serverEnv });
|
|
1980
|
+
}
|
|
1495
1981
|
return { deploy, token };
|
|
1496
1982
|
}
|
|
1497
1983
|
|
|
@@ -1503,9 +1989,10 @@ export class MemoryAnonymousStore {
|
|
|
1503
1989
|
clientBundleHash,
|
|
1504
1990
|
deployId,
|
|
1505
1991
|
publicRootUrl,
|
|
1506
|
-
requestedTtlSeconds
|
|
1992
|
+
requestedTtlSeconds,
|
|
1993
|
+
serverEnv
|
|
1507
1994
|
}) {
|
|
1508
|
-
const currentDeploy = await this.
|
|
1995
|
+
const currentDeploy = await this.getStoredDeployById(deployId);
|
|
1509
1996
|
if (!currentDeploy) {
|
|
1510
1997
|
return null;
|
|
1511
1998
|
}
|
|
@@ -1533,11 +2020,14 @@ export class MemoryAnonymousStore {
|
|
|
1533
2020
|
createdAt
|
|
1534
2021
|
});
|
|
1535
2022
|
this.deploys.set(deployId, deploy);
|
|
1536
|
-
|
|
2023
|
+
if (serverEnv !== undefined) {
|
|
2024
|
+
this.serverEnv.set(deployId, { ...serverEnv });
|
|
2025
|
+
}
|
|
2026
|
+
return this.deployWithUserLimitOverrides(deploy);
|
|
1537
2027
|
}
|
|
1538
2028
|
|
|
1539
2029
|
async claimDeploy(deployId, token, owner) {
|
|
1540
|
-
const currentDeploy = await this.
|
|
2030
|
+
const currentDeploy = await this.getStoredDeployById(deployId);
|
|
1541
2031
|
if (!currentDeploy) {
|
|
1542
2032
|
return { status: "missing" };
|
|
1543
2033
|
}
|
|
@@ -1556,12 +2046,13 @@ export class MemoryAnonymousStore {
|
|
|
1556
2046
|
owner,
|
|
1557
2047
|
ownerId: owner.id
|
|
1558
2048
|
};
|
|
2049
|
+
await this.rememberUser(owner);
|
|
1559
2050
|
this.deploys.set(deployId, deploy);
|
|
1560
|
-
return { deploy, status: currentDeploy.ownerId ? "already_claimed" : "claimed" };
|
|
2051
|
+
return { deploy: await this.deployWithUserLimitOverrides(deploy), status: currentDeploy.ownerId ? "already_claimed" : "claimed" };
|
|
1561
2052
|
}
|
|
1562
2053
|
|
|
1563
2054
|
async terminateDeploy(deployId) {
|
|
1564
|
-
const currentDeploy = await this.
|
|
2055
|
+
const currentDeploy = await this.getStoredDeployById(deployId);
|
|
1565
2056
|
if (!currentDeploy) {
|
|
1566
2057
|
return null;
|
|
1567
2058
|
}
|
|
@@ -1571,11 +2062,11 @@ export class MemoryAnonymousStore {
|
|
|
1571
2062
|
status: "terminated"
|
|
1572
2063
|
};
|
|
1573
2064
|
this.deploys.set(deployId, deploy);
|
|
1574
|
-
return deploy;
|
|
2065
|
+
return this.deployWithUserLimitOverrides(deploy);
|
|
1575
2066
|
}
|
|
1576
2067
|
|
|
1577
2068
|
async getDeployById(id) {
|
|
1578
|
-
return this.
|
|
2069
|
+
return this.deployWithUserLimitOverrides(await this.getStoredDeployById(id));
|
|
1579
2070
|
}
|
|
1580
2071
|
|
|
1581
2072
|
async getDeployBySlug(slug) {
|
|
@@ -1624,6 +2115,14 @@ export class MemoryAnonymousStore {
|
|
|
1624
2115
|
this.tableRows(deployId, tableName).delete(rowId);
|
|
1625
2116
|
}
|
|
1626
2117
|
|
|
2118
|
+
async replaceServerEnv(deployId, serverEnv) {
|
|
2119
|
+
this.serverEnv.set(deployId, { ...serverEnv });
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
async getServerEnv(deployId) {
|
|
2123
|
+
return { ...(this.serverEnv.get(deployId) ?? {}) };
|
|
2124
|
+
}
|
|
2125
|
+
|
|
1627
2126
|
async transaction(deployId, handler) {
|
|
1628
2127
|
const run = () => handler(this);
|
|
1629
2128
|
const next = (this.queues.get(deployId) ?? Promise.resolve()).then(run, run);
|
|
@@ -1730,12 +2229,13 @@ export class MemoryAnonymousStore {
|
|
|
1730
2229
|
const summaries = [];
|
|
1731
2230
|
|
|
1732
2231
|
for (const deploy of deploys) {
|
|
2232
|
+
const effectiveDeploy = await this.deployWithUserLimitOverrides(deploy);
|
|
1733
2233
|
const storedArtifact = await this.getArtifact(deploy.artifactHash);
|
|
1734
2234
|
summaries.push(
|
|
1735
2235
|
adminDeploySummary({
|
|
1736
2236
|
artifact: storedArtifact?.artifact,
|
|
1737
2237
|
artifactBytes: storedArtifact?.bytes ?? 0,
|
|
1738
|
-
deploy,
|
|
2238
|
+
deploy: effectiveDeploy,
|
|
1739
2239
|
...this.logResourceForDeploy(deploy.id),
|
|
1740
2240
|
...this.stateResourceForDeploy(deploy.id),
|
|
1741
2241
|
usage: await this.readUsage(deploy.id)
|
|
@@ -1753,11 +2253,12 @@ export class MemoryAnonymousStore {
|
|
|
1753
2253
|
const summaries = [];
|
|
1754
2254
|
|
|
1755
2255
|
for (const deploy of deploys) {
|
|
2256
|
+
const effectiveDeploy = await this.deployWithUserLimitOverrides(deploy);
|
|
1756
2257
|
const storedArtifact = await this.getArtifact(deploy.artifactHash);
|
|
1757
2258
|
summaries.push(
|
|
1758
2259
|
developerDeploySummary({
|
|
1759
2260
|
artifact: storedArtifact?.artifact,
|
|
1760
|
-
deploy,
|
|
2261
|
+
deploy: effectiveDeploy,
|
|
1761
2262
|
usage: await this.readUsage(deploy.id)
|
|
1762
2263
|
})
|
|
1763
2264
|
);
|
|
@@ -1768,10 +2269,11 @@ export class MemoryAnonymousStore {
|
|
|
1768
2269
|
}
|
|
1769
2270
|
|
|
1770
2271
|
export class PostgresAnonymousStore {
|
|
1771
|
-
constructor({ connectionString }) {
|
|
2272
|
+
constructor({ connectionString, serverEnvSecret = "" }) {
|
|
1772
2273
|
this.connectionString = connectionString;
|
|
1773
2274
|
this.pool = null;
|
|
1774
2275
|
this.queues = new Map();
|
|
2276
|
+
this.serverEnvSecret = serverEnvSecret;
|
|
1775
2277
|
}
|
|
1776
2278
|
|
|
1777
2279
|
async initialize() {
|
|
@@ -1841,6 +2343,57 @@ export class PostgresAnonymousStore {
|
|
|
1841
2343
|
primary key (deploy_id, bucket, window_start)
|
|
1842
2344
|
)
|
|
1843
2345
|
`);
|
|
2346
|
+
await this.query(`
|
|
2347
|
+
create table if not exists deploy_server_env(
|
|
2348
|
+
deploy_id text not null references deploys(id) on delete cascade,
|
|
2349
|
+
env_key text not null,
|
|
2350
|
+
env_value text not null,
|
|
2351
|
+
updated_at timestamptz not null,
|
|
2352
|
+
primary key (deploy_id, env_key)
|
|
2353
|
+
)
|
|
2354
|
+
`);
|
|
2355
|
+
await this.query(`
|
|
2356
|
+
create table if not exists users(
|
|
2357
|
+
id text primary key,
|
|
2358
|
+
provider text,
|
|
2359
|
+
provider_id text,
|
|
2360
|
+
login text,
|
|
2361
|
+
display_name text,
|
|
2362
|
+
avatar_url text,
|
|
2363
|
+
url text,
|
|
2364
|
+
limit_overrides_json jsonb not null default '{}',
|
|
2365
|
+
created_at timestamptz not null,
|
|
2366
|
+
updated_at timestamptz not null
|
|
2367
|
+
)
|
|
2368
|
+
`);
|
|
2369
|
+
await this.query(`
|
|
2370
|
+
insert into users(
|
|
2371
|
+
id, provider, provider_id, login, display_name, avatar_url, url,
|
|
2372
|
+
limit_overrides_json, created_at, updated_at
|
|
2373
|
+
)
|
|
2374
|
+
select distinct on (owner_id)
|
|
2375
|
+
owner_id,
|
|
2376
|
+
owner_json->>'provider',
|
|
2377
|
+
owner_json->>'providerId',
|
|
2378
|
+
owner_json->>'login',
|
|
2379
|
+
coalesce(owner_json->>'displayName', owner_json->>'login', owner_id),
|
|
2380
|
+
owner_json->>'avatarUrl',
|
|
2381
|
+
owner_json->>'url',
|
|
2382
|
+
'{}'::jsonb,
|
|
2383
|
+
coalesce(claimed_at, created_at, now()),
|
|
2384
|
+
coalesce(claimed_at, created_at, now())
|
|
2385
|
+
from deploys
|
|
2386
|
+
where owner_id is not null
|
|
2387
|
+
order by owner_id, claimed_at desc nulls last, created_at desc
|
|
2388
|
+
on conflict(id) do update set
|
|
2389
|
+
provider = coalesce(excluded.provider, users.provider),
|
|
2390
|
+
provider_id = coalesce(excluded.provider_id, users.provider_id),
|
|
2391
|
+
login = coalesce(excluded.login, users.login),
|
|
2392
|
+
display_name = coalesce(excluded.display_name, users.display_name),
|
|
2393
|
+
avatar_url = coalesce(excluded.avatar_url, users.avatar_url),
|
|
2394
|
+
url = coalesce(excluded.url, users.url),
|
|
2395
|
+
updated_at = greatest(users.updated_at, excluded.updated_at)
|
|
2396
|
+
`);
|
|
1844
2397
|
}
|
|
1845
2398
|
|
|
1846
2399
|
async query(sql, params = []) {
|
|
@@ -1851,6 +2404,199 @@ export class PostgresAnonymousStore {
|
|
|
1851
2404
|
return this.pool.query(sql, params);
|
|
1852
2405
|
}
|
|
1853
2406
|
|
|
2407
|
+
rowToUser(row) {
|
|
2408
|
+
if (!row) {
|
|
2409
|
+
return null;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
return {
|
|
2413
|
+
avatarUrl: row.avatar_url ?? null,
|
|
2414
|
+
createdAt: new Date(row.created_at).toISOString(),
|
|
2415
|
+
displayName: row.display_name ?? row.login ?? row.id,
|
|
2416
|
+
id: row.id,
|
|
2417
|
+
limitOverrides: normalizeUserLimitOverrides(row.limit_overrides_json ?? {}),
|
|
2418
|
+
login: row.login ?? null,
|
|
2419
|
+
provider: row.provider ?? null,
|
|
2420
|
+
providerId: row.provider_id ?? null,
|
|
2421
|
+
updatedAt: new Date(row.updated_at).toISOString(),
|
|
2422
|
+
url: row.url ?? null
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
async rememberUser(owner) {
|
|
2427
|
+
const user = userFromOwner(owner);
|
|
2428
|
+
const result = await this.query(
|
|
2429
|
+
`
|
|
2430
|
+
insert into users(
|
|
2431
|
+
id, provider, provider_id, login, display_name, avatar_url, url,
|
|
2432
|
+
limit_overrides_json, created_at, updated_at
|
|
2433
|
+
)
|
|
2434
|
+
values($1, $2, $3, $4, $5, $6, $7, '{}'::jsonb, $8, $9)
|
|
2435
|
+
on conflict(id) do update set
|
|
2436
|
+
provider = excluded.provider,
|
|
2437
|
+
provider_id = excluded.provider_id,
|
|
2438
|
+
login = excluded.login,
|
|
2439
|
+
display_name = excluded.display_name,
|
|
2440
|
+
avatar_url = excluded.avatar_url,
|
|
2441
|
+
url = excluded.url,
|
|
2442
|
+
updated_at = excluded.updated_at
|
|
2443
|
+
returning *
|
|
2444
|
+
`,
|
|
2445
|
+
[
|
|
2446
|
+
user.id,
|
|
2447
|
+
user.provider,
|
|
2448
|
+
user.providerId,
|
|
2449
|
+
user.login,
|
|
2450
|
+
user.displayName,
|
|
2451
|
+
user.avatarUrl,
|
|
2452
|
+
user.url,
|
|
2453
|
+
user.createdAt,
|
|
2454
|
+
user.updatedAt
|
|
2455
|
+
]
|
|
2456
|
+
);
|
|
2457
|
+
return this.rowToUser(result.rows[0]);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
async getUserLimitOverrides(userId) {
|
|
2461
|
+
const result = await this.query("select limit_overrides_json from users where id = $1", [normalizeUserId(userId)]);
|
|
2462
|
+
return normalizeUserLimitOverrides(result.rows[0]?.limit_overrides_json ?? {});
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
async setUserLimitOverrides(userId, limitOverrides) {
|
|
2466
|
+
const id = normalizeUserId(userId);
|
|
2467
|
+
const updatedAt = now();
|
|
2468
|
+
await this.query(
|
|
2469
|
+
`
|
|
2470
|
+
insert into users(id, display_name, limit_overrides_json, created_at, updated_at)
|
|
2471
|
+
values($1, $2, $3::jsonb, $4, $4)
|
|
2472
|
+
on conflict(id) do update set
|
|
2473
|
+
limit_overrides_json = excluded.limit_overrides_json,
|
|
2474
|
+
updated_at = excluded.updated_at
|
|
2475
|
+
`,
|
|
2476
|
+
[id, id, JSON.stringify(normalizeUserLimitOverrides(limitOverrides)), updatedAt]
|
|
2477
|
+
);
|
|
2478
|
+
return this.getAdminUser(id);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
async deployWithUserLimitOverrides(deploy) {
|
|
2482
|
+
if (!deploy?.ownerId) {
|
|
2483
|
+
return deploy;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
const limitOverrides = await this.getUserLimitOverrides(deploy.ownerId);
|
|
2487
|
+
if (Object.keys(limitOverrides).length === 0) {
|
|
2488
|
+
return deploy;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
return {
|
|
2492
|
+
...deploy,
|
|
2493
|
+
limits: limitsWithOverrides(deploy.limits, limitOverrides)
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
async getBaseDeployById(id) {
|
|
2498
|
+
const result = await this.query("select * from deploys where id = $1", [id]);
|
|
2499
|
+
return this.rowToDeploy(result.rows[0]);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
async getAdminUser(userId) {
|
|
2503
|
+
const windowStart = dayWindowStart();
|
|
2504
|
+
const result = await this.query(
|
|
2505
|
+
`
|
|
2506
|
+
select
|
|
2507
|
+
u.*,
|
|
2508
|
+
coalesce(d.deploy_count, 0)::int as deploy_count,
|
|
2509
|
+
coalesce(d.active_deploy_count, 0)::int as active_deploy_count,
|
|
2510
|
+
coalesce(q.requests_today, 0)::int as requests_today,
|
|
2511
|
+
coalesce(q.mutations_today, 0)::int as mutations_today
|
|
2512
|
+
from users u
|
|
2513
|
+
left join (
|
|
2514
|
+
select
|
|
2515
|
+
owner_id,
|
|
2516
|
+
count(*)::int as deploy_count,
|
|
2517
|
+
count(*) filter (where status = 'active' and expires_at > now())::int as active_deploy_count
|
|
2518
|
+
from deploys
|
|
2519
|
+
where owner_id is not null
|
|
2520
|
+
group by owner_id
|
|
2521
|
+
) d on d.owner_id = u.id
|
|
2522
|
+
left join (
|
|
2523
|
+
select
|
|
2524
|
+
d.owner_id,
|
|
2525
|
+
coalesce(sum(q.count) filter (where q.bucket = 'requests' and q.window_start = $2), 0)::int as requests_today,
|
|
2526
|
+
coalesce(sum(q.count) filter (where q.bucket = 'mutations' and q.window_start = $2), 0)::int as mutations_today
|
|
2527
|
+
from deploys d
|
|
2528
|
+
join quota_events q on q.deploy_id = d.id
|
|
2529
|
+
where d.owner_id is not null
|
|
2530
|
+
group by d.owner_id
|
|
2531
|
+
) q on q.owner_id = u.id
|
|
2532
|
+
where u.id = $1
|
|
2533
|
+
`,
|
|
2534
|
+
[normalizeUserId(userId), windowStart]
|
|
2535
|
+
);
|
|
2536
|
+
const row = result.rows[0];
|
|
2537
|
+
if (!row) {
|
|
2538
|
+
return null;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
return adminUserSummary({
|
|
2542
|
+
activeDeployCount: row.active_deploy_count,
|
|
2543
|
+
deployCount: row.deploy_count,
|
|
2544
|
+
usage: [
|
|
2545
|
+
{ bucket: "requests", count: row.requests_today, windowStart },
|
|
2546
|
+
{ bucket: "mutations", count: row.mutations_today, windowStart }
|
|
2547
|
+
],
|
|
2548
|
+
user: this.rowToUser(row)
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
async listAdminUsers() {
|
|
2553
|
+
const windowStart = dayWindowStart();
|
|
2554
|
+
const result = await this.query(
|
|
2555
|
+
`
|
|
2556
|
+
select
|
|
2557
|
+
u.*,
|
|
2558
|
+
coalesce(d.deploy_count, 0)::int as deploy_count,
|
|
2559
|
+
coalesce(d.active_deploy_count, 0)::int as active_deploy_count,
|
|
2560
|
+
coalesce(q.requests_today, 0)::int as requests_today,
|
|
2561
|
+
coalesce(q.mutations_today, 0)::int as mutations_today
|
|
2562
|
+
from users u
|
|
2563
|
+
left join (
|
|
2564
|
+
select
|
|
2565
|
+
owner_id,
|
|
2566
|
+
count(*)::int as deploy_count,
|
|
2567
|
+
count(*) filter (where status = 'active' and expires_at > now())::int as active_deploy_count
|
|
2568
|
+
from deploys
|
|
2569
|
+
where owner_id is not null
|
|
2570
|
+
group by owner_id
|
|
2571
|
+
) d on d.owner_id = u.id
|
|
2572
|
+
left join (
|
|
2573
|
+
select
|
|
2574
|
+
d.owner_id,
|
|
2575
|
+
coalesce(sum(q.count) filter (where q.bucket = 'requests' and q.window_start = $1), 0)::int as requests_today,
|
|
2576
|
+
coalesce(sum(q.count) filter (where q.bucket = 'mutations' and q.window_start = $1), 0)::int as mutations_today
|
|
2577
|
+
from deploys d
|
|
2578
|
+
join quota_events q on q.deploy_id = d.id
|
|
2579
|
+
where d.owner_id is not null
|
|
2580
|
+
group by d.owner_id
|
|
2581
|
+
) q on q.owner_id = u.id
|
|
2582
|
+
order by coalesce(u.login, u.id) asc
|
|
2583
|
+
`,
|
|
2584
|
+
[windowStart]
|
|
2585
|
+
);
|
|
2586
|
+
|
|
2587
|
+
return result.rows.map((row) =>
|
|
2588
|
+
adminUserSummary({
|
|
2589
|
+
activeDeployCount: row.active_deploy_count,
|
|
2590
|
+
deployCount: row.deploy_count,
|
|
2591
|
+
usage: [
|
|
2592
|
+
{ bucket: "requests", count: row.requests_today, windowStart },
|
|
2593
|
+
{ bucket: "mutations", count: row.mutations_today, windowStart }
|
|
2594
|
+
],
|
|
2595
|
+
user: this.rowToUser(row)
|
|
2596
|
+
})
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
1854
2600
|
async storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt }) {
|
|
1855
2601
|
await this.query(
|
|
1856
2602
|
`
|
|
@@ -1862,7 +2608,7 @@ export class PostgresAnonymousStore {
|
|
|
1862
2608
|
);
|
|
1863
2609
|
}
|
|
1864
2610
|
|
|
1865
|
-
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds }) {
|
|
2611
|
+
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds, serverEnv }) {
|
|
1866
2612
|
const createdAt = now();
|
|
1867
2613
|
const normalizedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
|
|
1868
2614
|
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
@@ -1917,6 +2663,9 @@ export class PostgresAnonymousStore {
|
|
|
1917
2663
|
deploy.url
|
|
1918
2664
|
]
|
|
1919
2665
|
);
|
|
2666
|
+
if (serverEnv !== undefined) {
|
|
2667
|
+
await this.replaceServerEnv(deploy.id, serverEnv, createdAt);
|
|
2668
|
+
}
|
|
1920
2669
|
return { deploy, token };
|
|
1921
2670
|
} catch (error) {
|
|
1922
2671
|
if (error?.code !== "23505" || attempt === 7) {
|
|
@@ -1936,9 +2685,10 @@ export class PostgresAnonymousStore {
|
|
|
1936
2685
|
clientBundleHash,
|
|
1937
2686
|
deployId,
|
|
1938
2687
|
publicRootUrl,
|
|
1939
|
-
requestedTtlSeconds
|
|
2688
|
+
requestedTtlSeconds,
|
|
2689
|
+
serverEnv
|
|
1940
2690
|
}) {
|
|
1941
|
-
const currentDeploy = await this.
|
|
2691
|
+
const currentDeploy = await this.getBaseDeployById(deployId);
|
|
1942
2692
|
if (!currentDeploy) {
|
|
1943
2693
|
return null;
|
|
1944
2694
|
}
|
|
@@ -1966,11 +2716,14 @@ export class PostgresAnonymousStore {
|
|
|
1966
2716
|
`,
|
|
1967
2717
|
[deployId, artifactHash, clientBundleHash, expiresAt, nextPublicRootUrl, nextAppBaseDomain || null, url]
|
|
1968
2718
|
);
|
|
1969
|
-
|
|
2719
|
+
if (serverEnv !== undefined) {
|
|
2720
|
+
await this.replaceServerEnv(deployId, serverEnv, createdAt);
|
|
2721
|
+
}
|
|
2722
|
+
return this.deployWithUserLimitOverrides(this.rowToDeploy(result.rows[0]));
|
|
1970
2723
|
}
|
|
1971
2724
|
|
|
1972
2725
|
async claimDeploy(deployId, token, owner) {
|
|
1973
|
-
const currentDeploy = await this.
|
|
2726
|
+
const currentDeploy = await this.getBaseDeployById(deployId);
|
|
1974
2727
|
if (!currentDeploy) {
|
|
1975
2728
|
return { status: "missing" };
|
|
1976
2729
|
}
|
|
@@ -1995,7 +2748,11 @@ export class PostgresAnonymousStore {
|
|
|
1995
2748
|
`,
|
|
1996
2749
|
[deployId, owner.id, claimedAt, JSON.stringify(owner)]
|
|
1997
2750
|
);
|
|
1998
|
-
|
|
2751
|
+
await this.rememberUser(owner);
|
|
2752
|
+
return {
|
|
2753
|
+
deploy: await this.deployWithUserLimitOverrides(this.rowToDeploy(result.rows[0])),
|
|
2754
|
+
status: currentDeploy.ownerId ? "already_claimed" : "claimed"
|
|
2755
|
+
};
|
|
1999
2756
|
}
|
|
2000
2757
|
|
|
2001
2758
|
async terminateDeploy(deployId) {
|
|
@@ -2008,7 +2765,7 @@ export class PostgresAnonymousStore {
|
|
|
2008
2765
|
`,
|
|
2009
2766
|
[deployId]
|
|
2010
2767
|
);
|
|
2011
|
-
return this.rowToDeploy(result.rows[0]);
|
|
2768
|
+
return this.deployWithUserLimitOverrides(this.rowToDeploy(result.rows[0]));
|
|
2012
2769
|
}
|
|
2013
2770
|
|
|
2014
2771
|
rowToDeploy(row) {
|
|
@@ -2036,13 +2793,12 @@ export class PostgresAnonymousStore {
|
|
|
2036
2793
|
}
|
|
2037
2794
|
|
|
2038
2795
|
async getDeployById(id) {
|
|
2039
|
-
|
|
2040
|
-
return this.rowToDeploy(result.rows[0]);
|
|
2796
|
+
return this.deployWithUserLimitOverrides(await this.getBaseDeployById(id));
|
|
2041
2797
|
}
|
|
2042
2798
|
|
|
2043
2799
|
async getDeployBySlug(slug) {
|
|
2044
2800
|
const result = await this.query("select * from deploys where slug = $1", [slug]);
|
|
2045
|
-
return this.rowToDeploy(result.rows[0]);
|
|
2801
|
+
return this.deployWithUserLimitOverrides(this.rowToDeploy(result.rows[0]));
|
|
2046
2802
|
}
|
|
2047
2803
|
|
|
2048
2804
|
async getArtifact(hash) {
|
|
@@ -2107,6 +2863,24 @@ export class PostgresAnonymousStore {
|
|
|
2107
2863
|
await this.query("delete from state_rows where deploy_id = $1 and table_name = $2 and row_id = $3", [deployId, tableName, rowId]);
|
|
2108
2864
|
}
|
|
2109
2865
|
|
|
2866
|
+
async replaceServerEnv(deployId, serverEnv, updatedAt = now()) {
|
|
2867
|
+
await this.query("delete from deploy_server_env where deploy_id = $1", [deployId]);
|
|
2868
|
+
for (const [key, value] of Object.entries(serverEnv)) {
|
|
2869
|
+
await this.query(
|
|
2870
|
+
`
|
|
2871
|
+
insert into deploy_server_env(deploy_id, env_key, env_value, updated_at)
|
|
2872
|
+
values($1, $2, $3, $4)
|
|
2873
|
+
`,
|
|
2874
|
+
[deployId, key, encryptServerEnvValue(value, this.serverEnvSecret), updatedAt]
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
async getServerEnv(deployId) {
|
|
2880
|
+
const result = await this.query("select env_key, env_value from deploy_server_env where deploy_id = $1", [deployId]);
|
|
2881
|
+
return Object.fromEntries(result.rows.map((row) => [row.env_key, decryptServerEnvValue(row.env_value, this.serverEnvSecret)]));
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2110
2884
|
async transaction(deployId, handler) {
|
|
2111
2885
|
const run = () => handler(this);
|
|
2112
2886
|
const next = (this.queues.get(deployId) ?? Promise.resolve()).then(run, run);
|
|
@@ -2244,11 +3018,13 @@ export class PostgresAnonymousStore {
|
|
|
2244
3018
|
[windowStart]
|
|
2245
3019
|
);
|
|
2246
3020
|
|
|
2247
|
-
return
|
|
2248
|
-
|
|
3021
|
+
return Promise.all(
|
|
3022
|
+
result.rows.map(async (row) => {
|
|
3023
|
+
const deploy = await this.deployWithUserLimitOverrides(this.rowToDeploy(row));
|
|
3024
|
+
return adminDeploySummary({
|
|
2249
3025
|
artifact: row.artifact_json,
|
|
2250
3026
|
artifactBytes: row.artifact_bytes,
|
|
2251
|
-
deploy
|
|
3027
|
+
deploy,
|
|
2252
3028
|
logBytes: row.log_bytes,
|
|
2253
3029
|
logEntries: row.log_entries,
|
|
2254
3030
|
stateBytes: row.state_bytes,
|
|
@@ -2257,6 +3033,7 @@ export class PostgresAnonymousStore {
|
|
|
2257
3033
|
{ bucket: "requests", count: row.requests_today, windowStart },
|
|
2258
3034
|
{ bucket: "mutations", count: row.mutations_today, windowStart }
|
|
2259
3035
|
]
|
|
3036
|
+
});
|
|
2260
3037
|
})
|
|
2261
3038
|
);
|
|
2262
3039
|
}
|
|
@@ -2286,14 +3063,17 @@ export class PostgresAnonymousStore {
|
|
|
2286
3063
|
[ownerId, windowStart]
|
|
2287
3064
|
);
|
|
2288
3065
|
|
|
2289
|
-
return
|
|
2290
|
-
|
|
3066
|
+
return Promise.all(
|
|
3067
|
+
result.rows.map(async (row) => {
|
|
3068
|
+
const deploy = await this.deployWithUserLimitOverrides(this.rowToDeploy(row));
|
|
3069
|
+
return developerDeploySummary({
|
|
2291
3070
|
artifact: row.artifact_json,
|
|
2292
|
-
deploy
|
|
3071
|
+
deploy,
|
|
2293
3072
|
usage: [
|
|
2294
3073
|
{ bucket: "requests", count: row.requests_today, windowStart },
|
|
2295
3074
|
{ bucket: "mutations", count: row.mutations_today, windowStart }
|
|
2296
3075
|
]
|
|
3076
|
+
});
|
|
2297
3077
|
})
|
|
2298
3078
|
);
|
|
2299
3079
|
}
|
|
@@ -2301,7 +3081,10 @@ export class PostgresAnonymousStore {
|
|
|
2301
3081
|
|
|
2302
3082
|
export async function createAnonymousStoreFromEnv(env = process.env) {
|
|
2303
3083
|
if (env.DATABASE_URL) {
|
|
2304
|
-
const store = new PostgresAnonymousStore({
|
|
3084
|
+
const store = new PostgresAnonymousStore({
|
|
3085
|
+
connectionString: env.DATABASE_URL,
|
|
3086
|
+
serverEnvSecret: serverEnvSecretFromEnv(env)
|
|
3087
|
+
});
|
|
2305
3088
|
await store.initialize();
|
|
2306
3089
|
return store;
|
|
2307
3090
|
}
|
|
@@ -2427,6 +3210,7 @@ export async function startAnonymousServer({
|
|
|
2427
3210
|
...deploy,
|
|
2428
3211
|
connections: connections.get(deploy.id) ?? 0
|
|
2429
3212
|
}));
|
|
3213
|
+
const users = await resolvedStore.listAdminUsers();
|
|
2430
3214
|
const totals = deploys.reduce(
|
|
2431
3215
|
(acc, deploy) => ({
|
|
2432
3216
|
artifactBytes: acc.artifactBytes + deploy.artifactBytes,
|
|
@@ -2454,7 +3238,9 @@ export async function startAnonymousServer({
|
|
|
2454
3238
|
deployCount: deploys.length,
|
|
2455
3239
|
deploys,
|
|
2456
3240
|
generatedAt: now(),
|
|
2457
|
-
totals
|
|
3241
|
+
totals,
|
|
3242
|
+
userCount: users.length,
|
|
3243
|
+
users
|
|
2458
3244
|
};
|
|
2459
3245
|
}
|
|
2460
3246
|
|
|
@@ -2480,6 +3266,19 @@ export async function startAnonymousServer({
|
|
|
2480
3266
|
}
|
|
2481
3267
|
}
|
|
2482
3268
|
|
|
3269
|
+
async function refreshOwnerSubscriptions(ownerId) {
|
|
3270
|
+
for (const subscription of subscriptions.values()) {
|
|
3271
|
+
if (subscription.deploy.ownerId !== ownerId) {
|
|
3272
|
+
continue;
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
const deploy = await resolvedStore.getDeployById(subscription.deploy.id);
|
|
3276
|
+
if (deploy) {
|
|
3277
|
+
subscription.deploy = deploy;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
|
|
2483
3282
|
function closeDeployConnections(deployId) {
|
|
2484
3283
|
for (const [ws, subscription] of subscriptions) {
|
|
2485
3284
|
if (subscription.deploy.id !== deployId) {
|
|
@@ -2515,6 +3314,14 @@ export async function startAnonymousServer({
|
|
|
2515
3314
|
}
|
|
2516
3315
|
}
|
|
2517
3316
|
|
|
3317
|
+
function refreshDeployClients(deploy) {
|
|
3318
|
+
for (const [ws, subscription] of subscriptions) {
|
|
3319
|
+
if (subscription.deploy.id === deploy.id) {
|
|
3320
|
+
websocketSend(ws, { clientBundleHash: deploy.clientBundleHash, op: "refresh" });
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
|
|
2518
3325
|
const server = createServer(async (req, res) => {
|
|
2519
3326
|
const host = req.headers.host ?? "localhost";
|
|
2520
3327
|
const requestUrl = new URL(req.url ?? "/", `http://${host}`);
|
|
@@ -2729,6 +3536,34 @@ export async function startAnonymousServer({
|
|
|
2729
3536
|
return;
|
|
2730
3537
|
}
|
|
2731
3538
|
|
|
3539
|
+
const userLimitsMatch =
|
|
3540
|
+
req.method === "PUT" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)\/limits$/) : null;
|
|
3541
|
+
if (userLimitsMatch) {
|
|
3542
|
+
if (!isAdminConfigured(adminPassword)) {
|
|
3543
|
+
sendJson(res, 503, { error: "Lakebed admin is not configured. Set LAKEBED_ADMIN_PASSWORD." });
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
if (!isAdminAuthenticated(req, adminPassword)) {
|
|
3548
|
+
sendJson(res, 401, { error: "Admin authentication required." });
|
|
3549
|
+
return;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
const body = await readJsonBody(req, 4096);
|
|
3553
|
+
let limitOverrides;
|
|
3554
|
+
try {
|
|
3555
|
+
limitOverrides = normalizeUserLimitOverrides(body.limits ?? body.limitOverrides ?? {});
|
|
3556
|
+
} catch (error) {
|
|
3557
|
+
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
3558
|
+
return;
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
const user = await resolvedStore.setUserLimitOverrides(decodeURIComponent(userLimitsMatch[1]), limitOverrides);
|
|
3562
|
+
await refreshOwnerSubscriptions(user.id);
|
|
3563
|
+
sendJson(res, 200, { user });
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
2732
3567
|
const terminateMatch =
|
|
2733
3568
|
req.method === "POST"
|
|
2734
3569
|
? requestUrl.pathname.match(/^\/admin\/api\/deploys\/([^/]+)\/terminate$/)
|
|
@@ -2766,6 +3601,10 @@ export async function startAnonymousServer({
|
|
|
2766
3601
|
if (req.method === "POST" && requestUrl.pathname === "/v1/anonymous-deploys") {
|
|
2767
3602
|
const body = await readJsonBody(req);
|
|
2768
3603
|
const payload = validateAnonymousDeployPayload(body);
|
|
3604
|
+
if (payload.serverEnv !== undefined && Object.keys(payload.serverEnv).length > 0) {
|
|
3605
|
+
sendJson(res, 400, { error: "Server env requires a claimed deploy." });
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
2769
3608
|
const { deploy, token } = await resolvedStore.createDeploy({
|
|
2770
3609
|
appBaseDomain: resolvedAppBaseDomain,
|
|
2771
3610
|
artifact: payload.artifact,
|
|
@@ -2773,7 +3612,8 @@ export async function startAnonymousServer({
|
|
|
2773
3612
|
clientBundleBase64: payload.clientBundleBase64,
|
|
2774
3613
|
clientBundleHash: payload.clientBundleHash,
|
|
2775
3614
|
publicRootUrl: resolvedPublicRootUrl,
|
|
2776
|
-
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined
|
|
3615
|
+
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined,
|
|
3616
|
+
serverEnv: payload.serverEnv
|
|
2777
3617
|
});
|
|
2778
3618
|
await resolvedStore.appendLog(deploy.id, "info", "anonymous deploy created", { artifactHash: deploy.artifactHash });
|
|
2779
3619
|
sendJson(res, 201, responseForDeploy({ deploy, token }));
|
|
@@ -2795,6 +3635,10 @@ export async function startAnonymousServer({
|
|
|
2795
3635
|
|
|
2796
3636
|
const body = await readJsonBody(req);
|
|
2797
3637
|
const payload = validateAnonymousDeployPayload(body, { allowClaimedSource: Boolean(currentDeploy.ownerId) });
|
|
3638
|
+
if (payload.serverEnv !== undefined && !currentDeploy.ownerId) {
|
|
3639
|
+
sendJson(res, 400, { error: "Server env requires a claimed deploy." });
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
2798
3642
|
const deploy = await resolvedStore.updateDeploy({
|
|
2799
3643
|
appBaseDomain: resolvedAppBaseDomain,
|
|
2800
3644
|
artifact: payload.artifact,
|
|
@@ -2803,7 +3647,8 @@ export async function startAnonymousServer({
|
|
|
2803
3647
|
clientBundleHash: payload.clientBundleHash,
|
|
2804
3648
|
deployId,
|
|
2805
3649
|
publicRootUrl: resolvedPublicRootUrl,
|
|
2806
|
-
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined
|
|
3650
|
+
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined,
|
|
3651
|
+
serverEnv: payload.serverEnv
|
|
2807
3652
|
});
|
|
2808
3653
|
if (!deploy) {
|
|
2809
3654
|
sendJson(res, 404, { error: "Unknown deploy." });
|
|
@@ -2812,6 +3657,7 @@ export async function startAnonymousServer({
|
|
|
2812
3657
|
|
|
2813
3658
|
await resolvedStore.appendLog(deploy.id, "info", "anonymous deploy updated", { artifactHash: deploy.artifactHash });
|
|
2814
3659
|
await refreshDeploySubscriptions(deploy);
|
|
3660
|
+
refreshDeployClients(deploy);
|
|
2815
3661
|
await publishDeploy(deploy.id);
|
|
2816
3662
|
sendJson(res, 200, responseForDeploy({ deploy }));
|
|
2817
3663
|
return;
|
|
@@ -2847,7 +3693,8 @@ export async function startAnonymousServer({
|
|
|
2847
3693
|
|
|
2848
3694
|
const appPath = routeSystemPath(loaded.route.appPath);
|
|
2849
3695
|
if (req.method === "GET" && (appPath === "/" || appPath === "/index.html" || appPath === "/auth/callback")) {
|
|
2850
|
-
sendText(res, 200, html(loaded.artifact.name ?? "Lakebed Capsule", loaded.basePath, { shooBaseUrl }), {
|
|
3696
|
+
sendText(res, 200, html(loaded.artifact.name ?? "Lakebed Capsule", loaded.basePath, { clientBundleHash: loaded.deploy.clientBundleHash, shooBaseUrl }), {
|
|
3697
|
+
"Cache-Control": "no-store",
|
|
2851
3698
|
"Content-Type": "text/html; charset=utf-8"
|
|
2852
3699
|
});
|
|
2853
3700
|
return;
|