lakebed 0.0.6 → 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 +1 -1
- package/package.json +1 -1
- package/src/anonymous-server.js +770 -27
- package/src/client.js +15 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -170,4 +170,4 @@ Claimed deploys are listed at `/deploys` on the deploy API origin. They keep the
|
|
|
170
170
|
|
|
171
171
|
## Admin Dashboard
|
|
172
172
|
|
|
173
|
-
Set `LAKEBED_ADMIN_PASSWORD` on the anonymous deploy runner, then open `/admin` on the runner origin. The password is exchanged for an HttpOnly cookie so the dashboard stays unlocked until the cookie expires or the password changes. The resource table can terminate active deploys while preserving their resource history.
|
|
173
|
+
Set `LAKEBED_ADMIN_PASSWORD` on the anonymous deploy runner, then open `/admin` on the runner origin. The password is exchanged for an HttpOnly cookie so the dashboard stays unlocked until the cookie expires or the password changes. The resource table can terminate active deploys while preserving their resource history, and the users table can set per-user request and mutation limit overrides for claimed deploys.
|
package/package.json
CHANGED
package/src/anonymous-server.js
CHANGED
|
@@ -62,7 +62,8 @@ function decryptServerEnvValue(value, secret) {
|
|
|
62
62
|
return Buffer.concat([decipher.update(Buffer.from(ciphertextBase64, "base64url")), decipher.final()]).toString("utf8");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function html(title, basePath, { shooBaseUrl } = {}) {
|
|
65
|
+
function html(title, basePath, { clientBundleHash, shooBaseUrl } = {}) {
|
|
66
|
+
const clientScriptUrl = `${basePath}/client.js${clientBundleHash ? `?v=${encodeURIComponent(clientBundleHash)}` : ""}`;
|
|
66
67
|
return `<!doctype html>
|
|
67
68
|
<html lang="en">
|
|
68
69
|
<head>
|
|
@@ -74,7 +75,7 @@ function html(title, basePath, { shooBaseUrl } = {}) {
|
|
|
74
75
|
<div id="app"></div>
|
|
75
76
|
<script>window.__LAKEBED_BASE_PATH__ = ${JSON.stringify(basePath)};</script>
|
|
76
77
|
<script>window.__LAKEBED_AUTH__ = ${JSON.stringify({ shooBaseUrl })};</script>
|
|
77
|
-
<script type="module" src="${
|
|
78
|
+
<script type="module" src="${clientScriptUrl}"></script>
|
|
78
79
|
<script>
|
|
79
80
|
const tailwind = document.createElement("script");
|
|
80
81
|
tailwind.src = "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4";
|
|
@@ -254,6 +255,93 @@ function quotaLimitForBucket(bucket, deploy) {
|
|
|
254
255
|
return deploy.limits.requestsPerDay;
|
|
255
256
|
}
|
|
256
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
|
+
|
|
257
345
|
const adminCookieName = "lakebed_admin";
|
|
258
346
|
const adminCookieMaxAgeSeconds = 60 * 60 * 24 * 7;
|
|
259
347
|
const developerCookieName = "lakebed_developer";
|
|
@@ -731,6 +819,10 @@ function adminHtml() {
|
|
|
731
819
|
overflow: hidden;
|
|
732
820
|
}
|
|
733
821
|
|
|
822
|
+
.panel + .panel {
|
|
823
|
+
margin-top: 18px;
|
|
824
|
+
}
|
|
825
|
+
|
|
734
826
|
.panel-head {
|
|
735
827
|
align-items: center;
|
|
736
828
|
border-bottom: 1px solid var(--line);
|
|
@@ -855,6 +947,58 @@ function adminHtml() {
|
|
|
855
947
|
opacity: 0.62;
|
|
856
948
|
}
|
|
857
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
|
+
|
|
858
1002
|
th:nth-child(8),
|
|
859
1003
|
td:nth-child(8),
|
|
860
1004
|
th:nth-child(9),
|
|
@@ -938,6 +1082,11 @@ function adminHtml() {
|
|
|
938
1082
|
justify-content: flex-start;
|
|
939
1083
|
}
|
|
940
1084
|
|
|
1085
|
+
.limit-form {
|
|
1086
|
+
grid-template-columns: 1fr;
|
|
1087
|
+
width: 100%;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
941
1090
|
.metrics {
|
|
942
1091
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
943
1092
|
}
|
|
@@ -1013,6 +1162,43 @@ function adminHtml() {
|
|
|
1013
1162
|
</table>
|
|
1014
1163
|
</div>
|
|
1015
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>
|
|
1016
1202
|
</section>
|
|
1017
1203
|
</main>
|
|
1018
1204
|
|
|
@@ -1022,8 +1208,11 @@ function adminHtml() {
|
|
|
1022
1208
|
const loginForm = document.getElementById("login-form");
|
|
1023
1209
|
const loginError = document.getElementById("login-error");
|
|
1024
1210
|
const rows = document.getElementById("deploy-rows");
|
|
1211
|
+
const userRows = document.getElementById("user-rows");
|
|
1212
|
+
const newUserLimitForm = document.getElementById("new-user-limit-form");
|
|
1025
1213
|
const statusLine = document.getElementById("status-line");
|
|
1026
1214
|
let terminatingDeployId = null;
|
|
1215
|
+
let savingUserId = null;
|
|
1027
1216
|
|
|
1028
1217
|
function show(view) {
|
|
1029
1218
|
loginView.classList.toggle("hidden", view !== "login");
|
|
@@ -1059,6 +1248,19 @@ function adminHtml() {
|
|
|
1059
1248
|
document.getElementById(id).textContent = value;
|
|
1060
1249
|
}
|
|
1061
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
|
+
|
|
1062
1264
|
function textCell(value, className) {
|
|
1063
1265
|
const td = document.createElement("td");
|
|
1064
1266
|
td.textContent = value;
|
|
@@ -1112,6 +1314,80 @@ function adminHtml() {
|
|
|
1112
1314
|
return td;
|
|
1113
1315
|
}
|
|
1114
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
|
+
|
|
1115
1391
|
function render(summary) {
|
|
1116
1392
|
setMetric("metric-deploys", formatNumber(summary.deployCount));
|
|
1117
1393
|
setMetric("metric-artifacts", formatBytes(summary.totals.artifactBytes));
|
|
@@ -1136,6 +1412,27 @@ function adminHtml() {
|
|
|
1136
1412
|
tr.appendChild(textCell(formatNumber(deploy.connections), "mono"));
|
|
1137
1413
|
rows.appendChild(tr);
|
|
1138
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
|
+
}
|
|
1139
1436
|
}
|
|
1140
1437
|
|
|
1141
1438
|
async function loadSummary() {
|
|
@@ -1156,6 +1453,35 @@ function adminHtml() {
|
|
|
1156
1453
|
show("dashboard");
|
|
1157
1454
|
}
|
|
1158
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
|
+
|
|
1159
1485
|
async function terminateDeploy(deploy) {
|
|
1160
1486
|
if (!confirm("Terminate " + (deploy.name || deploy.slug) + "?")) {
|
|
1161
1487
|
return;
|
|
@@ -1198,6 +1524,33 @@ function adminHtml() {
|
|
|
1198
1524
|
await loadSummary();
|
|
1199
1525
|
});
|
|
1200
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
|
+
|
|
1201
1554
|
document.getElementById("refresh-button").addEventListener("click", () => {
|
|
1202
1555
|
void loadSummary();
|
|
1203
1556
|
});
|
|
@@ -1475,10 +1828,99 @@ export class MemoryAnonymousStore {
|
|
|
1475
1828
|
this.queues = new Map();
|
|
1476
1829
|
this.rows = new Map();
|
|
1477
1830
|
this.serverEnv = new Map();
|
|
1831
|
+
this.users = new Map();
|
|
1478
1832
|
}
|
|
1479
1833
|
|
|
1480
1834
|
async initialize() {}
|
|
1481
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
|
+
|
|
1482
1924
|
storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt }) {
|
|
1483
1925
|
const currentArtifact = this.artifacts.get(artifactHash);
|
|
1484
1926
|
this.artifacts.set(artifactHash, {
|
|
@@ -1550,7 +1992,7 @@ export class MemoryAnonymousStore {
|
|
|
1550
1992
|
requestedTtlSeconds,
|
|
1551
1993
|
serverEnv
|
|
1552
1994
|
}) {
|
|
1553
|
-
const currentDeploy = await this.
|
|
1995
|
+
const currentDeploy = await this.getStoredDeployById(deployId);
|
|
1554
1996
|
if (!currentDeploy) {
|
|
1555
1997
|
return null;
|
|
1556
1998
|
}
|
|
@@ -1581,11 +2023,11 @@ export class MemoryAnonymousStore {
|
|
|
1581
2023
|
if (serverEnv !== undefined) {
|
|
1582
2024
|
this.serverEnv.set(deployId, { ...serverEnv });
|
|
1583
2025
|
}
|
|
1584
|
-
return deploy;
|
|
2026
|
+
return this.deployWithUserLimitOverrides(deploy);
|
|
1585
2027
|
}
|
|
1586
2028
|
|
|
1587
2029
|
async claimDeploy(deployId, token, owner) {
|
|
1588
|
-
const currentDeploy = await this.
|
|
2030
|
+
const currentDeploy = await this.getStoredDeployById(deployId);
|
|
1589
2031
|
if (!currentDeploy) {
|
|
1590
2032
|
return { status: "missing" };
|
|
1591
2033
|
}
|
|
@@ -1604,12 +2046,13 @@ export class MemoryAnonymousStore {
|
|
|
1604
2046
|
owner,
|
|
1605
2047
|
ownerId: owner.id
|
|
1606
2048
|
};
|
|
2049
|
+
await this.rememberUser(owner);
|
|
1607
2050
|
this.deploys.set(deployId, deploy);
|
|
1608
|
-
return { deploy, status: currentDeploy.ownerId ? "already_claimed" : "claimed" };
|
|
2051
|
+
return { deploy: await this.deployWithUserLimitOverrides(deploy), status: currentDeploy.ownerId ? "already_claimed" : "claimed" };
|
|
1609
2052
|
}
|
|
1610
2053
|
|
|
1611
2054
|
async terminateDeploy(deployId) {
|
|
1612
|
-
const currentDeploy = await this.
|
|
2055
|
+
const currentDeploy = await this.getStoredDeployById(deployId);
|
|
1613
2056
|
if (!currentDeploy) {
|
|
1614
2057
|
return null;
|
|
1615
2058
|
}
|
|
@@ -1619,11 +2062,11 @@ export class MemoryAnonymousStore {
|
|
|
1619
2062
|
status: "terminated"
|
|
1620
2063
|
};
|
|
1621
2064
|
this.deploys.set(deployId, deploy);
|
|
1622
|
-
return deploy;
|
|
2065
|
+
return this.deployWithUserLimitOverrides(deploy);
|
|
1623
2066
|
}
|
|
1624
2067
|
|
|
1625
2068
|
async getDeployById(id) {
|
|
1626
|
-
return this.
|
|
2069
|
+
return this.deployWithUserLimitOverrides(await this.getStoredDeployById(id));
|
|
1627
2070
|
}
|
|
1628
2071
|
|
|
1629
2072
|
async getDeployBySlug(slug) {
|
|
@@ -1786,12 +2229,13 @@ export class MemoryAnonymousStore {
|
|
|
1786
2229
|
const summaries = [];
|
|
1787
2230
|
|
|
1788
2231
|
for (const deploy of deploys) {
|
|
2232
|
+
const effectiveDeploy = await this.deployWithUserLimitOverrides(deploy);
|
|
1789
2233
|
const storedArtifact = await this.getArtifact(deploy.artifactHash);
|
|
1790
2234
|
summaries.push(
|
|
1791
2235
|
adminDeploySummary({
|
|
1792
2236
|
artifact: storedArtifact?.artifact,
|
|
1793
2237
|
artifactBytes: storedArtifact?.bytes ?? 0,
|
|
1794
|
-
deploy,
|
|
2238
|
+
deploy: effectiveDeploy,
|
|
1795
2239
|
...this.logResourceForDeploy(deploy.id),
|
|
1796
2240
|
...this.stateResourceForDeploy(deploy.id),
|
|
1797
2241
|
usage: await this.readUsage(deploy.id)
|
|
@@ -1809,11 +2253,12 @@ export class MemoryAnonymousStore {
|
|
|
1809
2253
|
const summaries = [];
|
|
1810
2254
|
|
|
1811
2255
|
for (const deploy of deploys) {
|
|
2256
|
+
const effectiveDeploy = await this.deployWithUserLimitOverrides(deploy);
|
|
1812
2257
|
const storedArtifact = await this.getArtifact(deploy.artifactHash);
|
|
1813
2258
|
summaries.push(
|
|
1814
2259
|
developerDeploySummary({
|
|
1815
2260
|
artifact: storedArtifact?.artifact,
|
|
1816
|
-
deploy,
|
|
2261
|
+
deploy: effectiveDeploy,
|
|
1817
2262
|
usage: await this.readUsage(deploy.id)
|
|
1818
2263
|
})
|
|
1819
2264
|
);
|
|
@@ -1907,6 +2352,48 @@ export class PostgresAnonymousStore {
|
|
|
1907
2352
|
primary key (deploy_id, env_key)
|
|
1908
2353
|
)
|
|
1909
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
|
+
`);
|
|
1910
2397
|
}
|
|
1911
2398
|
|
|
1912
2399
|
async query(sql, params = []) {
|
|
@@ -1917,6 +2404,199 @@ export class PostgresAnonymousStore {
|
|
|
1917
2404
|
return this.pool.query(sql, params);
|
|
1918
2405
|
}
|
|
1919
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
|
+
|
|
1920
2600
|
async storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt }) {
|
|
1921
2601
|
await this.query(
|
|
1922
2602
|
`
|
|
@@ -2008,7 +2688,7 @@ export class PostgresAnonymousStore {
|
|
|
2008
2688
|
requestedTtlSeconds,
|
|
2009
2689
|
serverEnv
|
|
2010
2690
|
}) {
|
|
2011
|
-
const currentDeploy = await this.
|
|
2691
|
+
const currentDeploy = await this.getBaseDeployById(deployId);
|
|
2012
2692
|
if (!currentDeploy) {
|
|
2013
2693
|
return null;
|
|
2014
2694
|
}
|
|
@@ -2039,11 +2719,11 @@ export class PostgresAnonymousStore {
|
|
|
2039
2719
|
if (serverEnv !== undefined) {
|
|
2040
2720
|
await this.replaceServerEnv(deployId, serverEnv, createdAt);
|
|
2041
2721
|
}
|
|
2042
|
-
return this.rowToDeploy(result.rows[0]);
|
|
2722
|
+
return this.deployWithUserLimitOverrides(this.rowToDeploy(result.rows[0]));
|
|
2043
2723
|
}
|
|
2044
2724
|
|
|
2045
2725
|
async claimDeploy(deployId, token, owner) {
|
|
2046
|
-
const currentDeploy = await this.
|
|
2726
|
+
const currentDeploy = await this.getBaseDeployById(deployId);
|
|
2047
2727
|
if (!currentDeploy) {
|
|
2048
2728
|
return { status: "missing" };
|
|
2049
2729
|
}
|
|
@@ -2068,7 +2748,11 @@ export class PostgresAnonymousStore {
|
|
|
2068
2748
|
`,
|
|
2069
2749
|
[deployId, owner.id, claimedAt, JSON.stringify(owner)]
|
|
2070
2750
|
);
|
|
2071
|
-
|
|
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
|
+
};
|
|
2072
2756
|
}
|
|
2073
2757
|
|
|
2074
2758
|
async terminateDeploy(deployId) {
|
|
@@ -2081,7 +2765,7 @@ export class PostgresAnonymousStore {
|
|
|
2081
2765
|
`,
|
|
2082
2766
|
[deployId]
|
|
2083
2767
|
);
|
|
2084
|
-
return this.rowToDeploy(result.rows[0]);
|
|
2768
|
+
return this.deployWithUserLimitOverrides(this.rowToDeploy(result.rows[0]));
|
|
2085
2769
|
}
|
|
2086
2770
|
|
|
2087
2771
|
rowToDeploy(row) {
|
|
@@ -2109,13 +2793,12 @@ export class PostgresAnonymousStore {
|
|
|
2109
2793
|
}
|
|
2110
2794
|
|
|
2111
2795
|
async getDeployById(id) {
|
|
2112
|
-
|
|
2113
|
-
return this.rowToDeploy(result.rows[0]);
|
|
2796
|
+
return this.deployWithUserLimitOverrides(await this.getBaseDeployById(id));
|
|
2114
2797
|
}
|
|
2115
2798
|
|
|
2116
2799
|
async getDeployBySlug(slug) {
|
|
2117
2800
|
const result = await this.query("select * from deploys where slug = $1", [slug]);
|
|
2118
|
-
return this.rowToDeploy(result.rows[0]);
|
|
2801
|
+
return this.deployWithUserLimitOverrides(this.rowToDeploy(result.rows[0]));
|
|
2119
2802
|
}
|
|
2120
2803
|
|
|
2121
2804
|
async getArtifact(hash) {
|
|
@@ -2335,11 +3018,13 @@ export class PostgresAnonymousStore {
|
|
|
2335
3018
|
[windowStart]
|
|
2336
3019
|
);
|
|
2337
3020
|
|
|
2338
|
-
return
|
|
2339
|
-
|
|
3021
|
+
return Promise.all(
|
|
3022
|
+
result.rows.map(async (row) => {
|
|
3023
|
+
const deploy = await this.deployWithUserLimitOverrides(this.rowToDeploy(row));
|
|
3024
|
+
return adminDeploySummary({
|
|
2340
3025
|
artifact: row.artifact_json,
|
|
2341
3026
|
artifactBytes: row.artifact_bytes,
|
|
2342
|
-
deploy
|
|
3027
|
+
deploy,
|
|
2343
3028
|
logBytes: row.log_bytes,
|
|
2344
3029
|
logEntries: row.log_entries,
|
|
2345
3030
|
stateBytes: row.state_bytes,
|
|
@@ -2348,6 +3033,7 @@ export class PostgresAnonymousStore {
|
|
|
2348
3033
|
{ bucket: "requests", count: row.requests_today, windowStart },
|
|
2349
3034
|
{ bucket: "mutations", count: row.mutations_today, windowStart }
|
|
2350
3035
|
]
|
|
3036
|
+
});
|
|
2351
3037
|
})
|
|
2352
3038
|
);
|
|
2353
3039
|
}
|
|
@@ -2377,14 +3063,17 @@ export class PostgresAnonymousStore {
|
|
|
2377
3063
|
[ownerId, windowStart]
|
|
2378
3064
|
);
|
|
2379
3065
|
|
|
2380
|
-
return
|
|
2381
|
-
|
|
3066
|
+
return Promise.all(
|
|
3067
|
+
result.rows.map(async (row) => {
|
|
3068
|
+
const deploy = await this.deployWithUserLimitOverrides(this.rowToDeploy(row));
|
|
3069
|
+
return developerDeploySummary({
|
|
2382
3070
|
artifact: row.artifact_json,
|
|
2383
|
-
deploy
|
|
3071
|
+
deploy,
|
|
2384
3072
|
usage: [
|
|
2385
3073
|
{ bucket: "requests", count: row.requests_today, windowStart },
|
|
2386
3074
|
{ bucket: "mutations", count: row.mutations_today, windowStart }
|
|
2387
3075
|
]
|
|
3076
|
+
});
|
|
2388
3077
|
})
|
|
2389
3078
|
);
|
|
2390
3079
|
}
|
|
@@ -2521,6 +3210,7 @@ export async function startAnonymousServer({
|
|
|
2521
3210
|
...deploy,
|
|
2522
3211
|
connections: connections.get(deploy.id) ?? 0
|
|
2523
3212
|
}));
|
|
3213
|
+
const users = await resolvedStore.listAdminUsers();
|
|
2524
3214
|
const totals = deploys.reduce(
|
|
2525
3215
|
(acc, deploy) => ({
|
|
2526
3216
|
artifactBytes: acc.artifactBytes + deploy.artifactBytes,
|
|
@@ -2548,7 +3238,9 @@ export async function startAnonymousServer({
|
|
|
2548
3238
|
deployCount: deploys.length,
|
|
2549
3239
|
deploys,
|
|
2550
3240
|
generatedAt: now(),
|
|
2551
|
-
totals
|
|
3241
|
+
totals,
|
|
3242
|
+
userCount: users.length,
|
|
3243
|
+
users
|
|
2552
3244
|
};
|
|
2553
3245
|
}
|
|
2554
3246
|
|
|
@@ -2574,6 +3266,19 @@ export async function startAnonymousServer({
|
|
|
2574
3266
|
}
|
|
2575
3267
|
}
|
|
2576
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
|
+
|
|
2577
3282
|
function closeDeployConnections(deployId) {
|
|
2578
3283
|
for (const [ws, subscription] of subscriptions) {
|
|
2579
3284
|
if (subscription.deploy.id !== deployId) {
|
|
@@ -2609,6 +3314,14 @@ export async function startAnonymousServer({
|
|
|
2609
3314
|
}
|
|
2610
3315
|
}
|
|
2611
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
|
+
|
|
2612
3325
|
const server = createServer(async (req, res) => {
|
|
2613
3326
|
const host = req.headers.host ?? "localhost";
|
|
2614
3327
|
const requestUrl = new URL(req.url ?? "/", `http://${host}`);
|
|
@@ -2823,6 +3536,34 @@ export async function startAnonymousServer({
|
|
|
2823
3536
|
return;
|
|
2824
3537
|
}
|
|
2825
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
|
+
|
|
2826
3567
|
const terminateMatch =
|
|
2827
3568
|
req.method === "POST"
|
|
2828
3569
|
? requestUrl.pathname.match(/^\/admin\/api\/deploys\/([^/]+)\/terminate$/)
|
|
@@ -2916,6 +3657,7 @@ export async function startAnonymousServer({
|
|
|
2916
3657
|
|
|
2917
3658
|
await resolvedStore.appendLog(deploy.id, "info", "anonymous deploy updated", { artifactHash: deploy.artifactHash });
|
|
2918
3659
|
await refreshDeploySubscriptions(deploy);
|
|
3660
|
+
refreshDeployClients(deploy);
|
|
2919
3661
|
await publishDeploy(deploy.id);
|
|
2920
3662
|
sendJson(res, 200, responseForDeploy({ deploy }));
|
|
2921
3663
|
return;
|
|
@@ -2951,7 +3693,8 @@ export async function startAnonymousServer({
|
|
|
2951
3693
|
|
|
2952
3694
|
const appPath = routeSystemPath(loaded.route.appPath);
|
|
2953
3695
|
if (req.method === "GET" && (appPath === "/" || appPath === "/index.html" || appPath === "/auth/callback")) {
|
|
2954
|
-
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",
|
|
2955
3698
|
"Content-Type": "text/html; charset=utf-8"
|
|
2956
3699
|
});
|
|
2957
3700
|
return;
|
package/src/client.js
CHANGED
|
@@ -19,6 +19,7 @@ const pending = new Map();
|
|
|
19
19
|
const activeSubscriptions = new Set();
|
|
20
20
|
let authInitPromise = null;
|
|
21
21
|
let authInitialized = false;
|
|
22
|
+
let refreshRequested = false;
|
|
22
23
|
|
|
23
24
|
function toGuestName(name) {
|
|
24
25
|
return (
|
|
@@ -68,6 +69,15 @@ function emitQuery(name, value) {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function refreshPage() {
|
|
73
|
+
if (refreshRequested) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
refreshRequested = true;
|
|
78
|
+
window.location.reload();
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
function send(message) {
|
|
72
82
|
const ws = connect();
|
|
73
83
|
const payload = JSON.stringify(message);
|
|
@@ -438,6 +448,11 @@ function connect() {
|
|
|
438
448
|
return;
|
|
439
449
|
}
|
|
440
450
|
|
|
451
|
+
if (message.op === "refresh") {
|
|
452
|
+
refreshPage();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
441
456
|
if (message.id && pending.has(message.id)) {
|
|
442
457
|
const handlers = pending.get(message.id);
|
|
443
458
|
pending.delete(message.id);
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const LAKEBED_VERSION = "0.0.
|
|
1
|
+
export const LAKEBED_VERSION = "0.0.7";
|