codeharbor 0.1.14 → 0.1.15
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/.env.example +5 -0
- package/README.md +9 -2
- package/dist/cli.js +167 -18
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -74,6 +74,11 @@ ADMIN_PORT=8787
|
|
|
74
74
|
# Strongly recommended for any non-localhost access.
|
|
75
75
|
# Required when exposing admin via reverse proxy/tunnel/public domain.
|
|
76
76
|
ADMIN_TOKEN=
|
|
77
|
+
# Optional multi-token RBAC (JSON array).
|
|
78
|
+
# Each item: {"token":"...","role":"admin|viewer","actor":"ops-name"}
|
|
79
|
+
# Example:
|
|
80
|
+
# ADMIN_TOKENS_JSON=[{"token":"admin-secret","role":"admin","actor":"ops-admin"},{"token":"viewer-secret","role":"viewer","actor":"ops-audit"}]
|
|
81
|
+
ADMIN_TOKENS_JSON=
|
|
77
82
|
# Optional IP allowlist (comma-separated, for example: 127.0.0.1,192.168.1.10).
|
|
78
83
|
ADMIN_IP_ALLOWLIST=
|
|
79
84
|
# Optional browser origin allowlist for CORS (comma-separated).
|
package/README.md
CHANGED
|
@@ -311,7 +311,7 @@ Optional overrides:
|
|
|
311
311
|
codeharbor admin serve --host 127.0.0.1 --port 8787
|
|
312
312
|
```
|
|
313
313
|
|
|
314
|
-
If you bind Admin to a non-loopback host and `ADMIN_TOKEN`
|
|
314
|
+
If you bind Admin to a non-loopback host and both `ADMIN_TOKEN` and `ADMIN_TOKENS_JSON` are empty, startup is rejected by default.
|
|
315
315
|
Explicit bypass exists but is not recommended:
|
|
316
316
|
|
|
317
317
|
```bash
|
|
@@ -336,7 +336,7 @@ Main endpoints:
|
|
|
336
336
|
- `GET /api/admin/health`
|
|
337
337
|
- `GET /api/admin/audit?limit=50`
|
|
338
338
|
|
|
339
|
-
When `ADMIN_TOKEN` is set, requests must include:
|
|
339
|
+
When `ADMIN_TOKEN` or `ADMIN_TOKENS_JSON` is set, requests must include:
|
|
340
340
|
|
|
341
341
|
```http
|
|
342
342
|
Authorization: Bearer <ADMIN_TOKEN>
|
|
@@ -345,9 +345,16 @@ Authorization: Bearer <ADMIN_TOKEN>
|
|
|
345
345
|
Access control options:
|
|
346
346
|
|
|
347
347
|
- `ADMIN_TOKEN`: require bearer token for `/api/admin/*`
|
|
348
|
+
- `ADMIN_TOKENS_JSON`: optional multi-token RBAC list (supports `admin` and `viewer` roles)
|
|
348
349
|
- `ADMIN_IP_ALLOWLIST`: optional comma-separated client IP whitelist (for example `127.0.0.1,192.168.1.10`)
|
|
349
350
|
- `ADMIN_ALLOWED_ORIGINS`: optional CORS origin allowlist for browser-based cross-origin admin access
|
|
350
351
|
|
|
352
|
+
RBAC behavior:
|
|
353
|
+
|
|
354
|
+
- `viewer` tokens can call read endpoints (`GET /api/admin/*`)
|
|
355
|
+
- `admin` tokens can call read + write endpoints (`PUT/POST/DELETE /api/admin/*`)
|
|
356
|
+
- for `ADMIN_TOKENS_JSON`, audit actor is derived from token identity (`actor` field), not `x-admin-actor`
|
|
357
|
+
|
|
351
358
|
Note: `PUT /api/admin/config/global` writes to `.env` and marks changes as restart-required.
|
|
352
359
|
|
|
353
360
|
### Admin UI Quick Walkthrough
|
package/dist/cli.js
CHANGED
|
@@ -646,6 +646,7 @@ var AdminServer = class {
|
|
|
646
646
|
host;
|
|
647
647
|
port;
|
|
648
648
|
adminToken;
|
|
649
|
+
adminTokens;
|
|
649
650
|
adminIpAllowlist;
|
|
650
651
|
adminAllowedOrigins;
|
|
651
652
|
cwd;
|
|
@@ -662,6 +663,7 @@ var AdminServer = class {
|
|
|
662
663
|
this.host = options.host;
|
|
663
664
|
this.port = options.port;
|
|
664
665
|
this.adminToken = options.adminToken;
|
|
666
|
+
this.adminTokens = buildAdminTokenMap(options.adminTokens ?? []);
|
|
665
667
|
this.adminIpAllowlist = normalizeAllowlist(options.adminIpAllowlist ?? []);
|
|
666
668
|
this.adminAllowedOrigins = normalizeOriginAllowlist(options.adminAllowedOrigins ?? []);
|
|
667
669
|
this.cwd = options.cwd ?? process.cwd();
|
|
@@ -753,10 +755,19 @@ var AdminServer = class {
|
|
|
753
755
|
this.sendHtml(res, renderAdminConsoleHtml());
|
|
754
756
|
return;
|
|
755
757
|
}
|
|
756
|
-
|
|
758
|
+
const requiredRole = requiredAdminRoleForRequest(req.method, url.pathname);
|
|
759
|
+
const authIdentity = requiredRole ? this.resolveAdminIdentity(req) : null;
|
|
760
|
+
if (requiredRole && !authIdentity) {
|
|
757
761
|
this.sendJson(res, 401, {
|
|
758
762
|
ok: false,
|
|
759
|
-
error: "Unauthorized. Provide Authorization: Bearer <ADMIN_TOKEN
|
|
763
|
+
error: "Unauthorized. Provide Authorization: Bearer <ADMIN_TOKEN> (or token from ADMIN_TOKENS_JSON)."
|
|
764
|
+
});
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (requiredRole && authIdentity && !hasRequiredAdminRole(authIdentity.role, requiredRole)) {
|
|
768
|
+
this.sendJson(res, 403, {
|
|
769
|
+
ok: false,
|
|
770
|
+
error: "Forbidden. This endpoint requires admin write permission."
|
|
760
771
|
});
|
|
761
772
|
return;
|
|
762
773
|
}
|
|
@@ -770,7 +781,7 @@ var AdminServer = class {
|
|
|
770
781
|
}
|
|
771
782
|
if (req.method === "PUT" && url.pathname === "/api/admin/config/global") {
|
|
772
783
|
const body = await readJsonBody(req);
|
|
773
|
-
const actor =
|
|
784
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
774
785
|
const result = this.updateGlobalConfig(body, actor);
|
|
775
786
|
this.sendJson(res, 200, {
|
|
776
787
|
ok: true,
|
|
@@ -798,13 +809,13 @@ var AdminServer = class {
|
|
|
798
809
|
}
|
|
799
810
|
if (req.method === "PUT") {
|
|
800
811
|
const body = await readJsonBody(req);
|
|
801
|
-
const actor =
|
|
812
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
802
813
|
const room = this.updateRoomConfig(roomId, body, actor);
|
|
803
814
|
this.sendJson(res, 200, { ok: true, data: room });
|
|
804
815
|
return;
|
|
805
816
|
}
|
|
806
817
|
if (req.method === "DELETE") {
|
|
807
|
-
const actor =
|
|
818
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
808
819
|
this.configService.deleteRoomSettings(roomId, actor);
|
|
809
820
|
this.sendJson(res, 200, { ok: true, roomId });
|
|
810
821
|
return;
|
|
@@ -834,7 +845,7 @@ var AdminServer = class {
|
|
|
834
845
|
if (req.method === "POST" && url.pathname === "/api/admin/service/restart") {
|
|
835
846
|
const body = asObject(await readJsonBody(req), "service restart payload");
|
|
836
847
|
const restartAdmin = normalizeBoolean(body.withAdmin, false);
|
|
837
|
-
const actor =
|
|
848
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
838
849
|
try {
|
|
839
850
|
const result = await this.restartServices(restartAdmin);
|
|
840
851
|
this.stateStore.appendConfigRevision(
|
|
@@ -1092,14 +1103,34 @@ var AdminServer = class {
|
|
|
1092
1103
|
summary: normalizeOptionalString(body.summary)
|
|
1093
1104
|
});
|
|
1094
1105
|
}
|
|
1095
|
-
|
|
1096
|
-
if (!this.adminToken) {
|
|
1097
|
-
return
|
|
1106
|
+
resolveAdminIdentity(req) {
|
|
1107
|
+
if (!this.adminToken && this.adminTokens.size === 0) {
|
|
1108
|
+
return {
|
|
1109
|
+
role: "admin",
|
|
1110
|
+
actor: null,
|
|
1111
|
+
source: "open"
|
|
1112
|
+
};
|
|
1098
1113
|
}
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1114
|
+
const token = readAdminToken(req);
|
|
1115
|
+
if (!token) {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
if (this.adminToken && token === this.adminToken) {
|
|
1119
|
+
return {
|
|
1120
|
+
role: "admin",
|
|
1121
|
+
actor: null,
|
|
1122
|
+
source: "legacy"
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const mappedIdentity = this.adminTokens.get(token);
|
|
1126
|
+
if (!mappedIdentity) {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
role: mappedIdentity.role,
|
|
1131
|
+
actor: mappedIdentity.actor,
|
|
1132
|
+
source: "scoped"
|
|
1133
|
+
};
|
|
1103
1134
|
}
|
|
1104
1135
|
isClientAllowed(req) {
|
|
1105
1136
|
if (this.adminIpAllowlist.length === 0) {
|
|
@@ -1391,10 +1422,54 @@ function normalizeHeaderValue(value) {
|
|
|
1391
1422
|
}
|
|
1392
1423
|
return value.trim();
|
|
1393
1424
|
}
|
|
1394
|
-
function
|
|
1425
|
+
function readAdminToken(req) {
|
|
1426
|
+
const authorization = normalizeHeaderValue(req.headers.authorization);
|
|
1427
|
+
if (authorization) {
|
|
1428
|
+
const match = /^bearer\s+(.+)$/i.exec(authorization);
|
|
1429
|
+
const token = match?.[1]?.trim() ?? "";
|
|
1430
|
+
if (token) {
|
|
1431
|
+
return token;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const fromHeader = normalizeHeaderValue(req.headers["x-admin-token"]);
|
|
1435
|
+
return fromHeader || null;
|
|
1436
|
+
}
|
|
1437
|
+
function resolveAuditActor(req, identity) {
|
|
1438
|
+
if (identity?.source === "scoped") {
|
|
1439
|
+
if (identity.actor) {
|
|
1440
|
+
return identity.actor;
|
|
1441
|
+
}
|
|
1442
|
+
return identity.role === "admin" ? "admin-token" : "viewer-token";
|
|
1443
|
+
}
|
|
1395
1444
|
const actor = normalizeHeaderValue(req.headers["x-admin-actor"]);
|
|
1396
1445
|
return actor || null;
|
|
1397
1446
|
}
|
|
1447
|
+
function requiredAdminRoleForRequest(method, pathname) {
|
|
1448
|
+
if (!pathname.startsWith("/api/admin/")) {
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
const normalizedMethod = (method ?? "GET").toUpperCase();
|
|
1452
|
+
if (normalizedMethod === "GET" || normalizedMethod === "HEAD") {
|
|
1453
|
+
return "viewer";
|
|
1454
|
+
}
|
|
1455
|
+
return "admin";
|
|
1456
|
+
}
|
|
1457
|
+
function hasRequiredAdminRole(role, requiredRole) {
|
|
1458
|
+
if (requiredRole === "viewer") {
|
|
1459
|
+
return role === "viewer" || role === "admin";
|
|
1460
|
+
}
|
|
1461
|
+
return role === "admin";
|
|
1462
|
+
}
|
|
1463
|
+
function buildAdminTokenMap(tokens) {
|
|
1464
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
1465
|
+
for (const token of tokens) {
|
|
1466
|
+
mapped.set(token.token, {
|
|
1467
|
+
role: token.role,
|
|
1468
|
+
actor: token.actor
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
return mapped;
|
|
1472
|
+
}
|
|
1398
1473
|
function formatError(error) {
|
|
1399
1474
|
if (error instanceof Error) {
|
|
1400
1475
|
return error.message;
|
|
@@ -5573,6 +5648,7 @@ var CodeHarborAdminApp = class {
|
|
|
5573
5648
|
host: options?.host ?? config.adminBindHost,
|
|
5574
5649
|
port: options?.port ?? config.adminPort,
|
|
5575
5650
|
adminToken: config.adminToken,
|
|
5651
|
+
adminTokens: config.adminTokens,
|
|
5576
5652
|
adminIpAllowlist: config.adminIpAllowlist,
|
|
5577
5653
|
adminAllowedOrigins: config.adminAllowedOrigins
|
|
5578
5654
|
});
|
|
@@ -5583,7 +5659,7 @@ var CodeHarborAdminApp = class {
|
|
|
5583
5659
|
this.logger.info("CodeHarbor admin server started", {
|
|
5584
5660
|
host: address?.host ?? this.config.adminBindHost,
|
|
5585
5661
|
port: address?.port ?? this.config.adminPort,
|
|
5586
|
-
tokenProtected: Boolean(this.config.adminToken)
|
|
5662
|
+
tokenProtected: Boolean(this.config.adminToken) || this.config.adminTokens.length > 0
|
|
5587
5663
|
});
|
|
5588
5664
|
}
|
|
5589
5665
|
async stop() {
|
|
@@ -5688,6 +5764,7 @@ var configSchema = import_zod.z.object({
|
|
|
5688
5764
|
ADMIN_BIND_HOST: import_zod.z.string().default("127.0.0.1"),
|
|
5689
5765
|
ADMIN_PORT: import_zod.z.string().default("8787").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(1).max(65535)),
|
|
5690
5766
|
ADMIN_TOKEN: import_zod.z.string().default(""),
|
|
5767
|
+
ADMIN_TOKENS_JSON: import_zod.z.string().default(""),
|
|
5691
5768
|
ADMIN_IP_ALLOWLIST: import_zod.z.string().default(""),
|
|
5692
5769
|
ADMIN_ALLOWED_ORIGINS: import_zod.z.string().default(""),
|
|
5693
5770
|
LOG_LEVEL: import_zod.z.enum(["debug", "info", "warn", "error"]).default("info")
|
|
@@ -5747,6 +5824,7 @@ var configSchema = import_zod.z.object({
|
|
|
5747
5824
|
adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
|
|
5748
5825
|
adminPort: v.ADMIN_PORT,
|
|
5749
5826
|
adminToken: v.ADMIN_TOKEN.trim() || null,
|
|
5827
|
+
adminTokens: parseAdminTokens(v.ADMIN_TOKENS_JSON),
|
|
5750
5828
|
adminIpAllowlist: parseCsvList(v.ADMIN_IP_ALLOWLIST),
|
|
5751
5829
|
adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
|
|
5752
5830
|
logLevel: v.LOG_LEVEL
|
|
@@ -5837,6 +5915,53 @@ function parseExtraEnv(raw) {
|
|
|
5837
5915
|
function parseCsvList(raw) {
|
|
5838
5916
|
return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
5839
5917
|
}
|
|
5918
|
+
function parseAdminTokens(raw) {
|
|
5919
|
+
const trimmed = raw.trim();
|
|
5920
|
+
if (!trimmed) {
|
|
5921
|
+
return [];
|
|
5922
|
+
}
|
|
5923
|
+
let parsed;
|
|
5924
|
+
try {
|
|
5925
|
+
parsed = JSON.parse(trimmed);
|
|
5926
|
+
} catch {
|
|
5927
|
+
throw new Error("ADMIN_TOKENS_JSON must be valid JSON.");
|
|
5928
|
+
}
|
|
5929
|
+
if (!Array.isArray(parsed)) {
|
|
5930
|
+
throw new Error("ADMIN_TOKENS_JSON must be a JSON array.");
|
|
5931
|
+
}
|
|
5932
|
+
const seenTokens = /* @__PURE__ */ new Set();
|
|
5933
|
+
return parsed.map((entry, index) => {
|
|
5934
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
5935
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}] must be an object.`);
|
|
5936
|
+
}
|
|
5937
|
+
const payload = entry;
|
|
5938
|
+
const tokenValue = payload.token;
|
|
5939
|
+
if (typeof tokenValue !== "string" || !tokenValue.trim()) {
|
|
5940
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}].token must be a non-empty string.`);
|
|
5941
|
+
}
|
|
5942
|
+
const token = tokenValue.trim();
|
|
5943
|
+
if (seenTokens.has(token)) {
|
|
5944
|
+
throw new Error(`ADMIN_TOKENS_JSON contains duplicated token at index ${index}.`);
|
|
5945
|
+
}
|
|
5946
|
+
seenTokens.add(token);
|
|
5947
|
+
let role = "admin";
|
|
5948
|
+
if (payload.role !== void 0) {
|
|
5949
|
+
if (payload.role !== "admin" && payload.role !== "viewer") {
|
|
5950
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}].role must be "admin" or "viewer".`);
|
|
5951
|
+
}
|
|
5952
|
+
role = payload.role;
|
|
5953
|
+
}
|
|
5954
|
+
if (payload.actor !== void 0 && payload.actor !== null && typeof payload.actor !== "string") {
|
|
5955
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}].actor must be a string when provided.`);
|
|
5956
|
+
}
|
|
5957
|
+
const actor = typeof payload.actor === "string" ? payload.actor.trim() || null : null;
|
|
5958
|
+
return {
|
|
5959
|
+
token,
|
|
5960
|
+
role,
|
|
5961
|
+
actor
|
|
5962
|
+
};
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5840
5965
|
|
|
5841
5966
|
// src/config-snapshot.ts
|
|
5842
5967
|
var import_node_fs9 = __toESM(require("fs"));
|
|
@@ -5891,6 +6016,7 @@ var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
|
5891
6016
|
"ADMIN_BIND_HOST",
|
|
5892
6017
|
"ADMIN_PORT",
|
|
5893
6018
|
"ADMIN_TOKEN",
|
|
6019
|
+
"ADMIN_TOKENS_JSON",
|
|
5894
6020
|
"ADMIN_IP_ALLOWLIST",
|
|
5895
6021
|
"ADMIN_ALLOWED_ORIGINS",
|
|
5896
6022
|
"LOG_LEVEL"
|
|
@@ -5955,6 +6081,7 @@ var envSnapshotSchema = import_zod2.z.object({
|
|
|
5955
6081
|
ADMIN_BIND_HOST: import_zod2.z.string(),
|
|
5956
6082
|
ADMIN_PORT: integerStringSchema("ADMIN_PORT", 1, 65535),
|
|
5957
6083
|
ADMIN_TOKEN: import_zod2.z.string(),
|
|
6084
|
+
ADMIN_TOKENS_JSON: jsonArrayStringSchema("ADMIN_TOKENS_JSON", true).default(""),
|
|
5958
6085
|
ADMIN_IP_ALLOWLIST: import_zod2.z.string(),
|
|
5959
6086
|
ADMIN_ALLOWED_ORIGINS: import_zod2.z.string().default(""),
|
|
5960
6087
|
LOG_LEVEL: import_zod2.z.enum(LOG_LEVELS)
|
|
@@ -6143,6 +6270,7 @@ function buildSnapshotEnv(config) {
|
|
|
6143
6270
|
ADMIN_BIND_HOST: config.adminBindHost,
|
|
6144
6271
|
ADMIN_PORT: String(config.adminPort),
|
|
6145
6272
|
ADMIN_TOKEN: config.adminToken ?? "",
|
|
6273
|
+
ADMIN_TOKENS_JSON: serializeAdminTokens(config.adminTokens),
|
|
6146
6274
|
ADMIN_IP_ALLOWLIST: config.adminIpAllowlist.join(","),
|
|
6147
6275
|
ADMIN_ALLOWED_ORIGINS: config.adminAllowedOrigins.join(","),
|
|
6148
6276
|
LOG_LEVEL: config.logLevel
|
|
@@ -6232,6 +6360,9 @@ function parseIntStrict(raw) {
|
|
|
6232
6360
|
function serializeJsonObject(value) {
|
|
6233
6361
|
return Object.keys(value).length > 0 ? JSON.stringify(value) : "";
|
|
6234
6362
|
}
|
|
6363
|
+
function serializeAdminTokens(tokens) {
|
|
6364
|
+
return tokens.length > 0 ? JSON.stringify(tokens) : "";
|
|
6365
|
+
}
|
|
6235
6366
|
function booleanStringSchema(key) {
|
|
6236
6367
|
return import_zod2.z.string().refine((value) => BOOLEAN_STRING.test(value), {
|
|
6237
6368
|
message: `${key} must be a boolean string (true/false).`
|
|
@@ -6269,6 +6400,23 @@ function jsonObjectStringSchema(key, allowEmpty) {
|
|
|
6269
6400
|
message: `${key} must be an empty string or a JSON object string.`
|
|
6270
6401
|
});
|
|
6271
6402
|
}
|
|
6403
|
+
function jsonArrayStringSchema(key, allowEmpty) {
|
|
6404
|
+
return import_zod2.z.string().refine((value) => {
|
|
6405
|
+
const trimmed = value.trim();
|
|
6406
|
+
if (!trimmed) {
|
|
6407
|
+
return allowEmpty;
|
|
6408
|
+
}
|
|
6409
|
+
let parsed;
|
|
6410
|
+
try {
|
|
6411
|
+
parsed = JSON.parse(trimmed);
|
|
6412
|
+
} catch {
|
|
6413
|
+
return false;
|
|
6414
|
+
}
|
|
6415
|
+
return Array.isArray(parsed);
|
|
6416
|
+
}, {
|
|
6417
|
+
message: `${key} must be an empty string or a JSON array string.`
|
|
6418
|
+
});
|
|
6419
|
+
}
|
|
6272
6420
|
|
|
6273
6421
|
// src/preflight.ts
|
|
6274
6422
|
var import_node_child_process6 = require("child_process");
|
|
@@ -6461,11 +6609,12 @@ admin.command("serve").description("Start admin config API server").option("--ho
|
|
|
6461
6609
|
const host = options.host?.trim() || config.adminBindHost;
|
|
6462
6610
|
const port = options.port ? parsePortOption(options.port, config.adminPort) : config.adminPort;
|
|
6463
6611
|
const allowInsecureNoToken = options.allowInsecureNoToken ?? false;
|
|
6464
|
-
|
|
6612
|
+
const hasAdminAuth = Boolean(config.adminToken) || config.adminTokens.length > 0;
|
|
6613
|
+
if (!hasAdminAuth && !allowInsecureNoToken && isNonLoopbackHost(host)) {
|
|
6465
6614
|
process.stderr.write(
|
|
6466
6615
|
[
|
|
6467
|
-
"Refusing to start admin server on non-loopback host without
|
|
6468
|
-
"Fix: set ADMIN_TOKEN in .env, or explicitly pass --allow-insecure-no-token.",
|
|
6616
|
+
"Refusing to start admin server on non-loopback host without admin auth token.",
|
|
6617
|
+
"Fix: set ADMIN_TOKEN or ADMIN_TOKENS_JSON in .env, or explicitly pass --allow-insecure-no-token.",
|
|
6469
6618
|
""
|
|
6470
6619
|
].join("\n")
|
|
6471
6620
|
);
|